mustacheopenapi-generatorswagger-codegen

Conditional Logic in Mustache Template?


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);
  }

Solution

  • 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.