-
Notifications
You must be signed in to change notification settings - Fork 30
feat: Support OpenAPI 3.1.x #1231
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
base: main
Are you sure you want to change the base?
Changes from all commits
b7dabdb
cbddab1
42a0e60
ffe6a01
ad395cb
fe03631
189c472
65b4d10
f14cdfa
0af7b8d
88e1d34
97d5048
776c994
208a588
39f1a95
8e1150c
b9cb23a
ec48f91
986ced0
cb41ee4
3ab66c7
8e3a83f
3e149d6
4719fbb
bae1184
fbd2d19
2e861a3
ecc67e7
208c6ad
5fcd64c
dafe605
0620345
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |||
| import org.openapitools.codegen.CodegenProperty; | ||||
| import org.openapitools.codegen.languages.JavaClientCodegen; | ||||
| import org.openapitools.codegen.model.ModelMap; | ||||
| import org.openapitools.codegen.model.ModelsMap; | ||||
| import org.openapitools.codegen.model.OperationsMap; | ||||
|
|
||||
| import com.sap.cloud.sdk.datamodel.openapi.generator.model.GenerationConfiguration; | ||||
|
|
@@ -38,6 +39,8 @@ class CustomJavaClientCodegen extends JavaClientCodegen | |||
| { | ||||
| private final GenerationConfiguration config; | ||||
| private static final Predicate<String> DOUBLE_IS_PATTERN = Pattern.compile("^isIs[A-Z]").asPredicate(); | ||||
| // schemaName -> (propertyName -> sibling description) captured before normalization strips $ref context | ||||
| private final Map<String, Map<String, String>> siblingDescriptions = new java.util.HashMap<>(); | ||||
|
|
||||
| public CustomJavaClientCodegen( @Nonnull final GenerationConfiguration config ) | ||||
| { | ||||
|
|
@@ -47,6 +50,9 @@ public CustomJavaClientCodegen( @Nonnull final GenerationConfiguration config ) | |||
| @Override | ||||
| public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI ) | ||||
| { | ||||
| // Capture sibling descriptions on $ref property schemas before normalization resolves them away. | ||||
| captureSiblingDescriptions(openAPI); | ||||
|
|
||||
| if( USE_EXCLUDE_PROPERTIES.isEnabled(config) ) { | ||||
| final String[] exclusions = USE_EXCLUDE_PROPERTIES.getValue(config).trim().split("[,\\s]+"); | ||||
| for( final String exclusion : exclusions ) { | ||||
|
|
@@ -55,11 +61,12 @@ public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI ) | |||
| } | ||||
| } | ||||
|
|
||||
| // OAS 3.1 documents may have no paths (webhooks-only or components-only). | ||||
| if( USE_EXCLUDE_PATHS.isEnabled(config) ) { | ||||
| final String[] exclusions = USE_EXCLUDE_PATHS.getValue(config).trim().split("[,\\s]+"); | ||||
| for( final String exclusion : exclusions ) { | ||||
| if( !openAPI.getPaths().keySet().remove(exclusion) ) { | ||||
| log.error("Could not remove path {}", exclusion); | ||||
| if( openAPI.getPaths() != null ) { | ||||
| for( final String exclusion : exclusions ) { | ||||
| openAPI.getPaths().remove(exclusion); | ||||
| } | ||||
| } | ||||
| } | ||||
|
|
@@ -137,6 +144,54 @@ protected void updateModelForComposedSchema( | |||
| } | ||||
| } | ||||
|
|
||||
| @SuppressWarnings( { "rawtypes", "RedundantSuppression" } ) | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Question) Is the SurpressWarnings actually necessary here? It looks to me like this method would be fine without it.
Suggested change
|
||||
| @Override | ||||
| @Nonnull | ||||
| public Map<String, ModelsMap> postProcessAllModels( @Nonnull final Map<String, ModelsMap> objs ) | ||||
| { | ||||
| final Map<String, ModelsMap> result = super.postProcessAllModels(objs); | ||||
|
|
||||
| // Restore sibling descriptions lost during $ref resolution for primitive-typed properties. | ||||
| for( final var schemaEntry : siblingDescriptions.entrySet() ) { | ||||
| final ModelsMap modelsMap = result.get(schemaEntry.getKey()); | ||||
| if( modelsMap == null ) { | ||||
| continue; | ||||
| } | ||||
| for( final ModelMap modelMap : modelsMap.getModels() ) { | ||||
| for( final CodegenProperty prop : modelMap.getModel().vars ) { | ||||
| final String siblingDesc = schemaEntry.getValue().get(prop.baseName); | ||||
| if( siblingDesc != null ) { | ||||
| prop.description = escapeText(siblingDesc); | ||||
| prop.unescapedDescription = siblingDesc; | ||||
| } | ||||
| } | ||||
| } | ||||
| } | ||||
| return result; | ||||
| } | ||||
|
|
||||
| @SuppressWarnings( { "rawtypes", "unchecked" } ) | ||||
| private void captureSiblingDescriptions( @Nonnull final OpenAPI openAPI ) | ||||
| { | ||||
| if( openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null ) { | ||||
| return; | ||||
| } | ||||
| for( final var schemaEntry : openAPI.getComponents().getSchemas().entrySet() ) { | ||||
| final Schema modelSchema = schemaEntry.getValue(); | ||||
| if( modelSchema.getProperties() == null ) { | ||||
| continue; | ||||
| } | ||||
| for( final var propEntry : ((Map<String, Schema>) modelSchema.getProperties()).entrySet() ) { | ||||
| final Schema propSchema = propEntry.getValue(); | ||||
| if( propSchema.get$ref() != null && propSchema.getDescription() != null ) { | ||||
| siblingDescriptions | ||||
| .computeIfAbsent(schemaEntry.getKey(), k -> new java.util.HashMap<>()) | ||||
| .put(propEntry.getKey(), propSchema.getDescription()); | ||||
| } | ||||
| } | ||||
| } | ||||
| } | ||||
|
|
||||
| /** | ||||
| * Remove property from specification. | ||||
| * | ||||
|
|
@@ -147,7 +202,7 @@ protected void updateModelForComposedSchema( | |||
| * @param propertyName | ||||
| * The name of the property to remove. | ||||
| */ | ||||
| @SuppressWarnings( { "rawtypes", "unchecked", "ReplaceInefficientStreamCount" } ) | ||||
| @SuppressWarnings( { "rawtypes", "unchecked" } ) | ||||
| private void preprocessRemoveProperty( | ||||
| @Nonnull final OpenAPI openAPI, | ||||
| @Nonnull final String schemaName, | ||||
|
|
@@ -161,7 +216,7 @@ private void preprocessRemoveProperty( | |||
| boolean removed = false; | ||||
|
|
||||
| final Predicate<Schema> remove = | ||||
| s -> s != null && s.getProperties() != null && s.getProperties().remove(propertyName) != null; | ||||
| s -> s.getProperties() != null && s.getProperties().remove(propertyName) != null; | ||||
|
Comment on lines
-164
to
+219
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Question) Why is it save to remove the null check here? |
||||
| final var schemasQueued = new LinkedList<Schema>(); | ||||
| final var schemasDone = new HashSet<Schema>(); | ||||
| schemasQueued.add(schema); | ||||
|
|
@@ -200,6 +255,11 @@ private void preprocessRemoveRedundancies( @Nonnull final OpenAPI openAPI ) | |||
| final var refs = new LinkedHashSet<String>(); | ||||
| final var pattern = Pattern.compile("\\$ref: #/components/schemas/(\\w+)"); | ||||
|
|
||||
| // OAS 3.1 documents may have no paths (webhooks-only or components-only). | ||||
| if( openAPI.getPaths() == null || openAPI.getPaths().isEmpty() ) { | ||||
| return; | ||||
|
CharlesDuboisSAP marked this conversation as resolved.
|
||||
| } | ||||
|
|
||||
| // find and queue schemas nested in paths | ||||
| for( final var path : openAPI.getPaths().values() ) { | ||||
| final var m = pattern.matcher(path.toString()); | ||||
|
|
@@ -211,6 +271,20 @@ private void preprocessRemoveRedundancies( @Nonnull final OpenAPI openAPI ) | |||
| } | ||||
| } | ||||
|
|
||||
| // OAS 3.1 adds components/pathItems — traverse them for schema references too | ||||
| final var pathItems = openAPI.getComponents() != null ? openAPI.getComponents().getPathItems() : null; | ||||
| if( pathItems != null ) { | ||||
| for( final var pathItem : pathItems.values() ) { | ||||
| final var m = pattern.matcher(pathItem.toString()); | ||||
| while( m.find() ) { | ||||
| final var name = m.group(1); | ||||
| final var schema = openAPI.getComponents().getSchemas().get(name); | ||||
| queue.add(schema); | ||||
| refs.add(m.group(0).split(" ")[1]); | ||||
|
ZhongpinWang marked this conversation as resolved.
|
||||
| } | ||||
| } | ||||
| } | ||||
|
|
||||
| while( !queue.isEmpty() ) { | ||||
| final var s = queue.remove(); | ||||
| if( s == null || !done.add(s) ) { | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package com.sap.cloud.sdk.datamodel.openapi.generator; | ||
|
|
||
| import java.util.Map; | ||
| import java.util.Set; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
|
|
||
|
|
@@ -11,10 +12,13 @@ | |
| import io.swagger.v3.oas.models.media.Schema; | ||
|
|
||
| /** | ||
| * Fix Api client methods with oneOf primitive param to stay simplified from OpenAPI generator 7.22.0 | ||
| * Fix Api client methods with oneOf primitive param to stay simplified from OpenAPI generator 7.22.0. Also adds OAS | ||
| * 3.1-aware normalisation: nullable warnings, example deprecation warnings, and contentEncoding/contentMediaType → | ||
| * format mapping for binary file uploads. | ||
| */ | ||
| public class CustomOpenAPINormalizer extends OpenAPINormalizer | ||
| { | ||
| private final boolean isOas31; | ||
|
|
||
| /** | ||
| * Initializes OpenAPI Normalizer with a set of rules | ||
|
|
@@ -27,10 +31,12 @@ public class CustomOpenAPINormalizer extends OpenAPINormalizer | |
| public CustomOpenAPINormalizer( final @Nonnull OpenAPI openAPI, final @Nonnull Map<String, String> inputRules ) | ||
| { | ||
| super(openAPI, inputRules); | ||
| this.isOas31 = OasVersionUtil.isOas31(openAPI); | ||
| } | ||
|
|
||
| /** | ||
| * Normalize reference schema with allOf to support sibling properties | ||
| * Normalize reference schema with allOf to support sibling properties. Also warns on OAS 3.1 deprecated keywords | ||
| * when processing a 3.1 spec. | ||
| * | ||
| * @param schema | ||
| * Schema | ||
|
|
@@ -46,15 +52,28 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema ) | |
| LOGGER.warn("Type(s) cleared (set to null) given $ref is set to {}.", schema.get$ref()); | ||
| } | ||
|
|
||
| // warn when deprecated nullable: true is used in an OAS 3.1 spec | ||
| if( isOas31 && schema.getNullable() != null ) { | ||
| LOGGER | ||
| .warn( | ||
| "'nullable: true' is not a valid OAS 3.1 keyword on $ref schema '{}'. " | ||
| + "Use anyOf: [{{$ref: \"...\"}}, {{type: \"null\"}}] instead.", | ||
| schema.get$ref()); | ||
| } | ||
|
|
||
|
Comment on lines
+55
to
+63
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Minor) Maybe we should test this path as well? |
||
| if( schema.getTitle() != null | ||
| || schema.getDescription() != null | ||
| || schema.getNullable() != null | ||
| || schema.getDefault() != null | ||
| || schema.getDeprecated() != null | ||
| || schema.getMaximum() != null | ||
| || schema.getMinimum() != null | ||
| // OAS 3.0 boolean exclusiveMaximum/exclusiveMinimum | ||
| || schema.getExclusiveMaximum() != null | ||
| || schema.getExclusiveMinimum() != null | ||
| // OAS 3.1 numeric exclusiveMaximumValue/exclusiveMinimumValue | ||
| || schema.getExclusiveMaximumValue() != null | ||
| || schema.getExclusiveMinimumValue() != null | ||
| || schema.getMaxItems() != null | ||
| || schema.getMinItems() != null | ||
| || schema.getMaxProperties() != null | ||
|
|
@@ -65,6 +84,8 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema ) | |
| || schema.getReadOnly() != null | ||
| || schema.getExample() != null | ||
| || (schema.getExamples() != null && !schema.getExamples().isEmpty()) | ||
| // OAS 3.1 const keyword as $ref sibling | ||
| || schema.getConst() != null | ||
| || schema.getMultipleOf() != null | ||
| || schema.getPattern() != null | ||
| || (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) ) { | ||
|
|
@@ -87,4 +108,41 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema ) | |
| schema.set$ref(null); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes any schema (not just $ref schemas). Adds OAS 3.1 specific mappings: | ||
| * <ul> | ||
| * <li>warn on deprecated singular {@code example} keyword in OAS 3.1 schemas</li> | ||
| * <li>map {@code contentEncoding}/{@code contentMediaType} to {@code format} for binary file uploads</li> | ||
| * </ul> | ||
| */ | ||
| @Override | ||
| @Nonnull | ||
| @SuppressWarnings( { "rawtypes" } ) | ||
| public Schema normalizeSchema( final @Nonnull Schema schema, final @Nonnull Set<Schema> visitedSchemas ) | ||
| { | ||
| // warn on deprecated singular `example` in OAS 3.1 Schema Objects | ||
| if( isOas31 && schema.getExample() != null ) { | ||
| LOGGER | ||
| .warn( | ||
| "The 'example' keyword is deprecated in OAS 3.1 Schema Objects. " | ||
| + "Use 'examples: [...]' (array form) instead."); | ||
| } | ||
|
|
||
| // map OAS 3.1 contentEncoding/contentMediaType to legacy format keyword | ||
| // so that downstream type-mapping (File -> byte[]) continues to work. | ||
| if( isOas31 && schema.getFormat() == null ) { | ||
| if( "base64".equalsIgnoreCase(schema.getContentEncoding()) ) { | ||
| schema.setFormat("byte"); | ||
| } else if( schema.getContentEncoding() != null ) { | ||
| // Any other content encoding (e.g., "binary") → treat as binary | ||
| schema.setFormat("binary"); | ||
| } else if( schema.getContentMediaType() != null ) { | ||
| // contentMediaType without contentEncoding → binary stream | ||
| schema.setFormat("binary"); | ||
|
ZhongpinWang marked this conversation as resolved.
|
||
| } | ||
| } | ||
|
|
||
| return super.normalizeSchema(schema, visitedSchemas); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.sap.cloud.sdk.datamodel.openapi.generator; | ||
|
|
||
| import javax.annotation.Nonnull; | ||
|
|
||
| import io.swagger.v3.oas.models.OpenAPI; | ||
|
|
||
| final class OasVersionUtil | ||
| { | ||
| private OasVersionUtil() | ||
| { | ||
| } | ||
|
|
||
| static boolean isOas31( @Nonnull final OpenAPI openAPI ) | ||
| { | ||
| final String version = openAPI.getOpenapi(); | ||
| return version != null && version.startsWith("3.1"); | ||
|
Jonas-Isr marked this conversation as resolved.
|
||
| } | ||
|
ZhongpinWang marked this conversation as resolved.
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Very Minor)
Import once. Is also used below (l.188).