diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 8aecaeab5865..3dbf56e879a1 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -6876,23 +6876,21 @@ protected void addVarsRequiredVarsAdditionalProps(Schema schema, IJsonSchemaVali if (!"object".equals(schema.getType())) { return; } - if (schema instanceof ObjectSchema) { - ObjectSchema objSchema = (ObjectSchema) schema; - HashSet requiredVars = new HashSet<>(); - if (objSchema.getRequired() != null) { - requiredVars.addAll(objSchema.getRequired()); - } - if (objSchema.getProperties() != null && objSchema.getProperties().size() > 0) { - property.setHasVars(true); - } - addVars(property, property.getVars(), objSchema.getProperties(), requiredVars); - List requireCpVars = property.getVars() - .stream() - .filter(p -> Boolean.TRUE.equals(p.required)).collect(Collectors.toList()); - property.setRequiredVars(requireCpVars); - if (property.getRequiredVars() != null && property.getRequiredVars().size() > 0) { - property.setHasRequired(true); - } + + HashSet requiredVars = new HashSet<>(); + if (schema.getRequired() != null) { + requiredVars.addAll(schema.getRequired()); + } + if (schema.getProperties() != null && schema.getProperties().size() > 0) { + property.setHasVars(true); + } + addVars(property, property.getVars(), schema.getProperties(), requiredVars); + List requireCpVars = property.getVars() + .stream() + .filter(p -> Boolean.TRUE.equals(p.required)).collect(Collectors.toList()); + property.setRequiredVars(requireCpVars); + if (property.getRequiredVars() != null && property.getRequiredVars().size() > 0) { + property.setHasRequired(true); } } diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index c4adb5adc8ff..b48e1a9b2f2e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -25,20 +25,33 @@ import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.DateSchema; +import io.swagger.v3.oas.models.media.DateTimeSchema; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.NumberSchema; +import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.security.*; +import io.swagger.v3.oas.models.parameters.QueryParameter; +import io.swagger.v3.oas.models.security.OAuthFlow; +import io.swagger.v3.oas.models.security.OAuthFlows; +import io.swagger.v3.oas.models.security.Scopes; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.tags.Tag; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.comparator.PathFileComparator; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.api.TemplateDefinition; +import org.openapitools.codegen.api.TemplateFileType; import org.openapitools.codegen.api.TemplatePathLocator; import org.openapitools.codegen.api.TemplateProcessor; -import org.openapitools.codegen.config.GlobalSettings; import org.openapitools.codegen.api.TemplatingEngineAdapter; -import org.openapitools.codegen.api.TemplateFileType; +import org.openapitools.codegen.config.GlobalSettings; import org.openapitools.codegen.ignore.CodegenIgnoreProcessor; import org.openapitools.codegen.languages.PythonClientCodegen; import org.openapitools.codegen.languages.PythonExperimentalClientCodegen; @@ -61,12 +74,28 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.*; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.ZonedDateTime; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; import java.util.concurrent.ConcurrentSkipListSet; import java.util.function.Function; import java.util.stream.Collectors; @@ -550,7 +579,7 @@ void generateApis(List files, List allOperations, List> paths = processPaths(this.openAPI.getPaths()); + Map> paths = processPaths(this.openAPI.getPaths(), allModels); Set apisToGenerate = null; String apiNames = GlobalSettings.getProperty("apis"); if (apiNames != null && !apiNames.isEmpty()) { @@ -808,9 +837,9 @@ Map buildSupportFileBundle(List allOperations, Li *

* Examples: *

- * boolean hasOAuthMethods + * boolean hasOAuthMethods *

- * List<CodegenSecurity> oauthMethods + * List<CodegenSecurity> oauthMethods * * @param bundle the map which the booleans and collections will be added */ @@ -1046,7 +1075,7 @@ private File processTemplateToFile(Map templateData, String temp } } - public Map> processPaths(Paths paths) { + public Map> processPaths(Paths paths, List allModels) { Map> ops = new TreeMap<>(); // when input file is not valid and doesn't contain any paths if (paths == null) { @@ -1055,19 +1084,19 @@ public Map> processPaths(Paths paths) { for (Map.Entry pathsEntry : paths.entrySet()) { String resourcePath = pathsEntry.getKey(); PathItem path = pathsEntry.getValue(); - processOperation(resourcePath, "get", path.getGet(), ops, path); - processOperation(resourcePath, "head", path.getHead(), ops, path); - processOperation(resourcePath, "put", path.getPut(), ops, path); - processOperation(resourcePath, "post", path.getPost(), ops, path); - processOperation(resourcePath, "delete", path.getDelete(), ops, path); - processOperation(resourcePath, "patch", path.getPatch(), ops, path); - processOperation(resourcePath, "options", path.getOptions(), ops, path); - processOperation(resourcePath, "trace", path.getTrace(), ops, path); + processOperation(resourcePath, "get", path.getGet(), ops, path, allModels); + processOperation(resourcePath, "head", path.getHead(), ops, path, allModels); + processOperation(resourcePath, "put", path.getPut(), ops, path, allModels); + processOperation(resourcePath, "post", path.getPost(), ops, path, allModels); + processOperation(resourcePath, "delete", path.getDelete(), ops, path, allModels); + processOperation(resourcePath, "patch", path.getPatch(), ops, path, allModels); + processOperation(resourcePath, "options", path.getOptions(), ops, path, allModels); + processOperation(resourcePath, "trace", path.getTrace(), ops, path, allModels); } return ops; } - private void processOperation(String resourcePath, String httpMethod, Operation operation, Map> operations, PathItem path) { + private void processOperation(String resourcePath, String httpMethod, Operation operation, Map> operations, PathItem path, List allModels) { if (operation == null) { return; } @@ -1115,6 +1144,39 @@ private void processOperation(String resourcePath, String httpMethod, Operation if (operation.getParameters() != null) { for (Parameter parameter : operation.getParameters()) { operationParameters.add(generateParameterId(parameter)); + + /* + Expand query parameters whose schema is defined by $ref. Find the referenced model, map its properties and add them to the schema. + */ + if ((parameter instanceof QueryParameter || "query".equalsIgnoreCase(parameter.getIn())) + && parameter.getSchema() != null && parameter.getSchema().get$ref() != null + && Parameter.StyleEnum.DEEPOBJECT != parameter.getStyle()) { + + // Find the referenced model + Optional matchedModel = allModels.stream() + .map(ModelMap::getModel) + .filter(codegenModel -> codegenModel != null + && codegenModel.getName() != null + && codegenModel.getName().equalsIgnoreCase(parameter.getName())).findFirst(); + + // If the model is present map its properties and add them to the schema + if (matchedModel.isPresent()) { + CodegenModel model = matchedModel.get(); + + Schema schema = parameter.getSchema(); + parameter.set$ref(schema.get$ref()); + schema.set$ref(null); + schema.setType(model.getDataType().toLowerCase(Locale.ROOT)); + + Map properties = mapProperties(model); + if(!properties.isEmpty()) { + if(schema.getProperties() != null) { + properties.putAll(schema.getProperties()); + } + schema.setProperties(properties); + } + } + } } } @@ -1170,6 +1232,75 @@ private void processOperation(String resourcePath, String httpMethod, Operation } + /*** + * Map all properties of a given input to corresponding schemas + * + * @param model the model to be mapped + * @return a map containing property name and its corresponding schema + */ + private Map mapProperties(CodegenModel model) { + Map properties = new LinkedHashMap<>(); + + for (CodegenProperty property : model.getVars()) { + Schema mappedSchema = mapToSchema(property); + + if (mappedSchema != null) { + mappedSchema.setDescription(model.getDescription()); + properties.put(property.name, mappedSchema); + } + } + return properties; + } + + /** + * Map a given property to a schema by accounting for its openApi type + * + * @param property the property to be mapped + * @return the mapped schema, null if the openApi type is not supported + */ + private Schema mapToSchema(CodegenProperty property) { + switch (property.openApiType) { + case "string": + StringSchema stringSchema = new StringSchema(); + stringSchema.setMinLength(property.getMinLength()); + stringSchema.setMaxLength(property.getMaxLength()); + stringSchema.setPattern(property.getPattern()); + + return stringSchema; + case "integer": + IntegerSchema integerSchema = new IntegerSchema(); + integerSchema.setMinimum(toBigDecimal(property.getMinimum())); + integerSchema.setMaximum(toBigDecimal(property.getMaximum())); + + return integerSchema; + case "number": + NumberSchema floatSchema = new NumberSchema(); + floatSchema.setMinimum(toBigDecimal(property.getMinimum())); + floatSchema.setMaximum(toBigDecimal(property.getMaximum())); + return floatSchema; + case "boolean": + return new BooleanSchema(); + case "object": + return new ObjectSchema(); + case "array": + ArraySchema arraySchema = new ArraySchema(); + arraySchema.setMinItems(property.getMinItems()); + arraySchema.setMaxItems(property.getMaxItems()); + + if (property.getItems() != null) { + arraySchema.setItems(mapToSchema(property.getItems())); + } + + return arraySchema; + } + + return null; + } + + private BigDecimal toBigDecimal(String value) { + return value == null ? null : new BigDecimal(value); + } + private static String generateParameterId(Parameter parameter) { return parameter.getName() + ":" + parameter.getIn(); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 412b7b95a32e..6b34f78109b3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -3381,12 +3381,12 @@ public void testHasRequiredInProperties() { "ComposedHasAllofOptPropHasPropertiesNoRequired", "ComposedHasAllofOptPropNoPropertiesHasRequired", // TODO: hasRequired should be true, fix this "ObjectHasPropertiesHasRequired", // False because this is extracted into another component and is a ref - "ComposedNoAllofPropsHasPropertiesHasRequired", // False because this is extracted into another component and is a ref - "ComposedHasAllofOptPropHasPropertiesHasRequired", // TODO: hasRequired should be true, fix this "ComposedHasAllofReqPropNoPropertiesNoRequired", "ComposedHasAllofReqPropHasPropertiesNoRequired", "ComposedHasAllofReqPropNoPropertiesHasRequired", // TODO: hasRequired should be true, fix this - "ComposedHasAllofReqPropHasPropertiesHasRequired" // TODO: hasRequired should be true, fix this + "ComposedNoAllofPropsHasPropertiesHasRequired", + "ComposedHasAllofOptPropHasPropertiesHasRequired", + "ComposedHasAllofReqPropHasPropertiesHasRequired" )); HashSet modelNamesWithRequired = new HashSet(Arrays.asList( )); @@ -3703,7 +3703,7 @@ public void testComposedPropertyTypes() { modelName = "ObjectWithComposedProperties"; CodegenModel m = codegen.fromModel(modelName, openAPI.getComponents().getSchemas().get(modelName)); /* TODO inline allOf schema are created as separate models and the following assumptions that - the properties are non-model are no longer valid and need to be revised + the properties are non-model are no longer valid and need to be revised assertTrue(m.vars.get(0).getIsMap()); assertTrue(m.vars.get(1).getIsNumber()); assertTrue(m.vars.get(2).getIsUnboundedInteger()); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java index 778d1d889bb0..d6c5249f1d9d 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultGeneratorTest.java @@ -1,5 +1,6 @@ package org.openapitools.codegen; +import com.google.common.collect.Lists; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -354,7 +355,7 @@ public void testNonStrictProcessPaths() throws Exception { DefaultGenerator generator = new DefaultGenerator(); generator.opts(opts); - Map> result = generator.processPaths(openAPI.getPaths()); + Map> result = generator.processPaths(openAPI.getPaths(), Lists.newArrayList()); Assert.assertEquals(result.size(), 1); List defaultList = result.get("Default"); Assert.assertEquals(defaultList.size(), 2); @@ -379,7 +380,7 @@ public void testProcessPaths() throws Exception { DefaultGenerator generator = new DefaultGenerator(); generator.opts(opts); - Map> result = generator.processPaths(openAPI.getPaths()); + Map> result = generator.processPaths(openAPI.getPaths(), Lists.newArrayList()); Assert.assertEquals(result.size(), 1); List defaultList = result.get("Default"); Assert.assertEquals(defaultList.size(), 4); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index f3c64cfd172f..7ba2ee6a128a 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -44,6 +44,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.google.common.collect.Lists; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.CodegenModel; @@ -550,7 +551,7 @@ public void testAuthorizationScopeValues_Issue392() { clientOptInput.config(new JavaClientCodegen()); defaultGenerator.opts(clientOptInput); - final List codegenOperations = defaultGenerator.processPaths(openAPI.getPaths()).get("Pet"); + final List codegenOperations = defaultGenerator.processPaths(openAPI.getPaths(), Lists.newArrayList()).get("Pet"); // Verify GET only has 'read' scope final CodegenOperation getCodegenOperation = codegenOperations.stream().filter(it -> it.httpMethod.equals("GET")).collect(Collectors.toList()).get(0); @@ -608,7 +609,7 @@ public void testAuthorizationsMethodsSizeWhenFiltered() { clientOptInput.config(new JavaClientCodegen()); defaultGenerator.opts(clientOptInput); - final List codegenOperations = defaultGenerator.processPaths(openAPI.getPaths()).get("Pet"); + final List codegenOperations = defaultGenerator.processPaths(openAPI.getPaths(), Lists.newArrayList()).get("Pet"); final CodegenOperation getCodegenOperation = codegenOperations.stream().filter(it -> it.httpMethod.equals("GET")).collect(Collectors.toList()).get(0); assertTrue(getCodegenOperation.hasAuthMethods); @@ -951,7 +952,7 @@ public void testRestTemplateFormMultipart() throws IOException { * * UPDATE: the following test has been ignored due to https://github.com/OpenAPITools/openapi-generator/pull/11081/ * We will contact the contributor of the following test to see if the fix will break their use cases and - * how we can fix it accordingly. + * how we can fix it accordingly. */ @Test @Ignore @@ -1086,7 +1087,7 @@ void testNotDuplicateOauth2FlowsScopes() { final DefaultGenerator defaultGenerator = new DefaultGenerator(); defaultGenerator.opts(clientOptInput); - final Map> paths = defaultGenerator.processPaths(openAPI.getPaths()); + final Map> paths = defaultGenerator.processPaths(openAPI.getPaths(), Lists.newArrayList()); final List codegenOperations = paths.values().stream().flatMap(Collection::stream).collect(Collectors.toList()); final CodegenOperation getWithBasicAuthAndOauth = getByOperationId(codegenOperations, "getWithBasicAuthAndOauth"); @@ -1227,7 +1228,7 @@ public void testRestTemplateWithFreeFormInQueryParameters() throws IOException { final Path defaultApi = Paths.get(output + "/src/main/java/xyz/abcdef/ApiClient.java"); TestUtils.assertFileContains(defaultApi, "value instanceof Map"); } - + /** * See https://github.com/OpenAPITools/openapi-generator/issues/8352 */ @@ -1569,4 +1570,39 @@ public void testReferencedHeader2() throws Exception { .assertParameterAnnotations() .containsWithName("NotNull"); } + + /** + * See https://github.com/OpenAPITools/openapi-generator/issues/907 + */ + @Test + public void testExpandedQueryParamWithRef() throws IOException { + Map properties = new HashMap<>(); + properties.put(CodegenConstants.API_PACKAGE, "xyz.abcdef.api"); + + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("java") + .setLibrary(JavaClientCodegen.NATIVE) + .setAdditionalProperties(properties) + .setInputSpec("src/test/resources/3_0/issue907.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 38); + validateJavaSourceFiles(files); + + TestUtils.assertFileContains(Paths.get(output + "/src/main/java/xyz/abcdef/api/DefaultApi.java"), + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"integerProp\", buildQuery.getIntegerProp()));", + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"stringProp\", buildQuery.getStringProp()));", + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"booleanProp\", buildQuery.getBooleanProp()));", + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"arrayProp\", buildQuery.getArrayProp()));", + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"objectProp\", buildQuery.getObjectProp()));", + "localVarQueryParams.addAll(ApiClient.parameterToPairs(\"numberProp\", buildQuery.getNumberProp()));" + ); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/issue907.yaml b/modules/openapi-generator/src/test/resources/3_0/issue907.yaml new file mode 100644 index 000000000000..eec4134e6d77 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/issue907.yaml @@ -0,0 +1,59 @@ +openapi: 3.0.3 +info: + title: Issue 907 - ref query params + description: "Ref query params" + version: "1.0.0" +servers: + - url: localhost:8080 +paths: + /api: + get: + parameters: + - in: path + name: id + schema: + type: string + required: true + - in: query + name: BuildQuery + explode: true + schema: + $ref: '#/components/schemas/BuildQuery' + operationId: GetBuild + responses: + '200': + description: Some description. + content: + application/json: + schema: + $ref: '#/components/schemas/SomeReturnValue' +components: + schemas: + SomeReturnValue: + type: object + required: + - someValue + properties: + someValue: + type: string + BuildQuery: + type: object + properties: + integerProp: + type: integer + minimum: 0 + stringProp: + type: string + minLength: 1 + booleanProp: + type: boolean + arrayProp: + type: array + items: + type: string + minLength: 1 + objectProp: + type: object + numberProp: + type: number + minimum: 0