Getting Started - Code Generator (SDK) Integration
Introduction
The examples in the Getting Started Guidelines show fairly simple integration with relatively native code.
However, it is often not really useful to "transcribe" entire models.
Most importantly, copy errors can occur, which prevents data from being mapped, or changes can be made to the API that are only detected during runtime. Finding and fixing these changes can be time-consuming.
In order to always be in sync with the current API and not put effort into writing the models and basic API clients, it is recommended to use a code generator.
Implementation Guide
See a full working project here: finAPI Data Intelligence Product Platform Examples (Bitbucket)
The guidelines and the example project are written in Kotlin with Spring Boot. But it is easily adoptable for other languages and frameworks.
Please also note that the code presented here and also the repository is only an illustration of a possible implementation. Therefore, the code is kept very simple and should not be used 1:1 in a production environment.
This guide is based on the sample project and uses Gradle for integration.
As code generators, we use OpenAPI Generator. These are usually much better maintained than the original generators from Smartbears.
Notes for different integrations
Generally, it is recommended to use the generators in a submodule or a separate module on which the domain-oriented code depends.
The advantage of outsourcing to a separate module would be that you do not depend on the availability of the Internet. In addition, one can write tests for the client to be able to ensure the basic functionality.
However, the negative side of this approach is that API changes may not be detected unless you run the build system regularly to generate an up-to-date version and test it to detect any problems.
In a submodule, you don't have this disadvantage, because the generated code is always fresh. However, it can be that one cannot compile the own project, should the Internet not be available, as far as one fetches the OpenAPI files dynamically.
To compensate for this weakness, one could also store the OpenAPI file locally, but must also update it regularly.
For this example, we simply add the client generation to the existing project. However, we use an extra source directory so that the files are visible but also separated.
Preparation
We want to place the client under /apiclient/src
.
This allows us to ignore some unneeded files via the .openapi-generator-ignore
file and keep the generated code out of our repository via the .gitignore
.
This should always be generated cleanly. Otherwise, there would be changed files on every build that the system wants to commit.
So we create the directory /apiclient
in our project.
Integrate Generator into Gradle Build
Adding Generator to Gradle Classpath
For this, we need to modify the build.gradle
file.
In the example project this can be viewed here.
First, we add the openapi-generator-gradle-plugin
to the classpath as a dependency in the buildscript
block (at the very beginning):
buildscript {
[...]
dependencies {
[...]
classpath "org.openapitools:openapi-generator-gradle-plugin:6.6.0"
}
}
In the sample project this code can be found in the build.gradle
at the bottom.
Now we still need to apply the plugins.
We also use the plugin en.undercouch.download
so we can download the OpenAPI specification.
apply plugin: "org.openapi.generator"
apply plugin: "de.undercouch.download"
Download, generate, and clean tasks
Right after adding the plugins in the build.gradle
file we define the OpenAPI file.
We introduce a variable openApiFileProcessController
which contains only the file name of the API. The reason for this is that we need the filename in multiple contexts, but the URL itself only once.
In this example it is the file openapi-processctl.yaml
:
def openApiFileProcessController = "openapi-processctl.yaml"
Download task
Now we can register a task that downloads the OpenAPI specification:
/**
* Download API file
*/
tasks.register('download_ProcessControllerAPISpec', Download) {
group "finAPI Process Controller"
src "https://di-processctl-finapi-general-sandbox.finapi.io/${openApiFileProcessController}"
dest buildDir
}
The task is of the type Download
, which comes from the plugin en.undercouch.download
.
In addition, we define the group finAPI Process Controller
, so that we can see all relevant tasks grouped in the IDE.
As a source, we use the URL of the API of Sandbox. We add the previously defined OpenAPI file name from our variable. For now, the target is only our buildDir
.
Generate task
In the next step, we create the task for generating the API.
/**
* Generate REST API client for Process Controller.
*/
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
tasks.register("generateRestAPI_ProcessController", GenerateTask) {
group "finAPI Process Controller"
generatorName = "kotlin"
inputSpec = "${buildDir}/${openApiFileProcessController}".toString()
outputDir = "${projectDir}/apiclient/".toString()
skipValidateSpec = true
packageName = "io.finapi.client.processctl"
apiPackage = "io.finapi.client.processctl.api"
invokerPackage = "io.finapi.client"
modelPackage = "io.finapi.client.processctl.models"
globalProperties = [
apiDocs: "false",
apiTests: "false",
modelTests: "false"
]
configOptions = [
library: "jvm-okhttp4",
useSpringBoot3: "true",
annotationLibrary: "none",
documentationProvider: "none",
modelMutable: "true",
dateLibrary: "java8",
enumPropertyNaming: "UPPERCASE"
]
}
The task is of type GenerateTask
, which comes from the Code Generator.
We also add this task to the finAPI Process Controller
group.
Under inputSpec
we define where the generator plugin should find the OpenAPI specification. Here we use again our buildDir
together with the variable openApiFileProcessController
.
As outputDir
we specify our previously created directory /apiclient/
which is inside our projectDir
.
The rest of the parameters is configuration of the packages and the generator.
With Spring Boot 3 you should make sure that the parameter useSpringBoot3: "true"
is present under configOptions
.
Furthermore, we use the okhttp4
library as HTTP client.
Otherwise, the generator may use the old javax
packages. However, Spring Boot 3 has switched to the jakarta
packages in the meantime.
Clean task
Of course, we want to remove old generated files before we build to avoid dead code that may cause problems.
So we create the following task:
/**
* Clean task.
*/
tasks.register('cleanApi_ProcessController', Delete) {
group "finAPI Process Controller"
delete "${projectDir}/apiclient/src"
delete "${projectDir}/apiclient/.openapi-generator"
}
The task itself is of type Delete
.
As before, the task is added to the finAPI Process Controller
group.
We now always delete the two directories
/apiclient/src
/apiclient/.openapi-generator
in ourprojectDir
.
Since we do not write our own code under /apiclient
, this is the fastest way. However, we cannot delete the /apiclient
directory, because we need to add a file here later.
Defining the task dependencies
Now we have all the required tasks together and can define the dependencies of the tasks.
tasks.generateRestAPI_ProcessController.dependsOn(tasks.download_ProcessControllerAPISpec)
tasks.generateRestAPI_ProcessController.dependsOn(tasks.cleanApi_ProcessController)
tasks.compileKotlin.dependsOn(tasks.generateRestAPI_ProcessController)
tasks.clean.dependsOn(tasks.cleanApi_ProcessController)
First, we make sure that the OpenAPI file is downloaded first. For this, we create a dependency from the "generate task" to the "download task".
The second dependency says that the “generate task” always depends on the “clean task”. This means that old generated files are always deleted before new ones are generated.
The third dependency is between the “compile task” and the “generate task”. This ensures that the API client sources are always generated before the compile task.
Last but not least, we append the “API clean task” to the “general clean task”, so that the generated sources are also deleted during a gradlew clean
.
Adding new source directories to the Gradle configuration
In order for Gradle to recognize the directory /apiclient/src/main/kotlin
as the source directory, we still have to configure this.
A very simple variant would be to extend the sourceSets
for main.java
with the apiclient/src/main/kotlin
:
// define source sets for generated classes
sourceSets {
main {
java {
srcDirs = [
'apiclient/src/main/kotlin',
'src/main/java',
'src/main/kotlin'
]
}
}
}
Ignoring unused and unwanted files
By default, the generator is configured to create the API Client as a standalone project.
However, we do not need this and it has a significant disadvantage:
We want to generate the code during a build phase and not download it from an API via a client, unpack it, and only then build it.
This means that if we were to create a Gradle project now, the generator might overwrite important files (e.g. settings.gradle
or build.gradle.kts
).
Also, we don't need a Spring Boot main class to start the application, only the client. This also means that we do not need an application.yaml
file.
In the /apiclient
directory, we create a file .openapi-generator-ignore
.
In this file, we can store files or directories that we do not want to generate.
So the content of the file could look like this:
docs/
gradle/
src/main/kotlin/io/finapi/client/Application.kt
src/main/resources/application.yaml
pom.xml
build.gradle.kts
build.gradle
gradlew
gradlew.bat
README.md
settings.gradle
This would give us relatively cleanly generated code that we can use soon.
Adding required dependencies
Since the generated code has dependencies, of course, so that it can be compiled. We define these in the dependencies
block.
In the case of the example project, we have moved the dependencies to the dependencies.gradle
file for clearer maintenance. This can be found here.
As dependencies, we need the following libraries:
dependencies {
[..]
// required for code generator
implementation "io.github.openfeign:feign-okhttp:12.4"
implementation "com.squareup.moshi:moshi-kotlin:1.15.0"
implementation "jakarta.validation:jakarta.validation-api"
}
Ignoring generated files in Git
The last step is to ignore the generated files in Git.
For this, we add the following lines to the .gitignore
file:
# api generator
/apiclient/src
/apiclient/.openapi-generator
Now the /apiclient/src
and the apiclient/.openapi-generator
directory will be ignored, but not our .openapi-generator-ignore
file, which we created earlier under /apiclient
.
Integrate Generator into Maven Build
For this example, we do not use the "normal" generation to target/generated-sources
, but create a new directory /apiclient
to have easier access to the code.
The full pom.xml can be shown here: pom.xml.sdk.example (Bitbucket)
Download the OpenAPI Specification
The first thing we need is a plugin to download the OpenAPI specification.
We also define the variable openApiFileProcessController
so that we can use the name of the file multiple times without having to specify it multiple times.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<properties>
<java.version>17</java.version>
<kotlin.version>1.8.22</kotlin.version>
<openApiFileProcessController>openapi-processctl.yaml</openApiFileProcessController>
</properties>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>com.googlecode.maven-download-plugin</groupId>
<artifactId>download-maven-plugin</artifactId>
<version>1.6.8</version>
<executions>
<execution>
<id>download_ProcessControllerAPISpec</id>
<phase>generate-sources</phase>
<goals>
<goal>wget</goal>
</goals>
</execution>
</executions>
<configuration>
<url>https://di-processctl-finapi-general-sandbox.finapi.io/${openApiFileProcessController}</url>
<outputDirectory>${project.build.directory}/</outputDirectory>
<library>jvm-okhttp4</library>
<library>jvm-okhttp4</library>
</configuration>
</plugin>
</plugins>
</build>
</project>
Now we have the OpenAPI specification in the /target
directory.
Integrate the Code Generator
Next, we add the code generator in the plugins section, just below the download plugin.
This is configured to use the kotlin
generator (<generatorName>
) and generate to ${project.basedir}/apiclient
.
In addition, we set the cleanupOutput
flag to true
to get a clean
on the directory before generation.
The other parameters are mostly the configuration of the generator for packages and to not generate some unnecessary things, because we just want to have the client code.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>6.6.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.build.directory}/${openApiFileProcessController}</inputSpec>
<output>${project.basedir}/apiclient</output>
<generatorName>kotlin</generatorName>
<cleanupOutput>true</cleanupOutput>
<ignoreFileOverride>${project.basedir}/.openapi-generator-ignore</ignoreFileOverride>
<packageName>io.finapi.client.processctl</packageName>
<apiPackage>io.finapi.client.processctl.api</apiPackage>
<invokerPackage>io.finapi.client</invokerPackage>
<modelPackage>io.finapi.client.processctl.models</modelPackage>
<generateApiDocumentation>false</generateApiDocumentation>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<generateModelDocumentation>false</generateModelDocumentation>
<library>jvm-okhttp4</library>
<configOptions>
<sourceFolder>src/main/kotlin</sourceFolder>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Define the new Folder as Source Folder
Basically, we now already have the code, but we still need to define apiclient/src/main/kotlin
as a source directory:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
[...]
<build>
[...]
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>apiclient/src/main/kotlin/</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugin>
</build>
</project>
Adding Dependencies
To be able to compile now, we have to add a few dependencies:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
[...]
<dependencies>
[...]
<!-- Required for Generator -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>12.4</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin</artifactId>
<version>1.15.0</version>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>
[...]
</project>
Ignoring unused and unwanted files
Since the generator creates a complete project, but we only want the source code for the client, we have to ignore some things.
For this, we create the file .openapi-generator-ignore
in the root directory of the project:
**/docs/
**/gradle/
**/src/main/kotlin/io/finapi/client/Application.kt
**/src/main/resources/application.yaml
**/pom.xml
**/build.gradle.kts
**/build.gradle
**/gradlew
**/gradlew.bat
**/README.md
**/settings.gradle
**/.openapi-generator-ignore
Ignoring generated files in Git
In the case of Maven, we can add the apiclient
folder to our .gitignore
file.
Integrate Process Controller Create User with generated client
See a full working project here: finAPI Data Intelligence Product Platform Examples (Bitbucket)
Code from this guide can be found here: finAPI SDK Usage (Bitbucket)
Environment overview can be found here: Environments
Now we integrate the generated client and use it to create a process via the process controller and convert the process token into an access_token
.
The process itself is described in a bit more detail under Obtain Authorization via Process Controller . It may therefore be worth reading this article as well.
Create client bean
To use the generated client, we need to initialize it.
We use the UserAndProcessTokenManagementApi
client to create a simple user and get the access_token
.
Our new class ProcessCtlSdkClient
, therefore, needs as constructor parameters the URL of the process controller, our client_id
, and our client_secret
.
In addition, we need the DiProcessRepository
to manage the state.
We initialize the client as a class variable with the passed basePath
in the shape of the processCtlUrl
.
@Service
@Suppress("TooGenericExceptionThrown")
class ProcessCtlSdkClient(
@Value("\${finapi.instances.processctl.url}") private val processCtlUrl: String,
@Value("\${finapi.security.credentials.clientId}") private val clientId: String,
@Value("\${finapi.security.credentials.clientSecret}") private val clientSecret: String,
private val processesRepository: DiProcessesRepository
) {
private val client = UserAndProcessTokenManagementApi(
basePath = processCtlUrl
)
}
Call the “Create new Process” endpoint with the client
Now we add the createProcessToken()
function to the class, which calls the createNewProcess()
method in the client.
We pass a valid DiProcessEntity
to the function so that we can track the status. This entity also contains its own processId
, which we use as clientReference
.
If everything was successful, we update the status to PROCESS_TOKEN_CREATED
and return the result of the API query.
@Service
@Suppress("TooGenericExceptionThrown")
class ProcessCtlSdkClient(
@Value("\${finapi.instances.processctl.url}") private val processCtlUrl: String,
@Value("\${finapi.security.credentials.clientId}") private val clientId: String,
@Value("\${finapi.security.credentials.clientSecret}") private val clientSecret: String,
private val processesRepository: DiProcessesRepository
) {
fun createProcessToken(
processEntity: DiProcessEntity
): StartProcessResponse {
// create process token with generated API client
val processToken = client.createNewProcess(
startProcessRequest = StartProcessRequest(
clientId = clientId,
clientSecret = clientSecret,
processId = ProcessId.USER_ONLY,
clientReferences = mutableListOf(
ClientReferenceEntry(
clientReference = processEntity.processId
)
)
),
withQRCode = false
)
// save new state
processEntity.status = EnumProcessStatus.PROCESS_TOKEN_CREATED
processesRepository.save(processEntity)
return processToken
}
[...]
}
Exchange Process Token with access_token
Now that we have a process token, we can replace it with an access_token
to gain access to the API.
For this, we call the enterProcess()
method in the client.
This method expects the processToken
as a mandatory parameter.
In this example, however, we want to invalidate the process token directly so that no one else can request an access_token
. Therefore we set the parameter invalidate=true
.
@Service
@Suppress("TooGenericExceptionThrown")
class ProcessCtlSdkClient(
@Value("\${finapi.instances.processctl.url}") private val processCtlUrl: String,
@Value("\${finapi.security.credentials.clientId}") private val clientId: String,
@Value("\${finapi.security.credentials.clientSecret}") private val clientSecret: String,
private val processesRepository: DiProcessesRepository
) {
@Throws(RuntimeException::class)
fun exchangeProcessTokenWithAccessToken(processId: String, processToken: UUID): AccessToken {
// exchange process token with access_token
val accessToken = client.enterProcess(
processToken = processToken,
invalidate = true
)
// update state of process in database or throw exception
val processEntity = processesRepository.findFirstByProcessId(processId = processId)
?: throw RuntimeException("Unable to exchange Process Token. No Process Entity found.")
processEntity.status = EnumProcessStatus.PROCESS_TOKEN_EXCHANGED
return accessToken
}
[...]
}
Now to show how to combine both methods, we create a function createAndExchangeProcessToken()
.
This takes care of creating the process entity, calls the function createProcessToken()
and exchangeProcessTokenWithAccessToken()
, and returns the object AccessToken
.
@Service
@Suppress("TooGenericExceptionThrown")
class ProcessCtlSdkClient(
@Value("\${finapi.instances.processctl.url}") private val processCtlUrl: String,
@Value("\${finapi.security.credentials.clientId}") private val clientId: String,
@Value("\${finapi.security.credentials.clientSecret}") private val clientSecret: String,
private val processesRepository: DiProcessesRepository
) {
fun createAndExchangeProcessToken(): AccessToken {
val processEntity = processesRepository.save(
DiProcessEntity(
processId = UUID.randomUUID().toString(),
status = EnumProcessStatus.PROCESS_TOKEN_INIT
)
)
val processTokens = createProcessToken(
processEntity = processEntity
)
if (processTokens.processes.isNullOrEmpty()) {
processEntity.status = EnumProcessStatus.PROCESS_TOKEN_FAILED
processesRepository.save(processEntity)
throw RuntimeException("No processes created.")
}
// We have created only one client reference, so it contains only one process token.
return exchangeProcessTokenWithAccessToken(
processId = processEntity.processId,
processToken = processTokens.processes!![0].processToken
)
}
[...]
}