Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2026-present Coinbase Global, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.coinbase.tools.modelgenerator;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/** Resolved Java SDK ownership and naming for one OpenAPI operation. */
public final class OperationBinding {
private final String operationId;
private final String serviceFolder;
private final String serviceName;
private final String sdkMethod;
private final boolean omitRequest;
private final boolean paginated;
private final Map<String, String> parameterTypeOverrides;

OperationBinding(
String operationId,
String serviceFolder,
String serviceName,
String sdkMethod,
boolean omitRequest,
boolean paginated,
Map<String, String> parameterTypeOverrides) {
this.operationId = operationId;
this.serviceFolder = serviceFolder;
this.serviceName = serviceName;
this.sdkMethod = sdkMethod;
this.omitRequest = omitRequest;
this.paginated = paginated;
this.parameterTypeOverrides = Collections.unmodifiableMap(new LinkedHashMap<>(parameterTypeOverrides));
}

public String operationId() { return operationId; }
public String serviceFolder() { return serviceFolder; }
public String serviceName() { return serviceName; }
public String sdkMethod() { return sdkMethod; }
public boolean omitRequest() { return omitRequest; }
public boolean paginated() { return paginated; }
public Map<String, String> parameterTypeOverrides() { return parameterTypeOverrides; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2026-present Coinbase Global, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.coinbase.tools.modelgenerator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/** Derives deterministic Java SDK names from parsed OpenAPI operations. */
public final class OperationBindingGenerator {
private static final String OPERATION_ID_PREFIX = "PrimeRESTAPI_";
private static final Map<String, String> METHOD_RENAMES = new HashMap<>();

static {
METHOD_RENAMES.put("CancelFuturesSweep", "CancelEntityFuturesSweep");
METHOD_RENAMES.put("CreateOnchainAddressGroup", "CreateOnchainAddressBookEntry");
METHOD_RENAMES.put("CreatePortfolioAddressBookEntry", "CreateAddressBookEntry");
METHOD_RENAMES.put("CreateQuoteRequest", "CreateQuote");
METHOD_RENAMES.put("GetAllocationsByClientNettingId", "ListAllocationsByNettingId");
METHOD_RENAMES.put("GetEntityAssets", "ListAssets");
METHOD_RENAMES.put("GetEntityPaymentMethodDetails", "GetPaymentMethodDetails");
METHOD_RENAMES.put("GetEntityUsers", "ListEntityUsers");
METHOD_RENAMES.put("GetFuturesSweeps", "ListEntityFuturesSweeps");
METHOD_RENAMES.put("GetLocateAvailabilities", "GetEntityLocateAvailabilities");
METHOD_RENAMES.put("GetMarginSummaries", "ListMarginCallSummaries");
METHOD_RENAMES.put("GetPortfolioAddressBook", "ListAddressBook");
METHOD_RENAMES.put("GetPortfolioInterestAccruals", "ListInterestAccrualsForPortfolio");
METHOD_RENAMES.put("GetPostTradeCredit", "GetPortfolioCreditInformation");
METHOD_RENAMES.put("GetTFTieredPricingFees", "GetTradeFinanceTieredPricingFees");
METHOD_RENAMES.put("ListTFObligations", "ListTradeFinanceObligations");
METHOD_RENAMES.put("OrderPreview", "GetOrderPreview");
METHOD_RENAMES.put("ScheduleFuturesSweep", "ScheduleEntityFuturesSweep");
METHOD_RENAMES.put("UpdateOnchainAddressGroup", "UpdateOnchainAddressBookEntry");
}

private OperationBindingGenerator() {}

public static List<OperationBinding> deriveAll(SpecModels.Document document) {
List<OperationBinding> bindings = new ArrayList<>();
for (SpecModels.Operation operation : document.operations()) bindings.add(derive(operation));
bindings.sort(Comparator.comparing(OperationBinding::operationId));
OperationBindingValidator.validate(document, bindings);
return Collections.unmodifiableList(bindings);
}

static OperationBinding derive(SpecModels.Operation operation) {
String tag = operation.tags().isEmpty() ? "Misc" : operation.tags().get(0);
String folder = "Travel Rule".equals(tag) ? "transactions" : tag.replaceAll("[^A-Za-z0-9]", "").replace(" ", "").toLowerCase(Locale.ROOT);
String serviceName = pascal(tag) + "Service";
String raw = operation.sdkMethodName().isEmpty() ? operation.operationId().replaceFirst("^" + OPERATION_ID_PREFIX, "") : operation.sdkMethodName();
String method = METHOD_RENAMES.getOrDefault(raw, raw);
if (operation.httpMethod().equals("GET") && method.startsWith("Get") && operation.summary().startsWith("List ")) method = "List" + method.substring(3);
boolean omitRequest = operation.parameters().isEmpty() && operation.requestBodySchema().isEmpty();
boolean paginated = operation.parameters().stream().anyMatch(p -> p.name().equals("cursor") || p.name().equals("sort_direction"));
return new OperationBinding(operation.operationId(), folder, serviceName, method, omitRequest, paginated, new LinkedHashMap<>());
}

private static String pascal(String value) {
StringBuilder result = new StringBuilder();
for (String part : Arrays.asList(value.replaceAll("[^A-Za-z0-9]+", " ").split(" +"))) {
if (!part.isEmpty()) result.append(Character.toUpperCase(part.charAt(0))).append(part.substring(1));
}
return result.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2026-present Coinbase Global, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.coinbase.tools.modelgenerator;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Fails early when an operation cannot safely own one Java SDK surface. */
public final class OperationBindingValidator {
private static final Pattern PATH_PARAMETER = Pattern.compile("\\{([^}]+)}");

private OperationBindingValidator() {}

public static void validate(SpecModels.Document document, List<OperationBinding> bindings) {
if (document.operations().size() != bindings.size()) {
throw new IllegalArgumentException("Every OpenAPI operation must have exactly one binding");
}
Set<String> operationIds = new HashSet<>();
Set<String> serviceMethods = new HashSet<>();
for (int index = 0; index < document.operations().size(); index++) {
SpecModels.Operation operation = document.operations().get(index);
OperationBinding binding = bindings.get(index);
if (!operationIds.add(binding.operationId())) throw new IllegalArgumentException("Duplicate operation binding: " + binding.operationId());
if (!operation.operationId().equals(binding.operationId())) throw new IllegalArgumentException("Bindings must remain operation-ID sorted");
if (!serviceMethods.add(binding.serviceFolder() + ":" + binding.sdkMethod())) {
throw new IllegalArgumentException("Duplicate Java service method: " + binding.serviceFolder() + ":" + binding.sdkMethod());
}
Set<String> parameterNames = new HashSet<>();
for (SpecModels.Parameter parameter : operation.parameters()) parameterNames.add(parameter.name());
Matcher matcher = PATH_PARAMETER.matcher(operation.path());
while (matcher.find()) {
if (!parameterNames.contains(matcher.group(1))) {
throw new IllegalArgumentException(operation.operationId() + " is missing path parameter " + matcher.group(1));
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2026-present Coinbase Global, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.coinbase.tools.modelgenerator;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;

class OperationBindingGeneratorTest {
@Test
void derivesStableBindingsForTheCommittedSpec() throws Exception {
Path root = Path.of(System.getProperty("user.dir")).toAbsolutePath().getParent().getParent();
List<OperationBinding> bindings = OperationBindingGenerator.deriveAll(
SpecParser.load(root.resolve("apiSpec/prime-public-spec.yaml")));

assertEquals(103, bindings.size());
OperationBinding createOrder = bindings.stream()
.filter(binding -> binding.operationId().equals("PrimeRESTAPI_CreateOrder"))
.findFirst().orElseThrow();
assertEquals("orders", createOrder.serviceFolder());
assertEquals("OrdersService", createOrder.serviceName());
assertEquals("CreateOrder", createOrder.sdkMethod());
assertTrue(!createOrder.omitRequest());

OperationBinding travelRule = bindings.stream()
.filter(binding -> binding.operationId().equals("PrimeRESTAPI_SubmitDepositTravelRuleData"))
.findFirst().orElseThrow();
assertEquals("transactions", travelRule.serviceFolder());
}
}
Loading