This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new 0ad9e02 CAMEL-15503 camel-openapi-java - Schema Definitions not generating correctly (#5477) 0ad9e02 is described below commit 0ad9e0242eede44bc8dce2c843fd021be6d7b7c5 Author: electrosaur <sramamoor...@guidewire.com> AuthorDate: Tue May 11 03:54:52 2021 -0700 CAMEL-15503 camel-openapi-java - Schema Definitions not generating correctly (#5477) --- components/camel-openapi-java/pom.xml | 6 + .../apache/camel/openapi/RestModelConverters.java | 148 +++++++++++++++--- .../apache/camel/openapi/RestOpenApiReader.java | 133 ++++++++++++++-- .../org/apache/camel/openapi/ComplexTypesTest.java | 169 +++++++++++++++++++++ .../RestOpenApiReaderModelBookOrderTest.java | 10 ++ .../org/apache/camel/openapi/model/CustomData.java | 26 ++++ .../openapi/model/GenericComplexRequestType.java | 52 +++++++ .../apache/camel/openapi/model/GenericData.java | 23 +++ .../openapi/model/SampleComplexRequestType.java | 71 +++++++++ .../openapi/model/SampleComplexResponseType.java | 59 +++++++ .../openapi/V2SchemaForComplexTypesRequest.json | 159 +++++++++++++++++++ .../openapi/V2SchemaForComplexTypesResponse.json | 102 +++++++++++++ .../openapi/V3SchemaForComplexTypesRequest.json | 169 +++++++++++++++++++++ .../openapi/V3SchemaForComplexTypesResponse.json | 112 ++++++++++++++ 14 files changed, 1209 insertions(+), 30 deletions(-) diff --git a/components/camel-openapi-java/pom.xml b/components/camel-openapi-java/pom.xml index 2a20972..203f836 100644 --- a/components/camel-openapi-java/pom.xml +++ b/components/camel-openapi-java/pom.xml @@ -82,6 +82,12 @@ <artifactId>apicurio-data-models</artifactId> </dependency> + <dependency> + <groupId>io.swagger.core.v3</groupId> + <artifactId>swagger-core</artifactId> + <version>2.1.9</version> + </dependency> + <!-- servlet api --> <dependency> diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestModelConverters.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestModelConverters.java index eb8460f..1223624 100644 --- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestModelConverters.java +++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestModelConverters.java @@ -16,25 +16,42 @@ */ package org.apache.camel.openapi; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; import io.apicurio.datamodels.core.models.Extension; import io.apicurio.datamodels.openapi.models.OasDocument; import io.apicurio.datamodels.openapi.models.OasSchema; -import io.apicurio.datamodels.openapi.v2.models.Oas20Definitions; import io.apicurio.datamodels.openapi.v2.models.Oas20Document; +import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; import io.apicurio.datamodels.openapi.v2.models.Oas20SchemaDefinition; import io.apicurio.datamodels.openapi.v3.models.Oas30Document; import io.apicurio.datamodels.openapi.v3.models.Oas30SchemaDefinition; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A Camel extended {@link ModelConverters} where we appending vendor extensions to include the java class name of the * model classes. */ +@SuppressWarnings("rawtypes") public class RestModelConverters { + private static final Logger LOG = LoggerFactory.getLogger(RestModelConverters.class); + private static final ModelConverters MODEL_CONVERTERS; + + static { + MODEL_CONVERTERS = ModelConverters.getInstance(); + MODEL_CONVERTERS.addConverter(new FqnModelResolver()); + } + public List<? extends OasSchema> readClass(OasDocument oasDocument, Class<?> clazz) { if (clazz.equals(java.io.File.class)) { // File is a special type in OAS2 / OAS3 (no model) @@ -53,19 +70,20 @@ public class RestModelConverters { if (!name.contains(".")) { return null; } + if (oasDocument.components == null) { oasDocument.components = oasDocument.createComponents(); } - Oas30SchemaDefinition model = oasDocument.components.createSchemaDefinition(clazz.getSimpleName()); - oasDocument.components.addSchemaDefinition(clazz.getSimpleName(), model); - model.type = clazz.getSimpleName(); - Extension extension = model.createExtension(); - extension.name = "x-className"; - Map<String, String> value = new HashMap<String, String>(); - value.put("type", "string"); - value.put("format", name); - extension.value = value; - model.addExtension("x-className", extension); + + Map<String, Schema> swaggerModel = MODEL_CONVERTERS.readAll(clazz); + swaggerModel.forEach((key, schema) -> { + Oas30SchemaDefinition model = oasDocument.components.createSchemaDefinition(key); + oasDocument.components.addSchemaDefinition(key, model); + processSchema(model, schema); + + addClassNameExtension(model, key); + }); + return oasDocument.components.getSchemaDefinitions(); } @@ -74,20 +92,112 @@ public class RestModelConverters { if (!name.contains(".")) { return null; } + if (oasDocument.definitions == null) { oasDocument.definitions = oasDocument.createDefinitions(); } - Oas20Definitions resolved = oasDocument.definitions; - Oas20SchemaDefinition model = resolved.createSchemaDefinition(clazz.getSimpleName()); - resolved.addDefinition(clazz.getSimpleName(), model); - model.type = clazz.getSimpleName(); - Extension extension = model.createExtension(); + + Map<String, Schema> swaggerModel = MODEL_CONVERTERS.getInstance().readAll(clazz); + swaggerModel.forEach((key, schema) -> { + Oas20SchemaDefinition model = oasDocument.definitions.createSchemaDefinition(key); + oasDocument.definitions.addDefinition(key, model); + processSchema(model, schema); + + addClassNameExtension(model, key); + }); + + return oasDocument.definitions.getDefinitions(); + } + + private void processSchema(OasSchema model, Schema schema) { + String type = schema.getType(); + model.type = type; + model.format = schema.getFormat(); + + String ref = schema.get$ref(); + if (ref != null) { + if (model instanceof Oas20Schema) { + // Change the prefix from 3.x to 2.x + model.$ref = RestOpenApiReader.OAS20_SCHEMA_DEFINITION_PREFIX + + ref.substring(RestOpenApiReader.OAS30_SCHEMA_DEFINITION_PREFIX.length()); + } else { + model.$ref = ref; + } + } + + if (type != null) { + switch (type) { + case "object": + if (schema.getProperties() != null) { + //noinspection unchecked + schema.getProperties().forEach((p, v) -> { + OasSchema property = model.createPropertySchema((String) p); + model.addProperty((String) p, property); + processSchema(property, (Schema) v); + }); + } + break; + case "array": + Schema items = ((ArraySchema) schema).getItems(); + OasSchema modelItems = model.createItemsSchema(); + model.items = modelItems; + processSchema(modelItems, items); + break; + case "string": + if (schema.getEnum() != null) { + //noinspection unchecked + model.enum_ = new ArrayList<String>(schema.getEnum()); + } + break; + case "number": + case "integer": + break; + default: + LOG.warn("Encountered unexpected type " + type + " in processing schema."); + break; + } + } + + if (schema.getRequired() != null) { + //noinspection unchecked + model.required = new ArrayList<String>(schema.getRequired()); + } + + if (schema.getAdditionalProperties() instanceof Schema) { + OasSchema additionalProperties = model.createAdditionalPropertiesSchema(); + model.additionalProperties = additionalProperties; + processSchema(additionalProperties, (Schema) schema.getAdditionalProperties()); + } + + if (schema.getExtensions() != null) { + //noinspection unchecked + schema.getExtensions().forEach((key, value) -> { + Extension extension = model.createExtension(); + extension.name = (String) key; + extension.value = value; + }); + } + } + + private void addClassNameExtension(OasSchema schema, String name) { + Extension extension = schema.createExtension(); extension.name = "x-className"; - Map<String, String> value = new HashMap<String, String>(); + Map<String, String> value = new HashMap<>(); value.put("type", "string"); value.put("format", name); extension.value = value; - model.addExtension("x-className", extension); - return resolved.getDefinitions(); + schema.addExtension("x-className", extension); + } + + private static class FqnModelResolver extends ModelResolver { + public FqnModelResolver() { + this(new ObjectMapper()); + } + + public FqnModelResolver(ObjectMapper mapper) { + super(mapper); + this._typeNameResolver.setUseFqn(true); + } } + } diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java index 3ad4d5c..004d80d 100644 --- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java +++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java @@ -20,22 +20,29 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodType; 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.Set; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; +import io.apicurio.datamodels.Library; import io.apicurio.datamodels.core.models.ExtensibleNode; import io.apicurio.datamodels.core.models.Extension; +import io.apicurio.datamodels.core.models.Node; import io.apicurio.datamodels.core.models.common.AuthorizationCodeOAuthFlow; import io.apicurio.datamodels.core.models.common.ImplicitOAuthFlow; import io.apicurio.datamodels.core.models.common.OAuthFlow; import io.apicurio.datamodels.core.models.common.SecurityRequirement; +import io.apicurio.datamodels.core.models.common.Tag; +import io.apicurio.datamodels.core.visitors.TraverserDirection; import io.apicurio.datamodels.openapi.models.OasDocument; import io.apicurio.datamodels.openapi.models.OasOperation; import io.apicurio.datamodels.openapi.models.OasParameter; @@ -50,6 +57,7 @@ import io.apicurio.datamodels.openapi.v2.models.Oas20Response; import io.apicurio.datamodels.openapi.v2.models.Oas20Schema; import io.apicurio.datamodels.openapi.v2.models.Oas20SchemaDefinition; import io.apicurio.datamodels.openapi.v2.models.Oas20SecurityScheme; +import io.apicurio.datamodels.openapi.v2.visitors.Oas20AllNodeVisitor; import io.apicurio.datamodels.openapi.v3.models.Oas30Document; import io.apicurio.datamodels.openapi.v3.models.Oas30Header; import io.apicurio.datamodels.openapi.v3.models.Oas30MediaType; @@ -59,6 +67,7 @@ import io.apicurio.datamodels.openapi.v3.models.Oas30Response; import io.apicurio.datamodels.openapi.v3.models.Oas30Schema; import io.apicurio.datamodels.openapi.v3.models.Oas30SchemaDefinition; import io.apicurio.datamodels.openapi.v3.models.Oas30SecurityScheme; +import io.apicurio.datamodels.openapi.v3.visitors.Oas30AllNodeVisitor; import org.apache.camel.CamelContext; import org.apache.camel.model.rest.RestDefinition; import org.apache.camel.model.rest.RestOperationParamDefinition; @@ -87,6 +96,15 @@ import static java.lang.invoke.MethodHandles.publicLookup; */ public class RestOpenApiReader { + public static final String OAS20_SCHEMA_DEFINITION_PREFIX = "#/definitions/"; + public static final String OAS30_SCHEMA_DEFINITION_PREFIX = "#/components/schemas/"; + // Types that are not allowed in references. + private static final Set<String> NO_REFERENCE_TYPE_NAMES = new HashSet<>( + Arrays.asList( + "byte", "char", "short", "int", "java.lang.Integer", "long", "java.lang.Long", "float", "java.lang.Float", + "double", "java.lang.Double", "string", "java.lang.String", "boolean", "java.lang.Boolean", + "file", "java.io.File")); + private static String getValue(CamelContext camelContext, String text) { return camelContext.resolvePropertyPlaceholders(text); } @@ -136,6 +154,27 @@ public class RestOpenApiReader { parse(camelContext, openApi, rest, camelContextId, classResolver); } + shortenClassNames(openApi); + + /* + * Fixes the problem of not generating the "paths" section when no rest route is defined. + * A schema with no paths is considered invalid. + */ + if (openApi.paths == null) { + openApi.paths = openApi.createPaths(); + } + + /* + * Fixes the problem of generating duplicated tags which is invalid per the specification + */ + if (openApi.tags != null) { + openApi.tags = new ArrayList<>( + openApi.tags + .stream() + .collect(Collectors.toMap(Tag::getName, Function.identity(), (prev, current) -> prev)) + .values()); + } + // configure before returning openApi = config.configure(openApi); return openApi; @@ -148,7 +187,7 @@ public class RestOpenApiReader { List<VerbDefinition> verbs = new ArrayList<>(rest.getVerbs()); // must sort the verbs by uri so we group them together when an uri has multiple operations - Collections.sort(verbs, new VerbOrdering(camelContext)); + verbs.sort(new VerbOrdering(camelContext)); // we need to group the operations within the same tag, so use the path as default if not // configured String pathAsTag = getValue(camelContext, rest.getTag() != null @@ -567,7 +606,7 @@ public class RestOpenApiReader { String ref = modelTypeAsRef(type, openApi); if (ref != null) { Oas30Schema refModel = (Oas30Schema) bp.createSchema(); - refModel.$ref = "#/components/schemas/" + ref; + refModel.$ref = OAS30_SCHEMA_DEFINITION_PREFIX + ref; bp.schema = refModel; } else { OasSchema model = (Oas30Schema) bp.createSchema(); @@ -790,7 +829,7 @@ public class RestOpenApiReader { String ref = modelTypeAsRef(type, openApi); if (ref != null) { Oas20Schema refModel = (Oas20Schema) bp.createSchema(); - refModel.$ref = "#/definitions/" + ref; + refModel.$ref = OAS20_SCHEMA_DEFINITION_PREFIX + ref; bp.schema = refModel; } else { OasSchema model = (Oas20Schema) bp.createSchema(); @@ -1216,13 +1255,11 @@ public class RestOpenApiReader { typeName = typeName.substring(0, typeName.length() - 2); } - OasSchema model = asModel(typeName, openApi); - if (model != null) { - typeName = model.type; - return typeName; + if (NO_REFERENCE_TYPE_NAMES.contains(typeName)) { + return null; } - return null; + return typeName; } private OasSchema modelTypeAsProperty(String typeName, OasDocument openApi, OasSchema prop) { @@ -1235,9 +1272,9 @@ public class RestOpenApiReader { if (ref != null) { if (openApi instanceof Oas20Document) { - prop.$ref = "#/definitions/" + ref; + prop.$ref = OAS20_SCHEMA_DEFINITION_PREFIX + ref; } else if (openApi instanceof Oas30Document) { - prop.$ref = "#/components/schemas/" + ref; + prop.$ref = OAS30_SCHEMA_DEFINITION_PREFIX + ref; } } else { // special for byte arrays @@ -1372,4 +1409,78 @@ public class RestOpenApiReader { } } + private void shortenClassNames(OasDocument document) { + if (document instanceof Oas30Document) { + Oas30Document oas30Document = (Oas30Document) document; + if (oas30Document.components == null || oas30Document.components.schemas == null) { + return; + } + } else { + Oas20Document oas20Document = (Oas20Document) document; + if (oas20Document.definitions == null || oas20Document.definitions.getDefinitions() == null) { + return; + } + } + + // Make a mapping from full name to possibly shortened name. + Map<String, String> names = new HashMap<>(); + Stream<String> schemaStream; + if (document instanceof Oas30Document) { + schemaStream = ((Oas30Document) document).components.schemas.keySet().stream(); + } else { + schemaStream = ((Oas20Document) document).definitions.getDefinitions().stream() + .map(Oas20SchemaDefinition::getName); + } + schemaStream.forEach(key -> { + String s = key.replaceAll("[^a-zA-Z0-9.-_]", "_"); + String shortName = s.substring(s.lastIndexOf('.') + 1); + names.put(key, names.containsValue(shortName) ? s : shortName); + }); + + if (document instanceof Oas30Document) { + Library.visitTree(document, new Oas30AllNodeVisitor() { + @Override + protected void visitNode(Node node) { + if (node instanceof Oas30SchemaDefinition) { + Oas30SchemaDefinition definition = (Oas30SchemaDefinition) node; + definition.rename(fixSchemaReference(definition.getName(), names, OAS30_SCHEMA_DEFINITION_PREFIX)); + } else if (node instanceof Oas30Schema) { + Oas30Schema schema = (Oas30Schema) node; + String ref = schema.$ref; + if (ref != null) { + schema.$ref = OAS30_SCHEMA_DEFINITION_PREFIX + + fixSchemaReference(ref, names, OAS30_SCHEMA_DEFINITION_PREFIX); + } + } + } + }, TraverserDirection.down); + } else { + Library.visitTree(document, new Oas20AllNodeVisitor() { + @Override + protected void visitNode(Node node) { + if (node instanceof Oas20SchemaDefinition) { + Oas20SchemaDefinition definition = (Oas20SchemaDefinition) node; + definition.rename(fixSchemaReference(definition.getName(), names, OAS20_SCHEMA_DEFINITION_PREFIX)); + } else if (node instanceof Oas20Schema) { + Oas20Schema schema = (Oas20Schema) node; + String ref = schema.$ref; + if (ref != null) { + schema.$ref = OAS20_SCHEMA_DEFINITION_PREFIX + + fixSchemaReference(ref, names, OAS20_SCHEMA_DEFINITION_PREFIX); + } + } + } + }, TraverserDirection.down); + } + } + + private String fixSchemaReference(String ref, Map<String, String> names, String prefix) { + if (ref.startsWith(prefix)) { + ref = ref.substring(prefix.length()); + } + + String name = names.get(ref); + return name == null ? ref : name; + } + } diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/ComplexTypesTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/ComplexTypesTest.java new file mode 100644 index 0000000..5aeacea --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/ComplexTypesTest.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.apicurio.datamodels.Library; +import io.apicurio.datamodels.openapi.models.OasDocument; +import org.apache.camel.BindToRegistry; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.engine.DefaultClassResolver; +import org.apache.camel.model.rest.RestBindingMode; +import org.apache.camel.model.rest.RestDefinition; +import org.apache.camel.openapi.model.SampleComplexRequestType; +import org.apache.camel.openapi.model.SampleComplexResponseType; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class ComplexTypesTest extends CamelTestSupport { + private static final Logger LOG = LoggerFactory.getLogger(ComplexTypesTest.class); + + @SuppressWarnings("unused") + @BindToRegistry("dummy-rest") + private final DummyRestConsumerFactory factory = new DummyRestConsumerFactory(); + + @Override + protected RouteBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() { + rest().securityDefinitions() + .oauth2("global") + .accessCode("https://AUTHORIZATION_URL", "https://TOKEN_URL") + .withScope("groups", "Required scopes for Camel REST APIs"); + + rest().post("/complexRequest") + .description("Demo complex request type") + .type(SampleComplexRequestType.class) + .consumes("application/json") + .produces("text/plain") + .bindingMode(RestBindingMode.json) + .responseMessage() + .code(200) + .message("Receives a complex object as parameter") + .endResponseMessage() + .outType(SampleComplexResponseType.InnerClass.class) + .route() + .routeId("complex request type") + .log("/complex request invoked"); + + rest().get("/complexResponse") + .description("Demo complex response type") + .type(SampleComplexRequestType.InnerClass.class) + .consumes("application/json") + .outType(SampleComplexResponseType.class) + .produces("application/json") + .bindingMode(RestBindingMode.json) + .responseMessage() + .code(200) + .message("Returns a complex object") + .endResponseMessage() + .route() + .routeId("complex response type") + .log("/complex invoked") + .setBody(constant(new SampleComplexResponseType())); + } + }; + } + + @Test + public void testV3SchemaForComplexTypesRequest() throws Exception { + checkSchemaGeneration("/complexRequest", "3.0", "V3SchemaForComplexTypesRequest.json"); + } + + @Test + public void testV2SchemaForComplexTypesRequest() throws Exception { + checkSchemaGeneration("/complexRequest", "2.0", "V2SchemaForComplexTypesRequest.json"); + } + + @Test + public void testV3SchemaForComplexTypesResponse() throws Exception { + checkSchemaGeneration("/complexResponse", "3.0", "V3SchemaForComplexTypesResponse.json"); + } + + @Test + public void testV2SchemaForComplexTypesResponse() throws Exception { + checkSchemaGeneration("/complexResponse", "2.0", "V2SchemaForComplexTypesResponse.json"); + } + + private void checkSchemaGeneration(String uri, String apiVersion, String schemaResource) throws Exception { + BeanConfig config = getBeanConfig(apiVersion); + + List<RestDefinition> rests = context.getRestDefinitions().stream() + // So we get the security schema and the route schema + .filter(def -> def.getVerbs().isEmpty() || def.getVerbs().get(0).getUri().equals(uri)) + .collect(Collectors.toList()); + + RestOpenApiReader reader = new RestOpenApiReader(); + OasDocument openApi = reader.read(context, rests, null, config, context.getName(), new DefaultClassResolver()); + assertNotNull(openApi); + + ObjectMapper mapper = new ObjectMapper(); + mapper.enable(SerializationFeature.INDENT_OUTPUT); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + Object dump = Library.writeNode(openApi); + String json = mapper.writeValueAsString(dump); + + LOG.info(json); + + json = generify(json); + + InputStream is = getClass().getClassLoader().getResourceAsStream("org/apache/camel/openapi/" + schemaResource); + assertNotNull(is); + String expected = new BufferedReader( + new InputStreamReader(is, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("\n")); + is.close(); + + assertEquals(expected, json); + } + + private BeanConfig getBeanConfig(String apiVersion) { + BeanConfig config = new BeanConfig(); + config.setHost("localhost:8080"); + config.setSchemes(new String[] { "http" }); + config.setBasePath("/api"); + config.setTitle("Camel User store"); + config.setLicense("Apache 2.0"); + config.setLicenseUrl("https://www.apache.org/licenses/LICENSE-2.0.html"); + config.setVersion(apiVersion); + return config; + } + + private String generify(String input) { + input = input.replaceAll("\"openapi\" : \"3\\..*\",", "\"openapi\" : \"3.x\","); + input = input.replaceAll("\"swagger\" : \"2\\..*\",", "\"swagger\" : \"2.x\","); + input = input.replaceAll("\"operationId\" : \"verb.*\",", "\"operationId\" : \"verb\","); + input = input.replaceAll("\"x-camelContextId\" : \"camel.*\",", "\"x-camelContextId\" : \"camel\","); + return input; + } +} diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderModelBookOrderTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderModelBookOrderTest.java index 2e45e5b..b8257ab 100644 --- a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderModelBookOrderTest.java +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderModelBookOrderTest.java @@ -48,6 +48,16 @@ public class RestOpenApiReaderModelBookOrderTest extends CamelTestSupport { return new RouteBuilder() { @Override public void configure() throws Exception { + rest() + .securityDefinitions() + .oauth2("global") + .accessCode( + "https://AUTHORIZATION_URL", + "https://TOKEN_URL" + ) + .withScope("groups", "Required scopes for Camel REST APIs") + .end(); + // this user REST service is json only rest("/books").tag("dude").description("Book order service").consumes("application/json") .produces("application/json") diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/CustomData.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/CustomData.java new file mode 100644 index 0000000..ff3c945 --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/CustomData.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi.model; + +class CustomData implements GenericData { + + private String customDataField; + + public String getCustomDataField() { + return customDataField; + } +} diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericComplexRequestType.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericComplexRequestType.java new file mode 100644 index 0000000..d10c4ed --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericComplexRequestType.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi.model; + +import java.util.List; +import java.util.Map; + +/** + * Sample request POJO that uses Generics. + */ +public class GenericComplexRequestType<T extends GenericData> { + + private T data; + private List<T> listOfData; + private List<List<T>> listOfListOfData; + private Map<String, T> mapOfData; + private Map<String, Map<String, T>> mapOfMapOfData; + + public T getData() { + return data; + } + + public List<T> getListOfData() { + return listOfData; + } + + public Map<String, T> getMapOfData() { + return mapOfData; + } + + public List<List<T>> getListOfListOfData() { + return listOfListOfData; + } + + public Map<String, Map<String, T>> getMapOfMapOfData() { + return mapOfMapOfData; + } +} diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericData.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericData.java new file mode 100644 index 0000000..6e37cdf --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/GenericData.java @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi.model; + +/** + * Generic interface used to validate that inheritance and generics also works with the camel open-api compoment + */ +interface GenericData { +} diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexRequestType.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexRequestType.java new file mode 100644 index 0000000..16b9675 --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexRequestType.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi.model; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SampleComplexRequestType extends GenericComplexRequestType<CustomData> { + @JsonProperty(required = true) + private String requestField1; + private String requestField2; + private List<String> listOfStrings; + private String[] arrayOfString; + private Map<String, String> mapOfStrings; + private TimeUnit timeUnit; + private InnerClass innerClass; + + public String getRequestField1() { + return requestField1; + } + + public String getRequestField2() { + return requestField2; + } + + public List<String> getListOfStrings() { + return listOfStrings; + } + + public String[] getArrayOfString() { + return arrayOfString; + } + + @JsonProperty(required = true) + public Map<String, String> getMapOfStrings() { + return mapOfStrings; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + public InnerClass getInnerClass() { + return innerClass; + } + + public static class InnerClass { + private long longField; + + public long getLongField() { + return longField; + } + } +} diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexResponseType.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexResponseType.java new file mode 100644 index 0000000..ba7dcdd --- /dev/null +++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/model/SampleComplexResponseType.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.openapi.model; + +import java.time.Month; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SampleComplexResponseType { + @JsonProperty(required = true) + private String responseField1 = "Response Field 1"; + private String responseField2 = "Response Field 2"; + private String[] arrayOfStrings; + private Month month; + private InnerClass innerClass; + + public String getResponseField1() { + return responseField1; + } + + public String getResponseField2() { + return responseField2; + } + + @JsonProperty(required = true) + public String[] getArrayOfStrings() { + return arrayOfStrings; + } + + public Month getMonth() { + return month; + } + + public InnerClass getInnerClass() { + return innerClass; + } + + public static class InnerClass { + double doubleField; + + public double getDoubleField() { + return doubleField; + } + } +} diff --git a/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesRequest.json b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesRequest.json new file mode 100644 index 0000000..92d0fa7 --- /dev/null +++ b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesRequest.json @@ -0,0 +1,159 @@ +{ + "swagger" : "2.x", + "host" : "localhost:8080", + "basePath" : "/api", + "schemes" : [ "http" ], + "paths" : { + "/complexRequest" : { + "post" : { + "consumes" : [ "application/json" ], + "produces" : [ "text/plain" ], + "parameters" : [ { + "name" : "body", + "schema" : { + "$ref" : "#/definitions/SampleComplexRequestType" + }, + "in" : "body", + "required" : true + } ], + "responses" : { + "200" : { + "description" : "Receives a complex object as parameter", + "schema" : { + "$ref" : "#/definitions/SampleComplexResponseType_InnerClass" + } + } + }, + "operationId" : "verb", + "summary" : "Demo complex request type", + "x-camelContextId" : "camel", + "x-routeId" : "complex request type" + } + } + }, + "definitions" : { + "CustomData" : { + "type" : "object", + "properties" : { + "customDataField" : { + "type" : "string" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.CustomData", + "type" : "string" + } + }, + "SampleComplexRequestType" : { + "required" : [ "mapOfStrings", "requestField1" ], + "type" : "object", + "properties" : { + "data" : { + "$ref" : "#/definitions/CustomData" + }, + "listOfData" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/CustomData" + } + }, + "listOfListOfData" : { + "type" : "array", + "items" : { + "type" : "array", + "items" : { + "$ref" : "#/definitions/CustomData" + } + } + }, + "mapOfData" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/definitions/CustomData" + } + }, + "mapOfMapOfData" : { + "type" : "object", + "additionalProperties" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/definitions/CustomData" + } + } + }, + "requestField1" : { + "type" : "string" + }, + "requestField2" : { + "type" : "string" + }, + "listOfStrings" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "arrayOfString" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "mapOfStrings" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "timeUnit" : { + "enum" : [ "NANOSECONDS", "MICROSECONDS", "MILLISECONDS", "SECONDS", "MINUTES", "HOURS", "DAYS" ], + "type" : "string" + }, + "innerClass" : { + "$ref" : "#/definitions/SampleComplexRequestType_InnerClass" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType", + "type" : "string" + } + }, + "SampleComplexRequestType_InnerClass" : { + "type" : "object", + "properties" : { + "longField" : { + "format" : "int64", + "type" : "integer" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType$InnerClass", + "type" : "string" + } + }, + "SampleComplexResponseType_InnerClass" : { + "type" : "object", + "properties" : { + "doubleField" : { + "format" : "double", + "type" : "number" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType$InnerClass", + "type" : "string" + } + } + }, + "securityDefinitions" : { + "global" : { + "flow" : "accessCode", + "authorizationUrl" : "https://AUTHORIZATION_URL", + "tokenUrl" : "https://TOKEN_URL", + "scopes" : { + "groups" : "Required scopes for Camel REST APIs" + }, + "type" : "oauth2" + } + } +} \ No newline at end of file diff --git a/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesResponse.json b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesResponse.json new file mode 100644 index 0000000..752686d --- /dev/null +++ b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V2SchemaForComplexTypesResponse.json @@ -0,0 +1,102 @@ +{ + "swagger" : "2.x", + "host" : "localhost:8080", + "basePath" : "/api", + "schemes" : [ "http" ], + "paths" : { + "/complexResponse" : { + "get" : { + "consumes" : [ "application/json" ], + "produces" : [ "application/json" ], + "parameters" : [ { + "name" : "body", + "schema" : { + "$ref" : "#/definitions/SampleComplexRequestType_InnerClass" + }, + "in" : "body", + "required" : true + } ], + "responses" : { + "200" : { + "description" : "Returns a complex object", + "schema" : { + "$ref" : "#/definitions/SampleComplexResponseType" + } + } + }, + "operationId" : "verb", + "summary" : "Demo complex response type", + "x-camelContextId" : "camel", + "x-routeId" : "complex response type" + } + } + }, + "definitions" : { + "SampleComplexRequestType_InnerClass" : { + "type" : "object", + "properties" : { + "longField" : { + "format" : "int64", + "type" : "integer" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType$InnerClass", + "type" : "string" + } + }, + "SampleComplexResponseType" : { + "required" : [ "arrayOfStrings", "responseField1" ], + "type" : "object", + "properties" : { + "responseField1" : { + "type" : "string" + }, + "responseField2" : { + "type" : "string" + }, + "arrayOfStrings" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "month" : { + "enum" : [ "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" ], + "type" : "string" + }, + "innerClass" : { + "$ref" : "#/definitions/SampleComplexResponseType_InnerClass" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType", + "type" : "string" + } + }, + "SampleComplexResponseType_InnerClass" : { + "type" : "object", + "properties" : { + "doubleField" : { + "format" : "double", + "type" : "number" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType$InnerClass", + "type" : "string" + } + } + }, + "securityDefinitions" : { + "global" : { + "flow" : "accessCode", + "authorizationUrl" : "https://AUTHORIZATION_URL", + "tokenUrl" : "https://TOKEN_URL", + "scopes" : { + "groups" : "Required scopes for Camel REST APIs" + }, + "type" : "oauth2" + } + } +} \ No newline at end of file diff --git a/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesRequest.json b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesRequest.json new file mode 100644 index 0000000..873d791 --- /dev/null +++ b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesRequest.json @@ -0,0 +1,169 @@ +{ + "openapi" : "3.x", + "servers" : [ { + "url" : "http://localhost:8080/api" + } ], + "paths" : { + "/complexRequest" : { + "post" : { + "requestBody" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SampleComplexRequestType" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "$ref" : "#/components/schemas/SampleComplexResponseType_InnerClass" + } + } + }, + "description" : "Receives a complex object as parameter" + } + }, + "operationId" : "verb", + "summary" : "Demo complex request type", + "x-camelContextId" : "camel", + "x-routeId" : "complex request type" + } + } + }, + "components" : { + "schemas" : { + "CustomData" : { + "type" : "object", + "properties" : { + "customDataField" : { + "type" : "string" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.CustomData", + "type" : "string" + } + }, + "SampleComplexRequestType" : { + "required" : [ "mapOfStrings", "requestField1" ], + "type" : "object", + "properties" : { + "data" : { + "$ref" : "#/components/schemas/CustomData" + }, + "listOfData" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/CustomData" + } + }, + "listOfListOfData" : { + "type" : "array", + "items" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/CustomData" + } + } + }, + "mapOfData" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/CustomData" + } + }, + "mapOfMapOfData" : { + "type" : "object", + "additionalProperties" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/CustomData" + } + } + }, + "requestField1" : { + "type" : "string" + }, + "requestField2" : { + "type" : "string" + }, + "listOfStrings" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "arrayOfString" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "mapOfStrings" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "timeUnit" : { + "enum" : [ "NANOSECONDS", "MICROSECONDS", "MILLISECONDS", "SECONDS", "MINUTES", "HOURS", "DAYS" ], + "type" : "string" + }, + "innerClass" : { + "$ref" : "#/components/schemas/SampleComplexRequestType_InnerClass" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType", + "type" : "string" + } + }, + "SampleComplexRequestType_InnerClass" : { + "type" : "object", + "properties" : { + "longField" : { + "format" : "int64", + "type" : "integer" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType$InnerClass", + "type" : "string" + } + }, + "SampleComplexResponseType_InnerClass" : { + "type" : "object", + "properties" : { + "doubleField" : { + "format" : "double", + "type" : "number" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType$InnerClass", + "type" : "string" + } + } + }, + "securitySchemes" : { + "global" : { + "flows" : { + "authorizationCode" : { + "authorizationUrl" : "https://AUTHORIZATION_URL", + "tokenUrl" : "https://TOKEN_URL", + "scopes" : { + "groups" : "Required scopes for Camel REST APIs" + } + } + }, + "type" : "oauth2" + } + } + } +} \ No newline at end of file diff --git a/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesResponse.json b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesResponse.json new file mode 100644 index 0000000..c4c691e --- /dev/null +++ b/components/camel-openapi-java/src/test/resources/org/apache/camel/openapi/V3SchemaForComplexTypesResponse.json @@ -0,0 +1,112 @@ +{ + "openapi" : "3.x", + "servers" : [ { + "url" : "http://localhost:8080/api" + } ], + "paths" : { + "/complexResponse" : { + "get" : { + "requestBody" : { + "description" : "", + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SampleComplexRequestType_InnerClass" + } + } + }, + "required" : true + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/SampleComplexResponseType" + } + } + }, + "description" : "Returns a complex object" + } + }, + "operationId" : "verb", + "summary" : "Demo complex response type", + "x-camelContextId" : "camel", + "x-routeId" : "complex response type" + } + } + }, + "components" : { + "schemas" : { + "SampleComplexRequestType_InnerClass" : { + "type" : "object", + "properties" : { + "longField" : { + "format" : "int64", + "type" : "integer" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexRequestType$InnerClass", + "type" : "string" + } + }, + "SampleComplexResponseType" : { + "required" : [ "arrayOfStrings", "responseField1" ], + "type" : "object", + "properties" : { + "responseField1" : { + "type" : "string" + }, + "responseField2" : { + "type" : "string" + }, + "arrayOfStrings" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "month" : { + "enum" : [ "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" ], + "type" : "string" + }, + "innerClass" : { + "$ref" : "#/components/schemas/SampleComplexResponseType_InnerClass" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType", + "type" : "string" + } + }, + "SampleComplexResponseType_InnerClass" : { + "type" : "object", + "properties" : { + "doubleField" : { + "format" : "double", + "type" : "number" + } + }, + "x-className" : { + "format" : "org.apache.camel.openapi.model.SampleComplexResponseType$InnerClass", + "type" : "string" + } + } + }, + "securitySchemes" : { + "global" : { + "flows" : { + "authorizationCode" : { + "authorizationUrl" : "https://AUTHORIZATION_URL", + "tokenUrl" : "https://TOKEN_URL", + "scopes" : { + "groups" : "Required scopes for Camel REST APIs" + } + } + }, + "type" : "oauth2" + } + } + } +} \ No newline at end of file