Skip to content

Commit 268f840

Browse files
feat(generator): add override mechanism for java and go
STACKITSDK-226
1 parent b0ea040 commit 268f840

32 files changed

Lines changed: 7957 additions & 28 deletions

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: [pull_request, workflow_dispatch]
44

55
env:
66
GO_VERSION_BUILD: "1.25"
7-
JAVA_VERSION: "11"
7+
JAVA_VERSION: "25"
88

99
jobs:
1010
main-go:

scripts/generate-sdk/languages/go.sh

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,8 @@ generate_go_sdk() {
109109

110110
# compile custom generator
111111
cd ${ROOT_DIR}
112-
mkdir -p custom/cloud/stackit/codegen
113-
javac -cp "${GENERATOR_JAR_PATH}" CustomRegionGenerator.java
114-
mv CustomRegionGenerator.class custom/cloud/stackit/codegen/CustomRegionGenerator.class
112+
mkdir -p custom
113+
javac -cp "${GENERATOR_JAR_PATH}" -d custom $(find ./scripts/generators -type f -name '*.java' | grep -v overrides)
115114

116115
warning=""
117116

@@ -167,9 +166,9 @@ generate_go_sdk() {
167166
cp "${ROOT_DIR}/languages/golang/.openapi-generator-ignore" "${SERVICES_FOLDER}/${service}/${version}api/.openapi-generator-ignore"
168167

169168
# Run the generator for Go
170-
java -Dlog.level=${GENERATOR_LOG_LEVEL} -cp "custom:scripts/bin/openapi-generator-cli.jar" \
169+
java -Dlog.level=${GENERATOR_LOG_LEVEL} -cp "custom:scripts/bin/openapi-generator-cli.jar:scripts/generators" \
171170
org.openapitools.codegen.OpenAPIGenerator generate \
172-
-g cloud.stackit.codegen.CustomRegionGenerator \
171+
-g GoGenerator \
173172
--input-spec "${service_version_json}" \
174173
--output "${SERVICES_FOLDER}/${service}/${version}api" \
175174
--package-name "${version}api" \

scripts/generate-sdk/languages/java.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ generate_java_sdk() {
7575
rm -rf "${SERVICES_FOLDER}"
7676
mkdir -p "${SERVICES_FOLDER}"
7777

78+
# compile custom generator
79+
cd ${ROOT_DIR}
80+
mkdir -p custom
81+
javac -cp "${GENERATOR_JAR_PATH}" -d custom $(find ./scripts/generators -type f -name '*.java' | grep -v overrides)
82+
7883
warning=""
7984

8085
# Generate SDK for each service
@@ -129,13 +134,15 @@ generate_java_sdk() {
129134
SERVICE_DESCRIPTION=$(cat "${service_version_json}" | jq .info.title --raw-output)
130135

131136
# Run the generator
132-
java -Dlog.level="${GENERATOR_LOG_LEVEL}" -jar "${GENERATOR_JAR_PATH}" generate \
133-
--generator-name java \
137+
java -Dlog.level="${GENERATOR_LOG_LEVEL}" -cp "custom:${GENERATOR_JAR_PATH}:scripts/generators" \
138+
org.openapitools.codegen.OpenAPIGenerator generate \
139+
--generator-name JavaGenerator \
134140
--input-spec "${service_version_json}" \
135141
--output "${SERVICES_FOLDER}/${service}" \
136142
--git-host "${GIT_HOST}" \
137143
--git-user-id "${GIT_USER_ID}" \
138144
--git-repo-id "${GIT_REPO_ID}" \
145+
--enable-post-process-file \
139146
--global-property apis,models,modelTests=false,modelDocs=false,apiDocs=false,apiTests=true,supportingFiles \
140147
--additional-properties="artifactId=${service},artifactDescription=${SERVICE_DESCRIPTION},invokerPackage=cloud.stackit.sdk.${service}.${version}api,modelPackage=cloud.stackit.sdk.${service}.${version}api.model,apiPackage=cloud.stackit.sdk.${service}.${version}api.api,serviceName=${service_pascal_case}" \
141148
--inline-schema-options "SKIP_SCHEMA_REUSE=true" \
Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,46 @@
1-
package cloud.stackit.codegen;
2-
1+
import io.swagger.v3.oas.models.media.Schema;
2+
import io.swagger.v3.oas.models.parameters.Parameter;
3+
import org.openapitools.codegen.CodegenParameter;
34
import org.openapitools.codegen.CodegenProperty;
45
import org.openapitools.codegen.languages.GoClientCodegen;
6+
import shared.PostProcessFileReplace;
57

6-
import org.openapitools.codegen.CodegenParameter;
7-
8+
import java.io.File;
89
import java.util.Set;
9-
import io.swagger.v3.oas.models.media.Schema;
10-
import io.swagger.v3.oas.models.parameters.Parameter;
1110

12-
public class CustomRegionGenerator extends GoClientCodegen {
11+
public class GoGenerator extends GoClientCodegen {
12+
private static RegionFix regionFix = new RegionFix();
13+
private static PostProcessFileReplace postProcessFileReplace = new PostProcessFileReplace("overrides/go");
1314

1415
@Override
1516
public String getName() {
16-
// This is the name you will pass to the -g flag
17-
return "cloud.stackit.codegen.CustomRegionGenerator";
17+
return "GoClientCodegen";
1818
}
1919

20-
public CustomRegionGenerator() {
20+
public GoGenerator(){
2121
super();
22-
System.out.println("=== CUSTOM GO CLIENT GENERATOR INITIALIZED ===");
22+
System.out.println("=== Custom Go Generator initialized ===");
2323
}
2424

2525
@Override
2626
public CodegenProperty fromProperty(String name, Schema p, boolean required) {
27-
CodegenProperty property = super.fromProperty(name, p, required);
27+
return regionFix.fromProperty(super.fromProperty(name, p, required));
28+
}
2829

30+
@Override
31+
public CodegenParameter fromParameter(Parameter parameter, Set<String> imports) {
32+
return regionFix.fromParameter(super.fromParameter(parameter, imports));
33+
}
34+
35+
@Override
36+
public void postProcessFile(File file, String fileType) {
37+
postProcessFileReplace.process(file);
38+
super.postProcessFile(file, fileType);
39+
}
40+
}
41+
42+
class RegionFix {
43+
CodegenProperty fromProperty(CodegenProperty property) {
2944
if (isRegionField(property.name)) {
3045
property.dataType = "string";
3146
property.datatypeWithEnum = "string";
@@ -41,13 +56,7 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required) {
4156
return property;
4257
}
4358

44-
/**
45-
* Intercepts operation parameters (query, path, header, body).
46-
*/
47-
@Override
48-
public CodegenParameter fromParameter(Parameter param, Set<String> imports) {
49-
CodegenParameter parameter = super.fromParameter(param, imports);
50-
59+
CodegenParameter fromParameter(CodegenParameter parameter) {
5160
if (isRegionField(parameter.paramName)) {
5261
parameter.dataType = "string";
5362

@@ -62,7 +71,7 @@ public CodegenParameter fromParameter(Parameter param, Set<String> imports) {
6271
return parameter;
6372
}
6473

65-
private boolean isRegionField(String name) {
74+
private static boolean isRegionField(String name) {
6675
if (name == null) {
6776
return false;
6877
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import org.openapitools.codegen.languages.JavaClientCodegen;
2+
import shared.PostProcessFileReplace;
3+
4+
import java.io.File;
5+
6+
public class JavaGenerator extends JavaClientCodegen {
7+
private static PostProcessFileReplace postProcessFileReplace = new PostProcessFileReplace("overrides/java");
8+
9+
@Override
10+
public String getName() {
11+
return "JavaClientCodegen";
12+
}
13+
14+
public JavaGenerator(){
15+
super();
16+
System.out.println("=== Custom Java Generator initialized ===");
17+
}
18+
19+
@Override
20+
public void postProcessFile(File file, String fileType) {
21+
postProcessFileReplace.process(file);
22+
super.postProcessFile(file, fileType);
23+
}
24+
}

scripts/generators/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Custom Generators
2+
3+
Customized templates are painful when updating the generator. Each template has to be compared with upstream and adapted
4+
accordingly.
5+
6+
We use custom generators for introducing workarounds and fixes to not further customize our templates.
7+
8+
## Structure
9+
10+
Custom generators live in this directory. For each language with a custom generator there should be a `<lang>Generator.java`
11+
file. Each fix/workaround should live in its own class and be called from the `<lang>Generator.java` file. This allows
12+
easy removal of such fixes, when they are no longer needed.
13+
14+
Fixes, applicable to multiple languages, should be implemented in the `shared` package.
15+
16+
We try to only use the JDK and no external libraries besides the `openapi-generator` itself to keep a simple build step.
17+
18+
## How Custom Generators are executed
19+
20+
Each language with a custom generator, needs an adapted `<lang>.sh` script. Before building each service the custom
21+
generator has to be built:
22+
23+
```bash
24+
# compile custom generator
25+
cd ${ROOT_DIR}
26+
mkdir -p custom
27+
javac -cp "${GENERATOR_JAR_PATH}" -d custom $(find ./scripts/generators -type f -name '*.java' | grep -v overrides)
28+
```
29+
30+
This compiles all java files into the `custom` directory. Here we also exclude java files in the `overrides` directory.
31+
These are resources and should not be compiled in this step.
32+
33+
The generator call itself has to be adapted:
34+
35+
```bash
36+
java -Dlog.level="${GENERATOR_LOG_LEVEL}" -cp "custom:${GENERATOR_JAR_PATH}:scripts/generators" \
37+
org.openapitools.codegen.OpenAPIGenerator generate \
38+
--generator-name JavaGenerator \
39+
--enable-post-process-file \
40+
# more params
41+
```
42+
43+
We have to:
44+
- extend the classpath: `custom` makes the compiled custom generator available, `scripts/genrators` the `overrides`
45+
resources
46+
- the `generator-name` needs to be changed to the fully-qualified class name of the custom generator
47+
- `enable-post-process-file` has to be activated to support `overrides`
48+
49+
## Implemented Fixes
50+
51+
### Go: Region Param Adjustment
52+
53+
- we use `RESOLVE_INLINE_ENUMS`
54+
- this creates model classes for inline enums
55+
- this leads to a lot of different types for region enums with the same options
56+
- which makes the API hard to use
57+
- we force a string parameter instead
58+
59+
### Go/Java: Overrides
60+
61+
- there are some usages of `oneOf` schemas in our API specs, that are correct schema wise, but the generated code fails
62+
- example: `{ "type": "cidrv4", "value": string }`, `{ "type": "cidrv6", "value": string }` both, have the same shape
63+
- but they use different patterns for `value`
64+
- some language generators do not apply the pattern when validating the `oneOf` and report a false positive violation
65+
- fixing this upstream would be very involved
66+
- we fix it by replacing after generating sources, by replacing files
67+
68+
How to configure an override:
69+
- edit the `overrides.properties` file for the language in question
70+
- to override a single file you need 3 properties: `<name>.path`, `<name>.replacementPath` and `<name>.hash`
71+
- `<name>` has to be equal for all 3 properties
72+
- `path` is used to match the generated file, which should be replaced
73+
- `hash` is used to check that the generated file still has the same content as when the override was configured
74+
- `replacementPath` specifies the file in resources, that should be used as replacement
75+
- mark adjusted sections in the replacement file in resources with an `OVERRIDE:` comment

0 commit comments

Comments
 (0)