Course – LS (cat=REST)

Get started with Spring and Spring Boot, through the reference Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we’ll continue to explore OpenAPI Generator‘s customization options. This time, we’ll show how the required steps to create a new generator that creates REST Producer routes for Apache Camel-based applications.

2. Why Create a New Generator?

In a previous tutorial, we’ve shown how to customize an existing generator’s template to suit a particular use case.

Sometimes, however, we’ll be faced with a situation where can’t use any of the existing generators. For example, this is the case when we need to target a new language or REST framework.

As a concrete example, the current OpenAPI Generator version support for the Apache Camel’s integration framework only supports the generation of Consumer routes. In Camel’s parlance, these are routes that receive a REST request and then send it to the mediation logic.

Now, if we want to invoke a REST API from a route, we’ll typically use Camel’s REST Component. This is how such invocation would look like using the DSL:

from(GET_QUOTE)
  .id(GET_QUOTE_ROUTE_ID)
  .to("rest:get:/quotes/{symbol}?outType=com.baeldung.tutorials.openapi.quotes.api.model.QuoteResponse");

We can see that some aspects of this code that would benefit from automatic generation:

  • Deriving endpoint parameters from the API definition
  • Specifying input and output types
  • Response payload validation
  • Consistent route and id naming across projects

Moreover, using code generation to address those cross-cutting concerns ensures that, as the called API evolves over time, the generated code will always be in sync with the contract.

3. Creating an OpenAPI Generator Project

From OpenAPI’s point of view, a custom generator is just a regular Java class that implements the CodegenConfig interface. Let’s start our project by bringing in the required dependencies:

<dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator</artifactId>
    <version>7.5.0</version>
    <scope>provided</scope>
</dependency>

The latest version of this dependency is available on Maven Central.

At runtime, the generator’s core logic uses the JRE’s standard Service mechanism to find and register all available implementations. This means that we must create a file under META-INF/services having the fully qualified name of our CodegenConfig implementation. When using a standard Maven project layout, this file goes under the src/main/resources folder.

The OpenAPI generator tool also supports generating a maven-based custom generator project. This is how we can bootstrap a project using just a few shell commands:

mkdir -p target wget -O target/openapi-generator-cli.jar
  https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.5.0/openapi-generator-cli-7.5.0.jar
  java -jar target/openapi-generator-cli.jar meta
  -o . -n java-camel-client -p com.baeldung.openapi.generators.camelclient

4. Implementing the Generator

As mentioned above, our generator must implement the CodegenConfig interface. However, if we look at it, we might feel a bit intimidated. After all, it has a whopping 155 methods!

Fortunately, the core logic already provides the DefaultCodegen class that we can extend. This greatly simplifies our task, as all we have to do is to override a few methods to get a working generator.

public class JavaCamelClientGenerator extends DefaultCodegen {
    // override methods as required
}

4.1. Generator Metadata

The very first methods we should implement are getName() and getTag(). The first one should return a friendly name that users will use to inform the integration plugins or the CLI tool they want to use our generator. A common convention is to use a three-part identifier consisting of the target language, REST library/framework, and kind – client or server:

public String getName() {
    return "java-camel-client";
}

As for the getTag() method, we should return a value from the CodegenType enum that matches the kind of generated code which, for us, is CLIENT:

public CodegenType getTag() {
    return CodegenType.CLIENT;
}

4.2. Help Instructions

An important aspect, usability-wise, is providing end users with helpful information about our generator’s purpose and options. We should return this information using the getHelp() method.

Here we’ll just return a brief description of its purpose, but a full implementation would add additional details and, ideally, a link to online documentation:

public String getHelp() {
    return "Generates Camel producer routes to invoke API operations.";
}

4.3. Destination Folders

Given an API definition, the generator will output several artifacts:

  • API implementation (client or server)
  • API tests
  • API documentation
  • Models
  • Model tests
  • Model documentation

For each artifact type, there’s a corresponding method that returns the path where the generated path will go. Let’s take a look at the implementation of two of these methods:

@Override
public String modelFileFolder() {
    return outputFolder() + File.separator + sourceFolder + 
      File.separator + modelPackage().replace('.', File.separatorChar);
}

@Override
public String apiFileFolder() {
    return outputFolder() + File.separator + sourceFolder + 
      File.separator + apiPackage().replace('.', File.separatorChar);
}

In both cases, we use the inherited outputFolder() method as a starting point and then append the sourceFolder – more on this field later – and the destination package converted to a path.

At runtime, the value of those parts will come from configuration options passed to the tool either through command line options or the available integrations (Maven, Gradle, etc.).

4.4. Template Locations

As we’ve seen in the template customization tutorial, every generator uses a set of templates to generate the target artifacts. For built-in generators, we can replace the templates, but we can’t rename or add new ones.

Custom generators, on the other hand, don’t have this limitation. At construction time, we can register as many as we want using one of the xxxTemplateFiles() methods.

Each of these xxxTemplateFIles() methods returns a modifiable map to which we can add our templates. Each map entry has the template name as its key and the generated file extension as its value.

For our Camel generator, this is how the producer template registration looks like:

public public JavaCamelClientGenerator() {
    super(); 
    // ... other configurations omitted
    apiTemplateFiles().put("camel-producer.mustache",".java");
    // ... other configurations omitted
}

This snippet registers a template named camel-producer.mustache that will be invoked for every API defined in the input document. The resulting file will be named after the API’s name, followed by the given extension (“.java”, in this case). 

Notice that there’s no requirement that the extension starts with a dot character. We can use this fact to generate multiple files for a given API.

We must also configure the base location for our templates using setTemplateDir(). A good convention is to use the generator’s name, this avoiding collisions with any of the built-in generators:

setTemplateDir("java-camel-client");

4.5. Configuration Options

Most generators support and/or require user-supplied values that will influence code generation in one way or another. We must register which ones we’ll support at construction time using cliOptions() to access a modifiable list consisting of CliOption objects.

In our case, we’ll add just two options: one to set the destination Java package for the generated class and another for the source directory relative to the output path. Both will have sensible default values, so the user won’t be required to specify them:

public JavaCamelClientGenerator() {
    // ... other configurartions omitted
    cliOptions().add(
      new CliOption(CodegenConstants.API_PACKAGE,CodegenConstants.API_PACKAGE_DESC)
        .defaultValue(apiPackage));
    cliOptions().add(
      new CliOption(CodegenConstants.SOURCE_FOLDER, CodegenConstants.SOURCE_FOLDER_DESC)
        .defaultValue(sourceFolder));
}

We’ve used CodegenConstants to specify the option name and description. Whenever possible, we should stick to those constants instead of using our own option names. This makes it easier for users to switch from one generator to another with similar features and promotes a consistent experience for them.

4.6. Processing Configuration Options

The generator core calls processOpts() before starting actual generation, so we have an opportunity to set up any required state before template processing.

Here, we’ll use this method to capture the actual value of the sourceFolder configuration option. This will be used by the destination folder methods to evaluate the final destination for the different generated files:

public void processOpts() {
    super.processOpts();

    if (additionalProperties().containsKey(CodegenConstants.SOURCE_FOLDER)) {
        sourceFolder = ((String) additionalProperties().get(CodegenConstants.SOURCE_FOLDER));
        // ... source folder validation omitted
    }
}

Within this method, we use additionalProperties() to retrieve a map of user and/or preconfigured properties. This method is also the last chance to validate the supplied options for any invalid values before the actual generation starts.

As of this writing, the only way to inform of inconsistencies at this point is by throwing a RuntimeException(), usually an IllegalArgumentException(). The downside of this approach is that the user gets the error message alongside a very nasty stack trace, which is not the best experience.

4.7. Additional Files

Although not needed in our example, it’s worth noting that we can also generate files that are not directly related to APIs and models. For instance, we can generate pom.xml, README, .gitignore files, or any other file we want.

For each additional file, we must add a SupportingFile instance at construction time to the list returned by the additionalFiles() method. A SupportingFile instance is a tuple consisting of:

  • Template name
  • Destination folder, relative to the specified output folder
  • Output file name

This is how we would register a template to generate a README file on the output folder’s root:

public JavaCamelClientGenerator() {
    // ... other configurations omitted
    supportingFiles().add(new SupportingFile("readme.mustache","","README.txt"));
}

4.8. Template Helpers

The default template engine, Mustache, is, by design, very limited when it comes to manipulating data before rendering it. For instance, the language itself has no string manipulation capabilities, such as splitting, replacing, and so forth.

If we need them as part of our template logic, we must use helper classes, also known as lambdas. Helpers must implement Mustache.Lambda and are registered by implementing addMustacheLambdas() in our generator class:

protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
    ImmutableMap.Builder<String, Mustache.Lambda> builder = super.addMustacheLambdas();
    return builder
      .put("javaconstant", new JavaConstantLambda())
      .put("path", new PathLambda());
}

Here, we first call the base class implementation so we can reuse other available lambdas. This returns an ImmutableMap.Builder instance, to which we add our helpers. The key is the name by which we’ll call the lambda in templates and the value is a lambda instance of the required type.

Once registered, we can use them from templates using the lambda map available in the context:

{{#lambda.javaconstant}}... any valid mustache content ...{{/lambda.javaconstant}}

Our Camel templates require two helpers: one to derive a suitable Java constant name from a method’s operationId and another to extract the path from an URL. Let’s take a look at the latter:

public class PathLambda implements Mustache.Lambda {
    @Override
    public void execute(Template.Fragment fragment, Writer writer) throws IOException {
        String maybeUri = fragment.execute();
        try {
            URI uri = new URI(maybeUri);
            if (uri.getPath() != null) {
                writer.write(uri.getPath());
            } else {
                writer.write("/");
            }
        }
        catch (URISyntaxException e) {
            // Not an URI. Keep as is
            writer.write(maybeUri);
        }
    }
}

The execute() method has two parameters. The first is Template.Fragment, which allows us to access the value of whatever expression was passed by the template to the lambda using execute(). Once we have the actual content, we apply our logic to extract the path part of the URI.

Finally, we use the Writer, passed as the second parameter, to send the result down the processing pipeline.

4.9. Template Authoring

In general, this is the part of the generator’s project that will require the most effort. However, we can use an existing template from another language/framework and use it as a starting point.

Also, since we’ve already covered this topic before, we won’t get into details here. We’ll assume that the generated code will be part of a Spring Boot application, so we won’t generate a full project. Instead, we’ll only generate a @Component class for each API that extends RouteBuilder.

For each operation, we’ll add a “direct” route that users can call. Each route uses the DSL to define a rest destination created from the corresponding operation.

The resulting template, although far from a production level, can be further enhanced with features like error handling, retry policies, and so on.

5. Unit Testing

For basic tests, we can use the CodegenConfigurator in regular unit tests to verify our generator’s basic functionality:

public void whenLaunchCodeGenerator_thenSuccess() throws Exception {
    Map<String, Object> opts = new HashMap<>();
    opts.put(CodegenConstants.SOURCE_FOLDER, "src/generated");
    opts.put(CodegenConstants.API_PACKAGE,"test.api");

    CodegenConfigurator configurator = new CodegenConfigurator()
      .setGeneratorName("java-camel-client")
      .setInputSpec("petstore.yaml")
      .setAdditionalProperties(opts)
      .setOutputDir("target/out/java-camel-client");

    ClientOptInput clientOptInput = configurator.toClientOptInput();
    DefaultGenerator generator = new DefaultGenerator();
    generator.opts(clientOptInput)
      .generate();

    File f = new File("target/out/java-camel-client/src/generated/test/api/PetApi.java");
    assertTrue(f.exists());
}

This test simulates a typical execution using a sample API definition and standard options. It then verifies that it has produced a file at the expected location: a single Java file, in our case, named after the API’s tags.

6. Integration Tests

Although useful, unit tests do not address the functionality of the generated code itself. For instance, even if the file looks fine and compiles, it may not behave correctly at runtime.

To ensure that, we’d need a more complex test setup where the generator’s output gets compiled and run together with the required libraries, mocks, etc.

A simpler approach is to use a dedicated project that uses our custom generator. In our case, the sample project is a Maven-based Spring Boot/Camel project to which we add the OpenAPI Generator plugin:

<plugins>
    <plugin>
        <groupId>org.openapitools</groupId>
        <artifactId>openapi-generator-maven-plugin</artifactId>
        <version>${openapi-generator.version}</version>
        <configuration>
            <skipValidateSpec>true</skipValidateSpec>
            <inputSpec>${project.basedir}/src/main/resources/api/quotes.yaml</inputSpec>
        </configuration>
        <executions>
            <execution>
                <id>generate-camel-client</id>
                <goals>
                    <goal>generate</goal>
                </goals>
                <configuration>
                    <generatorName>java-camel-client</generatorName>
                    <generateModels>false</generateModels>
                    <configOptions>
                        <apiPackage>com.baeldung.tutorials.openapi.quotes.client</apiPackage>
                        <modelPackage>com.baeldung.tutorials.openapi.quotes.api.model</modelPackage>
                    </configOptions>
                </configuration>
            </execution>
                ... other executions omitted
        </executions>
        <dependencies>
            <dependency>
                <groupId>com.baeldung</groupId>
                <artifactId>openapi-custom-generator</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </plugin>
    ... other plugins omitted
</plugins>

Notice how we’ve added our custom generator artifact as a plugin dependency. This allows us to specify java-camel-client for the generatorName configuration parameter.

Also, since our generator does not support model generation, in the full pom.xml we’ve added a second execution of the plugin using the off-the-shelf Java generator.

Now, we can use any test framework to verify that the generated code works as intended. Using Camel’s test support classes, this is how a typical test would look:

@SpringBootTest
class ApplicationUnitTest {
    @Autowired
    private FluentProducerTemplate producer;

    @Autowired
    private CamelContext camel;

    @Test
    void whenInvokeGeneratedRoute_thenSuccess() throws Exception {
        AdviceWith.adviceWith(camel, QuotesApi.GET_QUOTE_ROUTE_ID, in -> {
            in.mockEndpointsAndSkip("rest:*");
        });

        Exchange exg = producer.to(QuotesApi.GET_QUOTE)
          .withHeader("symbol", "BAEL")
          .send();
        assertNotNull(exg);
    }
}

7. Conclusion

In this tutorial, we’ve shown how the steps required to create a custom generator for the OpenAPI generator tool. We’ve also shown how to use a test project to validate the generated code in a realistic scenario.

As usual, all code is available over on GitHub.

Course – LS (cat=REST)

Get started with Spring and Spring Boot, through the Learn Spring course :

>> CHECK OUT THE COURSE
res – REST (eBook) (cat=REST)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments