Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenAPI Normalizer #14172

Merged
merged 12 commits into from
Dec 30, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,16 @@ Another useful option is `inlineSchemaNameDefaults`, which allows you to customi
```

Note: Only arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCHEMA_REUSE=true` is a special value to skip reusing inline schemas.

## OpenAPI Normalizer

OpenAPI Normalizer (off by default) transforms the input OpenAPI doc/spec (which may not perfectly conform to the specification) to make it workable with OpenAPI Generator. Here is a list of rules supported:

- `REF_AS_PARENT_IN_ALLOF`: when set to `true`, child schemas in `allOf` is considered a parent if it's a `$ref` (instead of inline schema)


Example:
```
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/allOf_extension_parent.yaml -o /tmp/java-okhttp/ --additional-properties hideGenerationTimestamp="true" --openapi-normalizer REF_AS_PARENT_IN_ALLOF=true
```

Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ public class ConfigHelp extends OpenApiGeneratorCommand {
@Option(name = {"--inline-schema-name-defaults"}, title = "inline schema name defaults", description = "default values used when naming inline schema name")
private Boolean inlineSchemaNameDefaults;

@Option(name = {"--openapi-normalizer"}, title = "openapi normalizer rules", description = "displays the OpenAPI normalizer rules (none)")
private Boolean openapiNormalizer;

@Option(name = {"--metadata"}, title = "metadata", description = "displays the generator metadata like the help txt for the generator and generator type etc")
private Boolean metadata;

Expand Down Expand Up @@ -494,6 +497,18 @@ private void generatePlainTextHelp(StringBuilder sb, CodegenConfig config) {
sb.append(newline);
}

if (Boolean.TRUE.equals(openapiNormalizer)) {
sb.append(newline).append("OPENAIP NORMALIZER RULEA").append(newline).append(newline);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧐 This looks like a typo OPENAIP
Also, is RULEA an example, or also a typo?

Map<String, String> map = config.openapiNormalizer()
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Is this just to check for duplicates? I'm failing to see how we could get any when the source collection is already a map.

throw new IllegalStateException(String.format(Locale.ROOT, "Duplicated options! %s and %s", a, b));
}, TreeMap::new));
writePlainTextFromMap(sb, map, optIndent, optNestedIndent, "OpenAPI normalizer rule", "Set to");
sb.append(newline);
}

if (Boolean.TRUE.equals(instantiationTypes)) {
sb.append(newline).append("INSTANTIATION TYPES").append(newline).append(newline);
Map<String, String> map = config.instantiationTypes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ public class Generate extends OpenApiGeneratorCommand {
+ " ONLY arrayItemSuffix, mapItemSuffix are supported at the moment. `SKIP_SCHEMA_REUSE=true` is a special value to skip reusing inline schemas.")
private List<String> inlineSchemaNameDefaults = new ArrayList<>();

@Option(
name = {"--openapi-normalizer"},
title = "OpenAPI normalizer rules",
description = "specifies the rules to be enabled in OpenAPI normalizer in the form of RULE_1=true,RULE_2=original."
+ " You can also have multiple occurrences of this option.")
private List<String> openapiNormalizer = new ArrayList<>();

@Option(
name = {"--server-variables"},
title = "server variables",
Expand Down Expand Up @@ -447,6 +454,7 @@ public void execute() {
applySchemaMappingsKvpList(schemaMappings, configurator);
applyInlineSchemaNameMappingsKvpList(inlineSchemaNameMappings, configurator);
applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator);
applyOpenAPINormalizerKvpList(openapiNormalizer, configurator);
applyTypeMappingsKvpList(typeMappings, configurator);
applyAdditionalPropertiesKvpList(additionalProperties, configurator);
applyLanguageSpecificPrimitivesCsvList(languageSpecificPrimitives, configurator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class GeneratorSettings implements Serializable {
private final Map<String, String> schemaMappings;
private final Map<String, String> inlineSchemaNameMappings;
private final Map<String, String> inlineSchemaNameDefaults;
private final Map<String, String> openapiNormalizer;
private final Set<String> languageSpecificPrimitives;
private final Map<String, String> reservedWordsMappings;
private final Map<String, String> serverVariables;
Expand Down Expand Up @@ -264,6 +265,15 @@ public Map<String, String> getInlineSchemaNameDefaults() {
return inlineSchemaNameDefaults;
}

/**
* Gets OpenAPI normalizer rules
*
* @return a map of rules
*/
public Map<String, String> getOpenAPINormalizer() {
return openapiNormalizer;
}

/**
* Gets language specific primitives. These are in addition to the "base" primitives defined in a generator.
* <p>
Expand Down Expand Up @@ -382,6 +392,7 @@ private GeneratorSettings(Builder builder) {
schemaMappings = Collections.unmodifiableMap(builder.schemaMappings);
inlineSchemaNameMappings = Collections.unmodifiableMap(builder.inlineSchemaNameMappings);
inlineSchemaNameDefaults = Collections.unmodifiableMap(builder.inlineSchemaNameDefaults);
openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer);
languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives);
reservedWordsMappings = Collections.unmodifiableMap(builder.reservedWordsMappings);
serverVariables = Collections.unmodifiableMap(builder.serverVariables);
Expand Down Expand Up @@ -455,6 +466,7 @@ public GeneratorSettings() {
schemaMappings = Collections.unmodifiableMap(new HashMap<>(0));
inlineSchemaNameMappings = Collections.unmodifiableMap(new HashMap<>(0));
inlineSchemaNameDefaults = Collections.unmodifiableMap(new HashMap<>(0));
openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why not use Collections.emptyMap() if it should be unmodifable in any case?

languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0));
reservedWordsMappings = Collections.unmodifiableMap(new HashMap<>(0));
serverVariables = Collections.unmodifiableMap(new HashMap<>(0));
Expand Down Expand Up @@ -515,6 +527,9 @@ public static Builder newBuilder(GeneratorSettings copy) {
if (copy.getInlineSchemaNameDefaults() != null) {
builder.inlineSchemaNameDefaults.putAll(copy.getInlineSchemaNameDefaults());
}
if (copy.getOpenAPINormalizer() != null) {
builder.openapiNormalizer.putAll(copy.getOpenAPINormalizer());
}
if (copy.getLanguageSpecificPrimitives() != null) {
builder.languageSpecificPrimitives.addAll(copy.getLanguageSpecificPrimitives());
}
Expand Down Expand Up @@ -557,6 +572,7 @@ public static final class Builder {
private Map<String, String> schemaMappings;
private Map<String, String> inlineSchemaNameMappings;
private Map<String, String> inlineSchemaNameDefaults;
private Map<String, String> openapiNormalizer;
private Set<String> languageSpecificPrimitives;
private Map<String, String> reservedWordsMappings;
private Map<String, String> serverVariables;
Expand All @@ -577,6 +593,7 @@ public Builder() {
schemaMappings = new HashMap<>();
inlineSchemaNameMappings = new HashMap<>();
inlineSchemaNameDefaults = new HashMap<>();
openapiNormalizer = new HashMap<>();
languageSpecificPrimitives = new HashSet<>();
reservedWordsMappings = new HashMap<>();
serverVariables = new HashMap<>();
Expand Down Expand Up @@ -897,6 +914,32 @@ public Builder withInlineSchemaNameMapping(String key, String value) {
return this;
}

/**
* Sets the {@code openapiNormalizer} and returns a reference to this Builder so that the methods can be chained together.
*
* @param openapiNormalizer the {@code openapiNormalizer} to set
* @return a reference to this Builder
*/
public Builder withOpenAPINormalizer(Map<String, String> openapiNormalizer) {
this.openapiNormalizer = openapiNormalizer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❔ Is it allowed to put null here, or should this use requireNonNull.

Alternatively, I you could write:

this.openapiNormalizer.clear();
if(openapiNormalizer != null) {
    this.openapiNormalizer.putAll(openapiNormalizer);
}

which would achieve two things: First, a defensive copy. Second, it prevents the field from ever being null.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like @leonard84 's approach since it's bad to set maps and collections to null in general

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can apply such improvement not only for this new option (or property) but all other options/properties. We will do it in a separate PR in the future instead.

return this;
}

/**
* Sets a single {@code inlineSchemaNameMappings} and returns a reference to this Builder so that the methods can be chained together.
*
* @param key A key for the inline schema mapping
* @param value The value of inline schema mapping
* @return a reference to this Builder
*/
public Builder withOpenAPINormalizer(String key, String value) {
if (this.openapiNormalizer == null) {
spacether marked this conversation as resolved.
Show resolved Hide resolved
this.openapiNormalizer = new HashMap<>();
}
this.openapiNormalizer.put(key, value);
return this;
}

/**
* Sets the {@code languageSpecificPrimitives} and returns a reference to this Builder so that the methods can be chained together.
*
Expand Down Expand Up @@ -1085,6 +1128,7 @@ public boolean equals(Object o) {
Objects.equals(getSchemaMappings(), that.getSchemaMappings()) &&
Objects.equals(getInlineSchemaNameMappings(), that.getInlineSchemaNameMappings()) &&
Objects.equals(getInlineSchemaNameDefaults(), that.getInlineSchemaNameDefaults()) &&
Objects.equals(getOpenAPINormalizer(), that.getOpenAPINormalizer()) &&
Objects.equals(getLanguageSpecificPrimitives(), that.getLanguageSpecificPrimitives()) &&
Objects.equals(getReservedWordsMappings(), that.getReservedWordsMappings()) &&
Objects.equals(getGitHost(), that.getGitHost()) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ class OpenApiGeneratorPlugin : Plugin<Project> {
schemaMappings.set(generate.schemaMappings)
inlineSchemaNameMappings.set(generate.inlineSchemaNameMappings)
inlineSchemaNameDefaults.set(generate.inlineSchemaNameDefaults)
openapiNormalizer.set(generate.openapiNormalizer)
invokerPackage.set(generate.invokerPackage)
groupId.set(generate.groupId)
id.set(generate.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ open class OpenApiGeneratorGenerateExtension(project: Project) {
*/
val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>()

/**
* Specifies mappings (rules) in OpenAPI normalizer
*/
val openapiNormalizer = project.objects.mapProperty<String, String>()

/**
* Root package for generated code.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,13 @@ open class GenerateTask : DefaultTask() {
@Input
val inlineSchemaNameDefaults = project.objects.mapProperty<String, String>()

/**
* Specifies mappings (rules) in OpenAPI normalizer
*/
@Optional
@Input
val openapiNormalizer = project.objects.mapProperty<String, String>()

/**
* Root package for generated code.
*/
Expand Down Expand Up @@ -758,6 +765,12 @@ open class GenerateTask : DefaultTask() {
}
}

if (openapiNormalizer.isPresent) {
openapiNormalizer.get().forEach { entry ->
configurator.addOpenAPINormalizer(entry.key, entry.value)
}
}

if (typeMappings.isPresent) {
typeMappings.get().forEach { entry ->
configurator.addTypeMapping(entry.key, entry.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ public class CodeGenMojo extends AbstractMojo {
@Parameter(name = "inlineSchemaNameDefaults", property = "openapi.generator.maven.plugin.inlineSchemaNameDefaults")
private List<String> inlineSchemaNameDefaults;

/**
* A set of rules for OpenAPI normalizer
*/
@Parameter(name = "openapiNormalizer", property = "openapi.generator.maven.plugin.openapiNormalizer")
private List<String> openapiNormalizer;

/**
* A map of swagger spec types and the generated code types to use for them
*/
Expand Down Expand Up @@ -700,6 +706,12 @@ public void execute() throws MojoExecutionException {
configurator);
}

// Retained for backwards-compatibility with configOptions -> openapi-normalizer
if (openapiNormalizer == null && configOptions.containsKey("openapi-normalizer")) {
applyOpenAPINormalizerKvp(configOptions.get("openapi-normalizer").toString(),
configurator);
}

// Retained for backwards-compatibility with configOptions -> type-mappings
if (typeMappings == null && configOptions.containsKey("type-mappings")) {
applyTypeMappingsKvp(configOptions.get("type-mappings").toString(), configurator);
Expand Down Expand Up @@ -753,6 +765,11 @@ public void execute() throws MojoExecutionException {
applyInlineSchemaNameDefaultsKvpList(inlineSchemaNameDefaults, configurator);
}

// Apply OpenAPI normalizer rules
if (openapiNormalizer != null && (configOptions == null || !configOptions.containsKey("openapi-normalizer"))) {
applyOpenAPINormalizerKvpList(openapiNormalizer, configurator);
}

// Apply Type Mappings
if (typeMappings != null && (configOptions == null || !configOptions.containsKey("type-mappings"))) {
applyTypeMappingsKvpList(typeMappings, configurator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public interface CodegenConfig {

Map<String, String> inlineSchemaNameDefault();

Map<String, String> openapiNormalizer();

Map<String, String> apiTemplateFiles();

Map<String, String> modelTemplateFiles();
Expand Down Expand Up @@ -330,4 +332,7 @@ public interface CodegenConfig {
boolean getUseInlineModelResolver();

boolean getAddSuffixToDuplicateOperationNicknames();

boolean getUseOpenAPINormalizer();

}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public class DefaultCodegen implements CodegenConfig {
protected Map<String, String> inlineSchemaNameMapping = new HashMap<>();
// a map to store the inline schema naming conventions
protected Map<String, String> inlineSchemaNameDefault = new HashMap<>();
// a map to store the rules in OpenAPI Normalizer
protected Map<String, String> openapiNormalizer = new HashMap<>();
protected String modelPackage = "", apiPackage = "", fileSuffix;
protected String modelNamePrefix = "", modelNameSuffix = "";
protected String apiNamePrefix = "", apiNameSuffix = "Api";
Expand Down Expand Up @@ -1122,6 +1124,11 @@ public Map<String, String> inlineSchemaNameDefault() {
return inlineSchemaNameDefault;
}

@Override
public Map<String, String> openapiNormalizer() {
return openapiNormalizer;
}

@Override
public String testPackage() {
return testPackage;
Expand Down Expand Up @@ -7918,6 +7925,9 @@ public List<VendorExtension> getSupportedVendorExtensions() {
@Override
public boolean getUseInlineModelResolver() { return true; }

@Override
public boolean getUseOpenAPINormalizer() { return true; }

/*
A function to convert yaml or json ingested strings like property names
And convert special characters like newline, tab, carriage return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ void configureGeneratorProperties() {

config.processOpts();

// normalize the spec
if (config.getUseOpenAPINormalizer()) {
OpenAPINormalizer openapiNormalizer = new OpenAPINormalizer(openAPI, config.openapiNormalizer());
openapiNormalizer.normalize();
}

// resolve inline models
if (config.getUseInlineModelResolver()) {
InlineModelResolver inlineModelResolver = new InlineModelResolver();
Expand Down
Loading