Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = "de.tum.cit.aet"
version = "1.0.0"
version = "1.1.0"

java {
toolchain {
Expand All @@ -23,7 +23,7 @@ repositories {
val openapiGeneratorCli by configurations.creating

dependencies {
val openapiGeneratorVersion = "7.18.0"
val openapiGeneratorVersion = "7.21.0"

// OpenAPI Generator core dependency
implementation("org.openapitools:openapi-generator:$openapiGeneratorVersion")
Expand Down
53 changes: 48 additions & 5 deletions src/main/java/de/tum/cit/aet/openapi/Angular21Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ public void processOpenAPI(OpenAPI openAPI) {
for (Map.Entry<String, TagUsage> entry : usageByTag.entrySet()) {
String apiFilename = toApiFilename(entry.getKey());
TagUsage usage = entry.getValue();
if (!usage.hasMutation) {
// In httpResource mode a GET-only tag has no mutations, so its API service file would be
// empty and is suppressed. In classical mode the GETs are rendered into the API service,
// so the file must be kept even when the tag has no mutations.
if (useHttpResource && !usage.hasMutation) {
openapiGeneratorIgnoreList.add("api/" + apiFilename + "-api.ts");
}
if (useHttpResource && separateResources && !usage.hasGet) {
Expand Down Expand Up @@ -265,6 +268,29 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
OperationsMap result = super.postProcessOperationsWithModels(objs, allModels);

// Each model must be imported from its own file. The default `imports` entries do not carry a
// per-entry filename, so `{{classFilename}}` in the api templates falls through to the
// enclosing API's filename and every model is (wrongly) imported from the same path. Compute
// the correct model filename per import here.
Object importsObj = result.get("imports");
if (importsObj instanceof List<?> importsList) {
for (Object item : importsList) {
if (item instanceof Map<?, ?> rawImport) {
Object className = rawImport.get("classname");
if (className == null) {
className = rawImport.get("import");
}
if (className != null) {
String simpleName = className.toString();
simpleName = simpleName.substring(simpleName.lastIndexOf('.') + 1);
@SuppressWarnings("unchecked")
Map<String, Object> mutableImport = (Map<String, Object>) rawImport;
mutableImport.put("classFilename", toModelFilename(simpleName));
}
}
}
}

OperationMap operations = result.getOperations();
List<CodegenOperation> ops = operations.getOperation();

Expand All @@ -276,7 +302,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
// Add custom vendor extensions
op.vendorExtensions.put("x-use-inject", useInjectFunction);

if ("GET".equalsIgnoreCase(op.httpMethod)) {
boolean isGet = "GET".equalsIgnoreCase(op.httpMethod);
if (isGet) {
op.vendorExtensions.put("x-is-get", true);
op.vendorExtensions.put("x-use-http-resource", useHttpResource);
getOperations.add(op);
Expand All @@ -286,6 +313,15 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
mutationOperations.add(op);
}

// A GET is rendered in the (classical) API service unless it is delegated to a
// signal-based httpResource. All non-GET operations always live in the API service.
op.vendorExtensions.put("x-render-in-service", !isGet || !useHttpResource);

// HttpClient.post/put/patch require a body argument (pass null when the operation has
// none); get/delete take only the URL. Independent of query params (those go in the URL).
String method = op.httpMethod == null ? "" : op.httpMethod.toUpperCase(Locale.ROOT);
op.vendorExtensions.put("x-needs-body-arg", method.equals("POST") || method.equals("PUT") || method.equals("PATCH"));

// Process path parameters
processPathParameters(op);

Expand Down Expand Up @@ -386,7 +422,10 @@ private String buildPathTemplate(CodegenOperation op, boolean useSignalValue) {
} else if (useSignalValue && signalValueByParamName.containsKey(valueVar)) {
replacementVar = signalValueByParamName.get(valueVar);
}
String replacement = "{" + replacementVar + "}";
// Emit a template-literal interpolation (${...}); the surrounding template wraps the path
// in a `${this.basePath}...` template string. Without the leading $, non-numeric path
// params were rendered as literal "{paramPath}" text (and their encode const went unused).
String replacement = "${" + replacementVar + "}";
matcher.appendReplacement(buffer, Matcher.quoteReplacement(replacement));
}
matcher.appendTail(buffer);
Expand All @@ -400,8 +439,12 @@ private String buildPathTemplate(CodegenOperation op, boolean useSignalValue) {
if (templateVarByParamName.containsKey(param.paramName)) {
valueVar = templateVarByParamName.get(param.paramName);
}
String placeholder = "{" + param.baseName + "}";
path = path.replace(placeholder, "${" + valueVar + "}");
// Fallback for any raw "{baseName}" placeholders not already turned into a
// template-literal interpolation by the encodeParam pass above. The negative
// lookbehind avoids matching the "{baseName}" inside an already-produced
// "${baseName}" (which would yield a doubled "$${baseName}").
String placeholder = "(?<!\\$)" + Pattern.quote("{" + param.baseName + "}");
path = path.replaceAll(placeholder, Matcher.quoteReplacement("${" + valueVar + "}"));
}
}

Expand Down
24 changes: 12 additions & 12 deletions src/main/resources/angular21/api-service.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class {{classname}} {

{{#operations}}
{{#operation}}
{{^vendorExtensions.x-is-get}}
{{#vendorExtensions.x-render-in-service}}
/**
* {{summary}}
* {{notes}}
Expand Down Expand Up @@ -61,26 +61,26 @@ export class {{classname}} {
{{/isArray}}
}
{{/formParams}}
return this.http.{{httpMethod}}{{#returnType}}<{{{.}}}>{{/returnType}}(url, formData);
return this.http.{{httpMethod}}<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}>(url, formData);
{{/hasFormParams}}
{{^hasFormParams}}
{{#bodyParam}}
return this.http.{{httpMethod}}{{#returnType}}<{{{.}}}>{{/returnType}}(url, {{paramName}});
return this.http.{{httpMethod}}<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}>(url, {{paramName}});
{{/bodyParam}}
{{^bodyParam}}
{{#hasQueryParams}}
return this.http.{{httpMethod}}{{#returnType}}<{{{.}}}>{{/returnType}}(url);
{{/hasQueryParams}}
{{^hasQueryParams}}
{{#vendorExtensions.x-is-mutation}}
return this.http.{{httpMethod}}{{#returnType}}<{{{.}}}>{{/returnType}}(url{{#isBodyAllowed}}, null{{/isBodyAllowed}});
{{/vendorExtensions.x-is-mutation}}
{{/hasQueryParams}}
{{! POST/PUT/PATCH require a body argument even when the operation has none (pass null). }}
{{! GET/DELETE take only the URL. Query parameters live in the URL, not the body. }}
{{#vendorExtensions.x-needs-body-arg}}
return this.http.{{httpMethod}}<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}>(url, null);
{{/vendorExtensions.x-needs-body-arg}}
{{^vendorExtensions.x-needs-body-arg}}
return this.http.{{httpMethod}}<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}void{{/returnType}}>(url);
{{/vendorExtensions.x-needs-body-arg}}
{{/bodyParam}}
{{/hasFormParams}}
}

{{/vendorExtensions.x-is-get}}
{{/vendorExtensions.x-render-in-service}}
{{/operation}}
{{/operations}}
}
4 changes: 2 additions & 2 deletions src/main/resources/angular21/model.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{{#models}}
{{#model}}
{{#tsImports}}
import type { {{classname}} } from '{{filename}}';
import type { {{classname}} } from './{{filename}}';
{{/tsImports}}

{{#description}}
Expand All @@ -22,7 +22,7 @@ export interface {{classname}}{{#parent}} extends {{.}}{{/parent}} {
{{#description}}
/** {{{.}}} */
{{/description}}
{{#vendorExtensions.x-is-readonly}} readonly {{/vendorExtensions.x-is-readonly}}{{^vendorExtensions.x-is-readonly}} {{/vendorExtensions.x-is-readonly}}{{name}}{{^required}}?{{/required}}: {{#isEnum}}{{classname}}{{enumName}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}};
{{#vendorExtensions.x-is-readonly}} readonly {{/vendorExtensions.x-is-readonly}}{{^vendorExtensions.x-is-readonly}} {{/vendorExtensions.x-is-readonly}}{{name}}{{^required}}?{{/required}}: {{#isEnum}}{{#isArray}}Array<{{/isArray}}{{classname}}{{enumName}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}} | null{{/isNullable}};
{{/vars}}
}
{{#hasEnums}}
Expand Down
Loading