Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support MP OpenAPI 3.0 #3692

Merged
merged 12 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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