Skip to content

Commit

Permalink
CAMEL-20825: camel-rest - Contract first for api-doc should include t…
Browse files Browse the repository at this point in the history
…he spec
  • Loading branch information
davsclaus committed Jun 1, 2024
1 parent e8ea022 commit 8eac240
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"apiContextPath": { "index": 10, "kind": "attribute", "displayName": "Api Context Path", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets a leading context-path the REST API will be using. This can be used when using components such as camel-servlet where the deployed web application is deployed using a context-path." },
"apiContextRouteId": { "index": 11, "kind": "attribute", "displayName": "Api Context Route Id", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the route id to use for the route that services the REST API. The route will by default use an auto assigned route id." },
"apiVendorExtension": { "index": 12, "kind": "attribute", "displayName": "Api Vendor Extension", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether vendor extension is enabled in the Rest APIs. If enabled then Camel will include additional information as vendor extension (eg keys starting with x-) such as route ids, class names etc. Not all 3rd party API gateways and tools supports vendor-extensions when importing your API docs." },
"hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." },
"hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." },
"bindingMode": { "index": 14, "kind": "attribute", "displayName": "Binding Mode", "group": "common", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestBindingMode", "enum": [ "off", "auto", "json", "xml", "json_xml" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "off", "description": "Sets the binding mode to use. The default value is off" },
"bindingPackageScan": { "index": 15, "kind": "attribute", "displayName": "Binding Package Scan", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Package name to use as base (offset) for classpath scanning of POJO classes are located when using binding mode is enabled for JSon or XML. Multiple package names can be separated by comma." },
"skipBindingOnErrorCode": { "index": 16, "kind": "attribute", "displayName": "Skip Binding On Error Code", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json \/ xml etc, as success messages otherwise will do." },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18201,6 +18201,7 @@ An optional certificate alias to use. This is useful when the keystore has multi
<xs:enumeration value="allLocalIp"/>
<xs:enumeration value="localIp"/>
<xs:enumeration value="localHostName"/>
<xs:enumeration value="none"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="restBindingMode">
Expand Down
4 changes: 4 additions & 0 deletions components/camel-openapi-java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-core-engine</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-platform-http</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-xml-io</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -61,10 +62,13 @@
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.servers.Server;
import io.swagger.v3.oas.models.tags.Tag;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.camel.CamelContext;
import org.apache.camel.component.platform.http.PlatformHttpComponent;
import org.apache.camel.component.platform.http.spi.PlatformHttpEngine;
import org.apache.camel.model.rest.ApiKeyDefinition;
import org.apache.camel.model.rest.BasicAuthDefinition;
import org.apache.camel.model.rest.BearerTokenDefinition;
Expand All @@ -76,6 +80,7 @@
import org.apache.camel.model.rest.ResponseHeaderDefinition;
import org.apache.camel.model.rest.ResponseMessageDefinition;
import org.apache.camel.model.rest.RestDefinition;
import org.apache.camel.model.rest.RestHostNameResolver;
import org.apache.camel.model.rest.RestPropertyDefinition;
import org.apache.camel.model.rest.RestSecuritiesDefinition;
import org.apache.camel.model.rest.RestSecurityDefinition;
Expand All @@ -84,10 +89,13 @@
import org.apache.camel.spi.ClassResolver;
import org.apache.camel.spi.NodeIdFactory;
import org.apache.camel.spi.Resource;
import org.apache.camel.spi.RestConfiguration;
import org.apache.camel.support.CamelContextHelper;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.RestComponentHelper;
import org.apache.camel.util.FileUtil;
import org.apache.camel.util.HostUtils;
import org.apache.camel.util.IOHelper;
import org.apache.commons.lang3.ClassUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -135,11 +143,12 @@ private static List<String> getValue(CamelContext camelContext, List<String> lis
* @param classResolver class resolver to use @return the openApi model
* @throws ClassNotFoundException is thrown if error loading class
* @throws IOException is thrown if error loading openapi specification
* @throws UnknownHostException is thrown if error resolving local hostname
*/
public OpenAPI read(
CamelContext camelContext, List<RestDefinition> rests, BeanConfig config,
String camelContextId, ClassResolver classResolver)
throws ClassNotFoundException, IOException {
throws ClassNotFoundException, IOException, UnknownHostException {

// contract first, then load the specification as-is and use as response
for (RestDefinition rest : rests) {
Expand All @@ -151,7 +160,42 @@ public OpenAPI read(
IOHelper.close(is);
OpenAPIV3Parser parser = new OpenAPIV3Parser();
SwaggerParseResult out = parser.readContents(data);
return out.getOpenAPI();
OpenAPI answer = out.getOpenAPI();

String host = null;
RestConfiguration restConfig = camelContext.getRestConfiguration();
if (restConfig.getHostNameResolver() != RestConfiguration.RestHostNameResolver.none) {
host = camelContext.getRestConfiguration().getApiHost();
if (host == null || host.isEmpty()) {
String scheme = "http://";
host = RestComponentHelper.resolveRestHostName(host, restConfig);
PlatformHttpComponent http = camelContext.getComponent("platform-http", PlatformHttpComponent.class);
if (http != null) {
int port = http.getEngine().getServerPort();
if (port > 0) {
host = host + ":" + port;
if (port == 443) {
scheme = "https://";
}
}
}
host = scheme + host;
}
}
if (host != null) {
String basePath = RestOpenApiSupport.getBasePathFromOasDocument(answer);
if (basePath == null) {
basePath = "/";
}
if (!basePath.startsWith("/")) {
basePath = "/" + basePath;
}
Server server = new Server();
server.setUrl(host + basePath);
answer.setServers(null);
answer.addServersItem(server);
}
return answer;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ public enum RestBindingMode {
public enum RestHostNameResolver {
allLocalIp,
localIp,
localHostName
localHostName,
none;
}

private String component;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"apiContextPath": { "index": 10, "kind": "attribute", "displayName": "Api Context Path", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets a leading context-path the REST API will be using. This can be used when using components such as camel-servlet where the deployed web application is deployed using a context-path." },
"apiContextRouteId": { "index": 11, "kind": "attribute", "displayName": "Api Context Route Id", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Sets the route id to use for the route that services the REST API. The route will by default use an auto assigned route id." },
"apiVendorExtension": { "index": 12, "kind": "attribute", "displayName": "Api Vendor Extension", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether vendor extension is enabled in the Rest APIs. If enabled then Camel will include additional information as vendor extension (eg keys starting with x-) such as route ids, class names etc. Not all 3rd party API gateways and tools supports vendor-extensions when importing your API docs." },
"hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." },
"hostNameResolver": { "index": 13, "kind": "attribute", "displayName": "Host Name Resolver", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestHostNameResolver", "enum": [ "allLocalIp", "localHostName", "localIp", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "allLocalIp", "description": "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using." },
"bindingMode": { "index": 14, "kind": "attribute", "displayName": "Binding Mode", "group": "common", "required": false, "type": "enum", "javaType": "org.apache.camel.model.rest.RestBindingMode", "enum": [ "off", "auto", "json", "xml", "json_xml" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "off", "description": "Sets the binding mode to use. The default value is off" },
"bindingPackageScan": { "index": 15, "kind": "attribute", "displayName": "Binding Package Scan", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Package name to use as base (offset) for classpath scanning of POJO classes are located when using binding mode is enabled for JSon or XML. Multiple package names can be separated by comma." },
"skipBindingOnErrorCode": { "index": 16, "kind": "attribute", "displayName": "Skip Binding On Error Code", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "java.lang.Boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to skip binding on output if there is a custom HTTP error code header. This allows to build custom error messages that do not bind to json \/ xml etc, as success messages otherwise will do." },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum RestHostNameResolver {

allLocalIp,
localIp,
localHostName
localHostName,
none

}
33 changes: 32 additions & 1 deletion docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,42 @@ Here Camel will detect the `schema` part:
}
----

And compute the class name as `Pet` and attempt to disover this class from classpath scanning specified via the `bindingPackageScan` option.
And compute the class name as `Pet` and attempt to discover this class from classpath scanning specified via the `bindingPackageScan` option.

You can source code generate Java POJO classes from an OpenAPI specification via tooling such as the `swagger-codegen-maven-plugin` Maven plugin.
For more details see this https://github.com/apache/camel-spring-boot-examples/tree/main/openapi-contract-first[Spring Boot example].

=== Expose API specification

The OpenAPI specification is by default not exposed on the HTTP endpoint. You can make this happen by setting the rest-configuration as follows:

[source,yaml]
----
- restConfiguration:
apiContextPath: /api-doc
----

Then the specification is accessible on `/api-doc` on the embedded HTTP server, so typically that would be `http://localhost:8080/api-doc`.

In the returned API specification the `server` section has been modified to return the IP of the current server. This can be controlled via:


[source,yaml]
----
- restConfiguration:
apiContextPath: /api-doc
hostNameResolver: localIp
----

And you can turn this off by setting the value to `none` so the server part is taken verbatim from the specification file.

[source,yaml]
----
- restConfiguration:
apiContextPath: /api-doc
hostNameResolver: none
----

== Examples

You can find a few examples such as:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13913,7 +13913,7 @@ protected boolean setProperty(RestBindingDefinition target, String propertyKey,
@YamlProperty(name = "enableNoContentResponse", type = "boolean", description = "Whether to return HTTP 204 with an empty body when a response contains an empty JSON object or XML root object. The default value is false.", displayName = "Enable No Content Response"),
@YamlProperty(name = "endpointProperty", type = "array:org.apache.camel.model.rest.RestPropertyDefinition", description = "Allows to configure as many additional properties for the rest endpoint in use.", displayName = "Endpoint Property"),
@YamlProperty(name = "host", type = "string", description = "The hostname to use for exposing the REST service.", displayName = "Host"),
@YamlProperty(name = "hostNameResolver", type = "enum:allLocalIp,localHostName,localIp", defaultValue = "allLocalIp", description = "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.", displayName = "Host Name Resolver"),
@YamlProperty(name = "hostNameResolver", type = "enum:allLocalIp,localHostName,localIp,none", defaultValue = "allLocalIp", description = "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.", displayName = "Host Name Resolver"),
@YamlProperty(name = "inlineRoutes", type = "boolean", description = "Inline routes in rest-dsl which are linked using direct endpoints. Each service in Rest DSL is an individual route, meaning that you would have at least two routes per service (rest-dsl, and the route linked from rest-dsl). By inlining (default) allows Camel to optimize and inline this as a single route, however this requires to use direct endpoints, which must be unique per service. If a route is not using direct endpoint then the rest-dsl is not inlined, and will become an individual route. This option is default true.", displayName = "Inline Routes"),
@YamlProperty(name = "jsonDataFormat", type = "string", description = "Name of specific json data format to use. By default jackson will be used. Important: This option is only for setting a custom name of the data format, not to refer to an existing data format instance.", displayName = "Json Data Format"),
@YamlProperty(name = "port", type = "string", description = "The port number to use for exposing the REST service. Notice if you use servlet component then the port number configured here does not apply, as the port number in use is the actual port number the servlet component is using. eg if using Apache Tomcat its the tomcat http port, if using Apache Karaf its the HTTP service in Karaf that uses port 8181 by default etc. Though in those situations setting the port number here, allows tooling and JMX to know the port number, so its recommended to set the port number to the number that the servlet engine uses.", displayName = "Port"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15393,7 +15393,7 @@
"title" : "Host Name Resolver",
"description" : "If no hostname has been explicit configured, then this resolver is used to compute the hostname the REST service will be using.",
"default" : "allLocalIp",
"enum" : [ "allLocalIp", "localHostName", "localIp" ]
"enum" : [ "allLocalIp", "localHostName", "localIp", "none" ]
},
"inlineRoutes" : {
"type" : "boolean",
Expand Down

0 comments on commit 8eac240

Please sign in to comment.