I have defined custom Mustache templates to generate a Java application by extending the AbstractJavaCodegen class in a custom Codegen generator.
I aim to generate a custom ServiceImpl.java file and include conditional logic within the method body based on the operationId.
The operationId could be one of the following: "createOrder", "patchOrder", "deleteOrder", "retrieveOrder", or "listOrder". defined in a swagger specification (all the above operationId's are present in the swagger json).
I would like the corresponding logic for each operation to be included, while skipping irrelevant logic for other operations.
For example, here is the structure of my template:
{{#operations}}
public class {{classname}}ServiceImpl implements {{classname}}Service {
{{#operation}}
@Override
public ResponseEntity<{{>returnTypes}}> {{operationId}}({{#allParams}}{{>optionalDataType}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) {
{{#create-operation}}
// Logic for createOrder operation and skip other operation's logic
{{/create-operation}}
{{#patch-operation}}
// Logic for patchOrder operation and skip other operation's logic
{{/patch-operation}}
{{#delete-operation}}
// Logic for deleteOrder operation and skip other operation's logic
{{/delete-operation}}
{{#retrieve-operation}}
// Logic for retrieveOrder operation and skip other operation's logic
{{/retrieve-operation}}
{{#list-operation}}
// Logic for listOrder operation and skip other operation's logic
{{/list-operation}}
return new ResponseEntity<>(HttpStatus.OK);
}
{{/operation}}
Is this achievable using conditional logic in the Mustache template, specifically to ensure that only the relevant operation logic is included for each method based on the operationId?
If not, is there a way we can do this using the custom Codegen?
What have I tried?
I have tried to preprocess the data before passing it to the Mustache template, but it did not work as the way I am expecting.
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
if (!interfaceOnly && SPRING_BOOT.equals(library) && containsEnums()) {
supportingFiles.add(new SupportingFile("converter.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator), "EnumConverterConfiguration.java"));
}
if (!additionalProperties.containsKey(TITLE)) {
// From the title, compute a reasonable name for the package and the API
String title = openAPI.getInfo().getTitle();
// Drop any API suffix
if (title != null) {
title = title.trim().replace(" ", "-");
if (title.toUpperCase(Locale.ROOT).endsWith("API")) {
title = title.substring(0, title.length() - 3);
}
this.title = camelize(sanitizeName(title), LOWERCASE_FIRST_LETTER);
}
additionalProperties.put(TITLE, this.title);
}
if (!additionalProperties.containsKey(SERVER_PORT)) {
final URL url = URLPathUtils.getServerURL(openAPI, serverVariableOverrides());
additionalProperties.put(SERVER_PORT, URLPathUtils.getPort(url, 8080));
}
if (openAPI.getPaths() != null) {
for (final Map.Entry<String, PathItem> openAPIGetPathsEntry : openAPI.getPaths().entrySet()) {
final String pathname = openAPIGetPathsEntry.getKey();
final PathItem path = openAPIGetPathsEntry.getValue();
if (path.readOperations() != null) {
for (final Operation operation : path.readOperations()) {
String operationId = operation.getOperationId();
if(operationId.startsWith("create")) {
additionalProperties.put("create-operation", Boolean.TRUE);
} else if(operationId.startsWith("patch")) {
additionalProperties.put("patch-operation", Boolean.TRUE);
} else if(operationId.startsWith("delete")) {
additionalProperties.put("delete-operation", Boolean.TRUE);
} else if(operationId.startsWith("retrieve")) {
additionalProperties.put("retrieve-operation", Boolean.TRUE);
} else if(operationId.startsWith("list")) {
additionalProperties.put("list-operation", Boolean.TRUE);
}
if (operation.getTags() != null) {
final List<Map<String, String>> tags = new ArrayList<>();
for (final String tag : operation.getTags()) {
final Map<String, String> value = new HashMap<>();
value.put("tag", tag);
tags.add(value);
}
if (operation.getTags().size() > 0) {
final String tag = operation.getTags().get(0);
operation.setTags(Arrays.asList(tag));
}
operation.addExtension("x-tags", tags);
}
}
}
}
}
}
Command to generate the code:
java -Dapis="productOrder" -Dmodels -DsupportingFiles -cp "generators/custom-codegen/target/custom-codegen-openapi-generator-1.0.0.jar;modules/openapi-generator-cli.jar" org.openapitools.codegen.OpenAPIGenerator generate -g custom-codegen -i .\swagger.json -o custom-codegen1 -c .\custom-config.yml
Sample Input: (Swagger.json)
{
"swagger": "2.0",
"info": {
"title": "Ordering",
"version": "4.0.0"
},
"basePath": "/uaf-api/product/v4/",
"paths": {
"/productOrder": {
"get": {
"operationId": "listOrder",
"summary": "List Order objects",
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"operationId": "createOrder",
"summary": "Creates an Order",
"responses": {
"201": {
"description": "Created"
}
}
}
},
"/productOrder/{id}": {
"get": {
"operationId": "retrieveOrder",
"summary": "Retrieves Order by ID",
"responses": {
"200": {
"description": "Success"
}
}
},
"patch": {
"operationId": "patchOrder",
"summary": "Updates partially an Order",
"responses": {
"200": {
"description": "Updated"
}
}
},
"delete": {
"operationId": "deleteOrder",
"summary": "Deletes an Order",
"responses": {
"204": {
"description": "Deleted"
}
}
}
}
}
}
openapi-spec.yaml: (removed components/schemas because of limitations)
openapi: 3.0.1
info:
title: Product Management
description: '**UAF API Reference : UAF 000 - Product Management**'
version: 4.0.0
tags:
- name: productOrder
paths:
/productOrder:
get:
tags:
- productOrder
summary: List or find Order objects
description: This operation list or find ProductOrder entities
operationId: listOrder
parameters:
- name: fields
in: query
description: Comma-separated properties to be provided in response
schema:
type: string
- name: offset
in: query
description: Requested index for start of resources to be provided in response
schema:
type: integer
- name: limit
in: query
description: Requested number of resources to be provided in response
schema:
type: integer
responses:
'200':
description: Success
content:
application/json;charset=utf-8:
schema:
type: array
items:
$ref: '#/components/schemas/ProductOrder'
post:
tags:
- productOrder
summary: Creates an Order
description: This operation creates a ProductOrder entity.
operationId: createOrder
requestBody:
description: The ProductOrder to be created
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/ProductOrderCreate'
required: true
responses:
'201':
description: Created
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/ProductOrder'
/productOrder/{id}:
get:
tags:
- productOrder
summary: Retrieves Order by ID
description: This operation retrieves a ProductOrder entity. Attribute selection
is enabled for all first level attributes.
operationId: retrieveOrder
parameters:
- name: id
in: path
description: Identifier of the ProductOrder
required: true
schema:
type: string
- name: fields
in: query
description: Comma-separated properties to provide in response
schema:
type: string
responses:
'200':
description: Success
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/ProductOrder'
delete:
tags:
- productOrder
summary: Deletes an Order
description: This operation deletes a ProductOrder entity.
operationId: deleteOrder
parameters:
- name: id
in: path
description: Identifier of the ProductOrder
required: true
schema:
type: string
responses:
'204':
description: Deleted
content: {}
patch:
tags:
- productOrder
summary: Updates partially an Order
description: This operation updates partially a ProductOrder entity.
operationId: patchOrder
parameters:
- name: id
in: path
description: Identifier of the ProductOrder
required: true
schema:
type: string
requestBody:
description: The ProductOrder to be updated
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/ProductOrderUpdate'
required: true
responses:
'200':
description: Updated
content:
application/json;charset=utf-8:
schema:
$ref: '#/components/schemas/ProductOrder'
x-original-swagger-version: '2.0'
Current Output:
It repeats the logic for all operations in all the methods, which is not as per my requirement & expected output (mentioned below).
@Override
public ResponseEntity<ProductOrder> createOrder(ProductOrderCreate productOrder) {
// Logic for createOrder operation and skip other operation's logic
// Logic for patchOrder operation and skip other operation's logic
// Logic for deleteOrder operation and skip other operation's logic
// Logic for retrieveOrder operation and skip other operation's logic
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<Void> deleteOrder(String id) {
// Logic for createOrder operation and skip other operation's logic
// Logic for patchOrder operation and skip other operation's logic
// Logic for deleteOrder operation and skip other operation's logic
// Logic for retrieveOrder operation and skip other operation's logic
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<List<ProductOrder>> listOrder(String fields,Integer offset,Integer limit) {
// Logic for createOrder operation and skip other operation's logic
// Logic for patchOrder operation and skip other operation's logic
// Logic for deleteOrder operation and skip other operation's logic
// Logic for retrieveOrder operation and skip other operation's logic
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<ProductOrder> patchOrder(String id,ProductOrderUpdate productOrder) {
// Logic for createOrder operation and skip other operation's logic
// Logic for patchOrder operation and skip other operation's logic
// Logic for deleteOrder operation and skip other operation's logic
// Logic for retrieveOrder operation and skip other operation's logic
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<ProductOrder> retrieveOrder(String id,String fields) {
// Logic for createOrder operation and skip other operation's logic
// Logic for patchOrder operation and skip other operation's logic
// Logic for deleteOrder operation and skip other operation's logic
// Logic for retrieveOrder operation and skip other operation's logic
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
Expected Output:
@Override
public ResponseEntity<ProductOrder> createOrder(ProductOrderCreate productOrder) {
// Logic for createOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<Void> deleteOrder(String id) {
// Logic for deleteOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<List<ProductOrder>> listOrder(String fields,Integer offset,Integer limit) {
// Logic for listOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<ProductOrder> patchOrder(String id,ProductOrderUpdate productOrder) {
// Logic for patchOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public ResponseEntity<ProductOrder> retrieveOrder(String id,String fields) {
// Logic for retrieveOrder operation and skip other operation's logic
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
I'm guessing that the data that are being fed into your template look roughly like the following JSON(ish) representation. I have simplified some details for demonstration purposes.
{
operations: [{
operation: {
returnTypes: 'ProductOrder',
operationId: 'createProductOrder',
allParams: [{
dataType: 'ProductOrderCreate',
paramName: 'productOrder',
'-last': true,
}],
},
}, {
operation: {
returnTypes: 'Void',
operationId: 'deleteProductOrder',
allParams: [{
dataType: 'String',
paramName: 'id',
'-last': true,
}],
},
}, {
operation: {
returnTypes: 'List<ProductOrder>',
operationId: 'listProductOrder',
allParams: [{
dataType: 'String',
paramName: 'fields',
'-last': false,
}, {
dataType: 'Integer',
paramName: 'offset',
'-last': false,
}, {
dataType: 'Integer',
paramName: 'limit',
'-last': true,
}],
},
}, {
operation: {
returnTypes: 'ProductOrder',
operationId: 'patchProductOrder',
allParams: [{
dataType: 'String',
paramName: 'id',
'-last': false,
}, {
dataType: 'ProductOrderUpdate',
paramName: 'productOrder',
'-last': true,
}],
},
}, {
operation: {
returnTypes: 'ProductOrder',
operationId: 'retrieveProductOrder',
allParams: [{
dataType: 'String',
paramName: 'id',
'-last': false,
}, {
dataType: 'String',
paramName: 'fields',
'-last': true,
}],
},
}],
}
In your data preprocessing attempt, you looped over the operations and called additionalProperties.put in order to add properties like create-operation. I don't know OpenAPI Generator, but I suspect this method inserts the property at the root of the input data. So after the first iteration, your input data look like this (pretending for the moment that operations is at the root as well):
{
operations: [{
operation: {
operationId: 'createProductOrder',
// ...
},
}, {
// ...
}],
'create-operation': true,
}
Once the loop is finished, it will look like this:
{
operations: [{
operation: {
operationId: 'createProductOrder',
// ...
},
}, {
// ...
}],
'create-operation': true,
'delete-operation': true,
'list-operation': true,
'patch-operation': true,
'retrieve-operation': true,
}
If you copy-paste the following savestate into the load/store box of the Mustache playground and press the render button, you will see that this indeed produces exactly the result you were getting:
{"data":{"text":"{\n operations: [{\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'createProductOrder',\n allParams: [{\n dataType: 'ProductOrderCreate',\n paramName: 'productOrder',\n '-last': true,\n }],\n },\n }, {\n operation: {\n returnTypes: 'Void',\n operationId: 'deleteProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': true,\n }],\n },\n }, {\n operation: {\n returnTypes: 'List<ProductOrder>',\n operationId: 'listProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'fields',\n '-last': false,\n }, {\n dataType: 'Integer',\n paramName: 'offset',\n '-last': false,\n }, {\n dataType: 'Integer',\n paramName: 'limit',\n '-last': true,\n }],\n },\n }, {\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'patchProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': false,\n }, {\n dataType: 'ProductOrderUpdate',\n paramName: 'productOrder',\n '-last': true,\n }],\n },\n }, {\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'retrieveProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': false,\n }, {\n dataType: 'String',\n paramName: 'fields',\n '-last': true,\n }],\n },\n }],\n 'create-operation': true,\n 'delete-operation': true,\n 'list-operation': true,\n 'patch-operation': true,\n 'retrieve-operation': true,\n}"},"templates":[{"name":"main","text":"{{#operations}}\n{{#operation}}\n@Override\npublic ResponseEntity<{{&returnTypes}}> {{operationId}}({{#allParams}}{{dataType}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) {\n {{#create-operation}}\n // Logic for createOrder operation and skip other operation's logic\n {{/create-operation}}\n {{#patch-operation}}\n // Logic for patchOrder operation and skip other operation's logic\n {{/patch-operation}}\n {{#delete-operation}}\n // Logic for deleteOrder operation and skip other operation's logic\n {{/delete-operation}}\n {{#retrieve-operation}}\n // Logic for retrieveOrder operation and skip other operation's logic\n {{/retrieve-operation}}\n {{#list-operation}}\n // Logic for listOrder operation and skip other operation's logic\n {{/list-operation}}\n return new ResponseEntity<>(HttpStatus.OK);\n}\n \n{{/operation}}\n{{/operations}}"}]}
Why does this happen? Because Mustache works with a context stack. When you iterate over a list or array, lower levels of the input data stay visible. Hence, each of your operations can see each of the create-operation, delete-operation, etcetera properties, causing all optional method bodies to be included in each rendered operation.
The template is correct. You just need to alter your preprocessing so that properties like create-operation are inserted within each individual operation, rather than at the root level. In other words, you want the input data to look like this:
{
operations: [{
operation: {
returnTypes: 'ProductOrder',
operationId: 'createProductOrder',
allParams: [ /*...*/ ],
'create-operation': true,
},
}, {
operation: {
returnTypes: 'Void',
operationId: 'deleteProductOrder',
allParams: [ /*...*/ ],
'delete-operation': true,
},
}, {
operation: {
returnTypes: 'List<ProductOrder>',
operationId: 'listProductOrder',
allParams: [ /*...*/ ],
'list-operation': true,
},
}, {
operation: {
returnTypes: 'ProductOrder',
operationId: 'patchProductOrder',
allParams: [ /*...*/ ],
'patch-operation': true,
},
}, {
operation: {
returnTypes: 'ProductOrder',
operationId: 'retrieveProductOrder',
allParams: [ /*...*/ ],
'retrieve-operation': true,
},
}],
}
The following savestate demonstrates that this would, indeed, produce the desired output:
{"data":{"text":"{\n operations: [{\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'createProductOrder',\n allParams: [{\n dataType: 'ProductOrderCreate',\n paramName: 'productOrder',\n '-last': true,\n }],\n 'create-operation': true,\n },\n }, {\n operation: {\n returnTypes: 'Void',\n operationId: 'deleteProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': true,\n }],\n 'delete-operation': true,\n },\n }, {\n operation: {\n returnTypes: 'List<ProductOrder>',\n operationId: 'listProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'fields',\n '-last': false,\n }, {\n dataType: 'Integer',\n paramName: 'offset',\n '-last': false,\n }, {\n dataType: 'Integer',\n paramName: 'limit',\n '-last': true,\n }],\n 'list-operation': true,\n },\n }, {\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'patchProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': false,\n }, {\n dataType: 'ProductOrderUpdate',\n paramName: 'productOrder',\n '-last': true,\n }],\n 'patch-operation': true,\n },\n }, {\n operation: {\n returnTypes: 'ProductOrder',\n operationId: 'retrieveProductOrder',\n allParams: [{\n dataType: 'String',\n paramName: 'id',\n '-last': false,\n }, {\n dataType: 'String',\n paramName: 'fields',\n '-last': true,\n }],\n 'retrieve-operation': true,\n },\n }],\n}"},"templates":[{"name":"main","text":"{{#operations}}\n{{#operation}}\n@Override\npublic ResponseEntity<{{&returnTypes}}> {{operationId}}({{#allParams}}{{dataType}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) {\n {{#create-operation}}\n // Logic for createOrder operation and skip other operation's logic\n {{/create-operation}}\n {{#patch-operation}}\n // Logic for patchOrder operation and skip other operation's logic\n {{/patch-operation}}\n {{#delete-operation}}\n // Logic for deleteOrder operation and skip other operation's logic\n {{/delete-operation}}\n {{#retrieve-operation}}\n // Logic for retrieveOrder operation and skip other operation's logic\n {{/retrieve-operation}}\n {{#list-operation}}\n // Logic for listOrder operation and skip other operation's logic\n {{/list-operation}}\n return new ResponseEntity<>(HttpStatus.OK);\n}\n \n{{/operation}}\n{{/operations}}"}]}
Unfortunately, since I don't know OpenAPI Generator, I cannot tell you what is the most correct way to get the data in the above shape. Maybe somebody else could add an answer that addresses this.