Skip to content
Open
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
73 changes: 68 additions & 5 deletions src/main/java/com/twilio/oai/DirectoryStructureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class DirectoryStructureService {
@Getter
private boolean isVersionLess = false;
private final Map<String, String> productMap = new HashMap<>();
private OpenAPI openAPI;
private final List<CodegenModel> allModels = new ArrayList<>();
private final List<Object> dependentList = new ArrayList<>();

Expand Down Expand Up @@ -78,10 +79,15 @@ public static class ContextResource {
}

public void configure(final OpenAPI openAPI) {
this.openAPI = openAPI;
final Map<String, DependentResource> versionResources = getVersionResourcesMap();
Map<String, PathItem> pathsToSkipMap = new HashMap<>();

isVersionLess = additionalProperties.getOrDefault(API_VERSION, "").equals("");
additionalProperties.put("isVersionLessSpec", isVersionLess ? "true" : "false");

final boolean isV1ApiSpec = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1();
additionalProperties.put("isV1ApiSpec", isV1ApiSpec ? "true" : "false");

openAPI.getPaths().forEach(resourceTree::addResource);
openAPI.getPaths().forEach((name, path) -> {
Expand All @@ -104,7 +110,7 @@ public void configure(final OpenAPI openAPI) {
operation.addTagsItem(tag);

if (!tag.contains(PATH_SEPARATOR_PLACEHOLDER)) {
final DependentResource dependent = generateDependent(name, operation);
final DependentResource dependent = generateDependent(name, path, operation);
final boolean isIgnoredOperation = Optional.ofNullable(operation.getExtensions())
.map(ext -> ext.get(IGNORE_EXTENSION_NAME))
.map(Boolean.class::cast)
Expand Down Expand Up @@ -137,10 +143,15 @@ public void configure(final OpenAPI openAPI) {
}

public void addVersionResources(DependentResource dependent, Map<String, DependentResource> versionResources) {
final boolean isV1ApiSpec = "true".equals(additionalProperties.get("isV1ApiSpec"));
if (versionResources.containsKey(dependent.getFilename())) {
DependentResource existingDependent = versionResources.get(dependent.getFilename());
if (existingDependent.getPathParams().size() == 0)
// For v1Api specs: also replace when new entry has listWithPathParams=true and existing
// does not — handles instance path processed before list path (spec ordering issue)
if (existingDependent.getPathParams().isEmpty()
|| (isV1ApiSpec && dependent.isListWithPathParams() && !existingDependent.isListWithPathParams())) {
versionResources.put(dependent.getFilename(), dependent);
}
} else {
versionResources.put(dependent.getFilename(), dependent);
}
Expand Down Expand Up @@ -183,8 +194,12 @@ private Stream<Parameter> getParamStream(final Operation operation) {
}

public DependentResource generateDependent(final String path, final Operation operation) {
return generateDependent(path, null, operation);
}

public DependentResource generateDependent(final String path, final PathItem pathItem, final Operation operation) {
final Resource.Aliases resourceAliases = getResourceAliases(path, operation);
List<Parameter> params = fetchNonParentPathParams(operation);
List<Parameter> params = fetchNonParentPathParams(pathItem, operation);
return new DependentResource.DependentResourceBuilder()
.version(PathUtils.getFirstPathPart(path))
.type(resourceAliases.getClassName() + LIST_INSTANCE)
Expand Down Expand Up @@ -225,10 +240,38 @@ public void addContextdependents(final List<Object> resourceList, final String p
}

private List<Parameter> fetchNonParentPathParams(Operation operation) {
return fetchNonParentPathParams(null, operation);
}

private List<Parameter> fetchNonParentPathParams(PathItem pathItem, Operation operation) {
List<Parameter> params = new ArrayList<>();
if (null == operation) return params;
List<Parameter> pathParams = Optional.ofNullable(operation.getParameters())
.stream().flatMap(Collection::stream)

// For v1Api specs: merge path-item level params (which may use $ref components) with
// operation-level params so nested list resources can detect their path parameters.
final boolean isV1ApiSpec = "true".equals(additionalProperties.get("isV1ApiSpec"));
List<Parameter> allParams = new ArrayList<>();
if (isV1ApiSpec && pathItem != null && pathItem.getParameters() != null) {
pathItem.getParameters().stream()
.map(this::resolveParameterRef)
.filter(Objects::nonNull)
.forEach(allParams::add);
}
if (operation.getParameters() != null) {
Set<String> operationParamNames = operation.getParameters().stream()
.map(this::resolveParameterRef)
.filter(Objects::nonNull)
.filter(p -> p.getName() != null)
.map(Parameter::getName)
.collect(Collectors.toSet());
allParams.removeIf(p -> p.getName() != null && operationParamNames.contains(p.getName()));
operation.getParameters().stream()
.map(this::resolveParameterRef)
.filter(Objects::nonNull)
.forEach(allParams::add);
}

List<Parameter> pathParams = allParams.stream()
.filter(param -> Objects.nonNull(param.getIn())).filter(PathUtils::isPathParam)
.collect(Collectors.toList());
params = pathParams.stream().filter(parameter -> Objects.isNull(parameter.getExtensions()))
Expand All @@ -239,6 +282,26 @@ private List<Parameter> fetchNonParentPathParams(Operation operation) {
return params;
}

/**
* Resolves a $ref parameter stub to its full definition from openAPI components.
* Returns the parameter as-is if it is already fully defined (no $ref).
*/
private Parameter resolveParameterRef(Parameter param) {
if (param == null) return null;
if (param.get$ref() != null && param.getIn() == null) {
// extract component name from e.g. "#/components/parameters/StoreId"
String ref = param.get$ref();
String componentName = ref.substring(ref.lastIndexOf('/') + 1);
if (openAPI != null
&& openAPI.getComponents() != null
&& openAPI.getComponents().getParameters() != null) {
return openAPI.getComponents().getParameters().get(componentName);
}
return null;
}
return param;
}

private Resource.Aliases getResourceAliases(final String path, final Operation operation) {
return resourceTree
.findResource(path)
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/twilio/oai/api/ApiResources.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ public ApiResources(ApiResourceBuilder apiResourceBuilder) {
}
responseInstanceModels = apiResourceBuilder.responseInstanceModels;
}

public Boolean getIsApiV1() {
return isApiV1;
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/twilio/oai/api/FluentApiResourceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;

import com.twilio.oai.java.cache.ResourceCacheContext;
import static com.twilio.oai.common.ApplicationConstants.IS_PARENT_PARAM_EXTENSION_NAME;
import static com.twilio.oai.common.ApplicationConstants.STRING;
import static com.twilio.oai.template.AbstractApiActionTemplate.API_TEMPLATE;

Expand Down Expand Up @@ -109,6 +111,20 @@ public ApiResourceBuilder updateOperations(final Resolver<CodegenParameter> code
instancePathParams.add(param);
}
}

// For v1Api specs only: mark params shared between list and instance paths as parent params.
// These are already provided when the list instance is constructed, so they
// should not appear in the instance context callable (e.g. get(profileId) not get(storeId, profileId)).
// Scoped to v1Api to avoid breaking existing non-v1Api SDKs.
final boolean isApiV1 = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1();
if (isApiV1) {
final Set<String> listParamNames = listPathParams.stream()
.map(p -> p.paramName)
.collect(Collectors.toSet());
instancePathParams.stream()
.filter(p -> listParamNames.contains(p.paramName))
.forEach(p -> p.vendorExtensions.put(IS_PARENT_PARAM_EXTENSION_NAME, true));
}
}

return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ public ApiResourceBuilder updateResponseModel(Resolver<CodegenProperty> codegenP
.stream()
.flatMap(co -> co.responses
.stream()
.filter(response -> !isApiV1 || (response.code != null && response.code.startsWith("2")))
.map(response -> response.dataType)
.filter(Objects::nonNull)
.flatMap(modelName -> getModel(modelName, co).stream())
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/twilio/oai/api/PythonApiResourceBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,23 @@ public PythonApiResourceBuilder(final IApiActionTemplate template,
public ApiResourceBuilder updateOperations(final Resolver<CodegenParameter> codegenParameterIResolver) {
super.updateOperations(codegenParameterIResolver);
updatePaths();
boolean isApiV1 = ResourceCacheContext.get() != null && ResourceCacheContext.get().isV1();
for (final CodegenOperation co : codegenOperationList) {
co.httpMethod = co.httpMethod.toLowerCase();
updateNamespaceSubPart(co);
if (co.operationId.startsWith("list")) {
addOperationName(co, "Page");
}

// Mark delete operations with response body for V1 APIs
if (isApiV1 && co.operationId.toLowerCase().startsWith("delete")) {
boolean hasResponseBody = co.responses != null && co.responses.stream()
.anyMatch(response -> response.is2xx && response.getContent() != null && !response.getContent().isEmpty());
if (hasResponseBody) {
co.vendorExtensions.put("x-delete-has-response-body", true);
}
}

for (CodegenParameter cp : co.allParams) {
if (cp.paramName.equals("from")) {
cp.paramName = "from_";
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/twilio/oai/java/cache/ResourceCache2.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class ResourceCache2 {
@Getter
@Setter
private Set<CodegenProperty> responsePatch = new TreeSet<>((p1, p2) -> p1.baseName.compareTo(p2.baseName));
@Getter
@Setter
private Set<CodegenProperty> responseDelete = new TreeSet<>((p1, p2) -> p1.baseName.compareTo(p2.baseName));

@Getter
private ArrayList<CodegenModel> allModelsByDefaultGenerator = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ public class V1JsonResponseProcessor implements ResponseProcessor {

@Override
public void process(CodegenOperation codegenOperation) {
// delete operation does not have response body
if (codegenOperation.operationId.toLowerCase().startsWith("delete")) return;
List<CodegenModel> allModels = ResourceCacheContext.get().getAllModelsByDefaultGenerator();
CodegenResponse response = codegenOperation.responses.stream()
.filter(codegenResponse -> codegenResponse.is2xx || codegenResponse.is3xx)
.findFirst().get();
.findFirst().orElse(null);
if (response == null || response.getContent() == null) return;

String modelName = response.dataType;
Expand All @@ -38,6 +36,14 @@ public void process(CodegenOperation codegenOperation) {
recursiveModelProcessor.process(property);
}
String operationId = codegenOperation.operationId;

// Mark delete operations that have response body for special handling in Python
if (operationId.toLowerCase().startsWith("delete")) {
codegenOperation.vendorExtensions.put("x-delete-has-response-body", true);
codegenModel.vars.forEach(ResourceCacheContext.get().getResponseDelete()::add);
return;
}

// Adding responseModel vars to cache
if (operationId.toLowerCase().startsWith("create")) {
codegenModel.vars.forEach(ResourceCacheContext.get().getResponseCreate()::add);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,10 @@ public void addSupportVersion() {
final String fileExtension = templateStrings.get(1);
final String apiVersionClass = codegen.additionalProperties().get("apiVersionClass").toString();

if (apiVersionClass.startsWith("V")) {
final boolean isVersionLess = "true".equals(
String.valueOf(codegen.additionalProperties().getOrDefault("isVersionLessSpec", "false")));

if (!isVersionLess) {
codegen
.supportingFiles()
.add(new SupportingFile(templateName,
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/twilio-node/api-single.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export class {{apiName}}ContextImpl implements {{apiName}}Context {
}
{{#hasMultipleResponseModels}}
{{#models}}
{{^isEnum}}
/**
* Nested model for {{name}}
*/
Expand All @@ -131,6 +132,7 @@ export interface {{name}} {
{{/vars}}
}

{{/isEnum}}
{{/models}}
{{/hasMultipleResponseModels}}

Expand Down
9 changes: 5 additions & 4 deletions src/main/resources/twilio-node/operation.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
{{^hasRequiredParams}}
if (params instanceof Function) {
callback = params;
params = {};
params = {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}};
} else {
params = params || {};
params = params || {}{{^vendorExtensions.x-is-read-operation}}{{#isApiV1}}{{#bodyParam}} as Partial<{{dataType}}> as {{dataType}}{{/bodyParam}}{{^bodyParam}} as any{{/bodyParam}}{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}};
}

{{/hasRequiredParams}}
Expand Down Expand Up @@ -53,7 +53,8 @@
{{/bodyParams}}
{{/hasParams}}
{{^hasParams}}
const headers: any = {};
const headers: any = {};{{#vendorExtensions.x-is-read-operation}}{{#isApiV1}}
const data: any = {};{{/isApiV1}}{{/vendorExtensions.x-is-read-operation}}
{{#consumes}}
{{#-first}}
headers["Content-Type"] = "{{{mediaType}}}"
Expand All @@ -70,7 +71,7 @@
const instance = this;
{{/vendorExtensions.x-is-context-operation}}
let operationVersion = {{#vendorExtensions.x-is-context-operation}}instance._version{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}version{{/vendorExtensions.x-is-list-operation}},
operationPromise = operationVersion.{{vendorExtensions.x-name-lower}}({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers});
operationPromise = operationVersion.{{#vendorExtensions.x-delete-returns-model}}fetch{{/vendorExtensions.x-delete-returns-model}}{{^vendorExtensions.x-delete-returns-model}}{{vendorExtensions.x-name-lower}}{{/vendorExtensions.x-delete-returns-model}}({ uri: instance._uri, method: "{{httpMethod}}"{{#hasParams}}, {{^isBodyAllowed}}params: {{/isBodyAllowed}}data{{/hasParams}}, headers});
{{^vendorExtensions.x-is-read-operation}}{{#vendorExtensions.x-delete-returns-model}}
operationPromise = operationPromise.then(payload => new {{instanceName}}(operationVersion, payload{{#vendorExtensions.x-is-context-operation}}{{#instancePathParams}}, instance._solution.{{paramName}}{{/instancePathParams}}{{/vendorExtensions.x-is-context-operation}}{{#vendorExtensions.x-is-list-operation}}{{#listPathParams}}, instance._solution.{{paramName}}{{/listPathParams}}{{/vendorExtensions.x-is-list-operation}}));
{{/vendorExtensions.x-delete-returns-model}}{{^vendorExtensions.x-is-delete-operation}}
Expand Down
Loading
Loading