Skip to content

Commit

Permalink
Support MP OpenAPI 3.0 (helidon-io#3692)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjquinno authored Dec 9, 2021
1 parent 2433d98 commit 6f53b08
Show file tree
Hide file tree
Showing 23 changed files with 755 additions and 146 deletions.
4 changes: 2 additions & 2 deletions dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
<version.lib.jakarta.validation-api>3.0.0</version.lib.jakarta.validation-api>
<version.lib.jakarta.websockets-api>2.0.0</version.lib.jakarta.websockets-api>
<version.lib.jakarta.xml.bind-api>3.0.1</version.lib.jakarta.xml.bind-api>
<version.lib.jandex>2.3.1.Final</version.lib.jandex>
<version.lib.jandex>2.4.1.Final</version.lib.jandex>
<version.lib.jaxb-core>3.0.2</version.lib.jaxb-core>
<version.lib.jaxb-impl>3.0.2</version.lib.jaxb-impl>
<version.lib.jboss.classfilewriter>1.2.5.Final</version.lib.jboss.classfilewriter>
Expand Down Expand Up @@ -137,7 +137,7 @@
<version.lib.postgresql>42.2.18</version.lib.postgresql>
<version.lib.prometheus>0.9.0</version.lib.prometheus>
<version.lib.slf4j>1.7.32</version.lib.slf4j>
<version.lib.smallrye-openapi>2.1.15</version.lib.smallrye-openapi>
<version.lib.smallrye-openapi>2.1.16</version.lib.smallrye-openapi>
<version.lib.snakeyaml>1.27</version.lib.snakeyaml>
<version.lib.typesafe-config>1.4.1</version.lib.typesafe-config>
<version.lib.tyrus>2.0.1</version.lib.tyrus>
Expand Down
6 changes: 3 additions & 3 deletions docs/mp/openapi/01_openapi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ link:{mp-openapi-spec}#configuration[configuration section] of the MicroProfile
OpenAPI spec.
== Accessing the OpenAPI document
Now your Helidon MP application will automatially respond to an additional endpoint --
Now your Helidon MP application will automatically respond to an additional endpoint --
`/openapi` -- and it will return the OpenAPI document describing the endpoints
in your application.
Expand All @@ -187,5 +187,5 @@ There is not yet an adopted IANA YAML media type, but a proposed one specificall
for OpenAPI documents that has some support is `application/vnd.oai.openapi`.
That is what Helidon returns, by default.
A client can specify `Accept:` as either `application/vnd.oai.openapi+json` or `application/json`
to request JSON.
In addition a client can specify the HTTP header `Accept:` as either `application/vnd.oai.openapi+json` or `application/json`
to request JSON. Alternatively, the client can pass the query parameter `format` as either `JSON` or `YAML` to receive `application/json` or `application/vnd.oai.openapi` (YAML) output, respectively.
5 changes: 3 additions & 2 deletions docs/se/openapi/01_openapi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ those described in the MicroProfile OpenAPI spec, two of which were mentioned ab
servers for given paths
|`openapi.servers.operation` |Prefix for config properties specifying alternative
servers for given operations
|`openapi.schema` |Prefix for config properties defining the schema for a class
|===
For more information on what these settings do consult the MicroProfile OpenAPI spec.
Expand Down Expand Up @@ -182,5 +183,5 @@ There is not yet an adopted IANA YAML media type, but a proposed one specificall
for OpenAPI documents that has some support is `application/vnd.oai.openapi`.
That is what Helidon returns, by default.
In addition a client can specify `Accept:` as either `application/vnd.oai.openapi+json` or `application/json`
to request JSON.
In addition a client can specify the HTTP header `Accept:` as either `application/vnd.oai.openapi+json` or `application/json`
to request JSON. Alternatively, the client can pass the query parameter `format` as either `JSON` or `YAML` to receive `application/json` or `application/vnd.oai.openapi` (YAML) output, respectively.
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled("3.0.0-JAKARTA") // OpenAPI: Caused by: java.lang.NoSuchMethodError:
// 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()'
class MainTest {
private static Server server;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled("3.0.0-JAKARTA") // OpenAPI: Caused by: java.lang.NoSuchMethodError:
// 'java.util.List org.jboss.jandex.ClassInfo.unsortedFields()'
class MainTest {
private static Server server;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@ public final class MPOpenAPIBuilder extends OpenAPISupport.Builder<MPOpenAPIBuil

private Config mpConfig;

protected MPOpenAPIBuilder() {
super(MPOpenAPIBuilder.class);
}

@Override
public OpenApiConfig openAPIConfig() {
return openAPIConfig;
Expand Down
23 changes: 18 additions & 5 deletions microprofile/tests/tck/tck-openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@
<artifactId>tck-openapi</artifactId>
<name>Helidon Microprofile Tests TCK OpenAPI</name>

<properties>
<!-- 3.0.0-JAKARTA -->
<skipTests>true</skipTests>
</properties>

<dependencies>
<dependency>
<groupId>io.helidon.microprofile.tests</groupId>
Expand Down Expand Up @@ -72,6 +67,24 @@
<artifactId>jakarta.activation-api</artifactId>
<scope>test</scope>
</dependency>
<!--
RestAssured xml-path, used by the TCK, requires the javax flavor of JAX-B, so add the API and impl.
-->
<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
<scope>test</scope>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
185 changes: 148 additions & 37 deletions openapi/src/main/java/io/helidon/openapi/CustomConstructor.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
package io.helidon.openapi;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand All @@ -40,64 +40,149 @@
import org.yaml.snakeyaml.nodes.Tag;

/**
* Specialized SnakeYAML constructor for modifying {@code Node} objects for OpenAPI types that extend {@code Map} to adjust the
* type of the child nodes of such nodes.
* Specialized SnakeYAML constructor for modifying {@code Node} objects for OpenAPI types needing special attention.
* <p>
* Several MicroProfile OpenAPI interfaces extend {@code Map}. For example, {@code Paths} extends {@code Map
* <String, PathItem>} and {@code SecurityRequirement} extends {@code Map<String, List<String>>}. When SnakeYAML builds the node
* corresponding to one of these types, it correctly creates each child node as a {@code MappingNode} but it assigns those
* child nodes a type of {@code Object} instead of the mapped type -- {@code PathItem} in the example above.
* </p>
* <p>
* This class customizes the preparation of the node tree in these situations by setting the types for the child nodes explicitly
* to the corresponding child type. In OpenAPI 1.1.2 there are two situations, depending on whether the mapped-to type is a
* {@code List} or not.
* </p>
* <p>
* The MicroProfile OpenAPI 2.0 versions of the interfaces no longer use this construct of an interface extending {@code Map}, so
* ideally we can remove this workaround when we adopt 2.0.
* Several MP OpenAPI types resemble maps with strings for keys and various child types as values. Such interfaces
* expose an {@code addX} method, where X is the child type (e.g., {@link Paths} exposes {@link Paths#addPathItem}.
* SnakeYAML parsing, left to itself, would incorrectly attempt to use the string keys as property names in converting OpenAPI
* documents to and from the in-memory POJO model. To prevent that, this custom constructor takes over the job of
* creating these parent instances and populating the children from the SnakeYAML node graph.
* </p>
*/
final class CustomConstructor extends Constructor {

// maps OpenAPI interfaces which extend Map<?, type> to the mapped-to type where that mapped-to type is NOT List
private static final Map<Class<?>, Class<?>> CHILD_MAP_TYPES = new HashMap<>();
// OpenAPI interfaces which resemble Map<?, type>, linked to info used to prepare the type description for that type where
// the mapped-to type is NOT a list. For typing reasons (in ExpandedTypeDescription$MapLikeTypeDescription#create)
// we provide type-specific factory functions as part of the type metadata here where we can specify the actual parent
// and child types.
static final Map<Class<?>, ChildMapType<?, ?>> CHILD_MAP_TYPES = Map.of(
APIResponses.class, new ChildMapType<>(APIResponses.class,
APIResponse.class,
APIResponses::addAPIResponse,
impl -> ExpandedTypeDescription.MapLikeTypeDescription.create(
APIResponses.class,
impl,
APIResponse.class,
APIResponses::addAPIResponse)),
Callback.class, new ChildMapType<>(Callback.class,
PathItem.class,
Callback::addPathItem,
impl -> ExpandedTypeDescription.MapLikeTypeDescription.create(
Callback.class,
impl,
PathItem.class,
Callback::addPathItem)),
Content.class, new ChildMapType<>(Content.class,
MediaType.class,
Content::addMediaType,
impl -> ExpandedTypeDescription.MapLikeTypeDescription.create(
Content.class,
impl,
MediaType.class,
Content::addMediaType)),
Paths.class, new ChildMapType<>(Paths.class,
PathItem.class,
Paths::addPathItem,
impl -> ExpandedTypeDescription.MapLikeTypeDescription.create(
Paths.class,
impl,
PathItem.class,
Paths::addPathItem)));

// maps OpenAPI interfaces which extend Map<?, List<type>> to the type that appears in the list
private static final Map<Class<?>, Class<?>> CHILD_MAP_OF_LIST_TYPES = new HashMap<>();
// OpenAPI interfaces which resemble Map<?, List<type>>, linked to info used to prepare the type description for that type
// where the mapped-to type IS a list.
static final Map<Class<?>, ChildMapListType<?, ?>> CHILD_MAP_OF_LIST_TYPES = Map.of(
SecurityRequirement.class, new ChildMapListType<>(SecurityRequirement.class,
String.class,
SecurityRequirement::addScheme,
SecurityRequirement::addScheme,
SecurityRequirement::addScheme,
impl -> ExpandedTypeDescription.ListMapLikeTypeDescription.create(
SecurityRequirement.class,
impl,
String.class,
SecurityRequirement::addScheme,
SecurityRequirement::addScheme,
SecurityRequirement::addScheme)));

private static final Logger LOGGER = Logger.getLogger(CustomConstructor.class.getName());
/**
* Adds a single named child to the parent.
*
* @param <P> parent type
* @param <C> child type
*/
@FunctionalInterface
interface ChildAdder<P, C> {
Object addChild(P parent, String name, C child);
}

/**
* Adds a list of children to the parent.
*
* @param <P> parent type
* @param <C> child type
*/
@FunctionalInterface
interface ChildListAdder<P, C> {
Object addChildren(P parent, String name, List<C> children);
}

static {
CHILD_MAP_TYPES.put(Paths.class, PathItem.class);
CHILD_MAP_TYPES.put(Callback.class, PathItem.class);
CHILD_MAP_TYPES.put(Content.class, MediaType.class);
CHILD_MAP_TYPES.put(APIResponses.class, APIResponse.class);
/*
TODO 3.0.0-JAKARTA
CHILD_MAP_TYPES.put(ServerVariables.class, ServerVariable.class);
CHILD_MAP_TYPES.put(Scopes.class, String.class);
*/
CHILD_MAP_OF_LIST_TYPES.put(SecurityRequirement.class, String.class);
/**
* Adds a valueless child name to the parent.
*
* @param <P> parent type
*/
@FunctionalInterface
interface ChildNameAdder<P> {
P addChild(P parent, String name);
}

/**
* Type information about a map-resembling interface.
*
* @param <P> parent type
* @param <C> child type
*/
record ChildMapType<P, C>(Class<P> parentType,
Class<C> childType,
ChildAdder<P, C> childAdder,
Function<Class<?>, ExpandedTypeDescription.MapLikeTypeDescription<P, C>> typeDescriptionFactory) { }

/**
* Type information about a map-resembling interface in which a child can have 0, 1, or more values i.e., the child is
* a list).
*
* @param <P> parent type
* @param <C> child type
*/
record ChildMapListType<P, C>(
Class<P> parentType,
Class<C> childType,
ChildAdder<P, C> childAdder,
ChildListAdder<P, C> childListAdder,
ChildNameAdder<P> childNameAdder,
Function<Class<?>, ExpandedTypeDescription.ListMapLikeTypeDescription<P, C>> typeDescriptionFunction) { }

private static final Logger LOGGER = Logger.getLogger(CustomConstructor.class.getName());

CustomConstructor(TypeDescription td) {
super(td);
yamlClassConstructors.put(NodeId.mapping, new ConstructMapping());
}

@Override
protected void constructMapping2ndStep(MappingNode node, Map<Object, Object> mapping) {
Class<?> parentType = node.getType();
if (CHILD_MAP_TYPES.containsKey(parentType)) {
Class<?> childType = CHILD_MAP_TYPES.get(parentType);
Class<?> childType = CHILD_MAP_TYPES.get(parentType).childType;
node.getValue().forEach(tuple -> {
Node valueNode = tuple.getValueNode();
if (valueNode.getType() == Object.class) {
valueNode.setType(childType);
}
});
} else if (CHILD_MAP_OF_LIST_TYPES.containsKey(parentType)) {
Class<?> childType = CHILD_MAP_OF_LIST_TYPES.get(parentType);
Class<?> childType = CHILD_MAP_OF_LIST_TYPES.get(parentType).childType;
node.getValue().forEach(tuple -> {
Node valueNode = tuple.getValueNode();
if (valueNode.getNodeId() == NodeId.sequence) {
Expand Down Expand Up @@ -125,9 +210,35 @@ private void convertIntHttpStatuses(MappingNode node) {
});
if (!numericHttpStatusMarks.isEmpty()) {
LOGGER.log(Level.WARNING,
"Numeric HTTP status value(s) should be quoted. "
+ "Please change the following; unquoted numeric values might be rejected in a future release: {0}",
numericHttpStatusMarks);
"Numeric HTTP status value(s) should be quoted. "
+ "Please change the following; unquoted numeric values might be rejected in a future release: "
+ "{0}",
numericHttpStatusMarks);
}
}

/**
* Override of SnakeYAML logic which constructs an object from a node.
* <p>
* This class makes sure that parent/child relationships that resemble maps are handled correctly and defers to the
* superclass implementation in other cases.
* </p>
*/
class ConstructMapping extends Constructor.ConstructMapping {

@Override
public Object construct(Node node) {
Class<?> parentType = node.getType();
if (CHILD_MAP_TYPES.containsKey(parentType) || CHILD_MAP_OF_LIST_TYPES.containsKey(parentType)) {
// Following is inspired by SnakeYAML Constructor$ConstructMapping#construct.
MappingNode mappingNode = (MappingNode) node;
if (node.isTwoStepsConstruction()) {
return newMap(mappingNode);
} else {
return constructMapping(mappingNode);
}
}
return super.construct(node);
}
}
}
Loading

0 comments on commit 6f53b08

Please sign in to comment.