diff --git a/bom/pom.xml b/bom/pom.xml
index 88ddd25551d..9baf6465362 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -910,6 +910,11 @@
helidon-openapi${helidon.version}
+
+ io.helidon.openapi
+ helidon-openapi-ui
+ ${helidon.version}
+ io.helidon.microprofile.openapihelidon-microprofile-openapi
diff --git a/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/MapMatcher.java b/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/MapMatcher.java
new file mode 100644
index 00000000000..0358078ae48
--- /dev/null
+++ b/common/testing/junit5/src/main/java/io/helidon/common/testing/junit5/MapMatcher.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.common.testing.junit5;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+/**
+ * Hamcrest matchers for {@link java.util.Map}.
+ */
+public final class MapMatcher {
+ private MapMatcher() {
+ }
+
+ /**
+ * A matcher that performs {@link java.util.Map} deep equality.
+ *
+ *
+ * This method targets trees implemented using {@link java.util.Map} where values of type {@link java.util.Map}
+ * are considered tree nodes, and values of any other type are considered leaf nodes.
+ *
+ * The deep-equality is performed by diffing a flat string representation of each map. If the diff yields no differences,
+ * the maps are considered deeply equal.
+ *
+ * The entries are compared using strings, both keys and leaf nodes must implement {@link Object#toString()}.
+ *
+ * @param expected expected map
+ * @param type of the map keys
+ * @param type of the map values
+ * @return matcher validating the {@link java.util.Map} is deeply equal
+ */
+ public static Matcher
-
io.helidon.common.featureshelidon-common-features-api
@@ -136,6 +135,11 @@
hamcrest-alltest
+
+ io.helidon.common.testing
+ helidon-common-testing-junit5
+ test
+
@@ -196,8 +200,50 @@
helidon-common-features-processor${helidon.version}
+
+ io.helidon.config
+ helidon-config-metadata-processor
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-processor
+ ${helidon.version}
+
+
+ io.helidon.common.processor
+ helidon-common-processor-helidon-copyright
+ ${helidon.version}
+
+
+
+ io.helidon.common.features
+ helidon-common-features-processor
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-processor
+ ${helidon.version}
+
+
+ io.helidon.config
+ helidon-config-metadata-processor
+ ${helidon.version}
+
+
+ io.helidon.inject.configdriven
+ helidon-inject-configdriven-processor
+ ${helidon.version}
+
+
+ io.helidon.common.processor
+ helidon-common-processor-helidon-copyright
+ ${helidon.version}
+
+ org.apache.maven.plugins
@@ -246,6 +292,7 @@
generate-sources
+ ${project.build.directory}/generated-sources/annotationsio.helidon.microprofile.openapi.SnakeYAMLParserHelper${openapi-interfaces-dir}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java
index c985b7fc131..f0304ee8790 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ExpandedTypeDescription.java
@@ -18,6 +18,7 @@
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@@ -55,12 +56,12 @@
* Some of the MP OpenAPI items are extensible, meaning they accept sub-item keys with the
* "x-" prefix. This class supports extensions. For scalars it delegates to the normal
* SnakeYAML processing to correctly type and parse the scalar. For sequences it
- * creates {@code List}s. For mappings it creates {@code Map}s. The subnodes of the lists and
+ * creates {@code List}s. For mappings it creates {@code Map}s. The sub-nodes of the lists and
* maps are handled by the normal SnakeYAML parsing, so the resulting elements in lists and
* maps are of the SnakeYAML-inferred types.
*
*
- * A subnode {@code $ref} maps to the {@code ref} property on the MP OpenAPI types. This type
+ * A sub-node {@code $ref} maps to the {@code ref} property on the MP OpenAPI types. This type
* description simplifies defining the {@code $ref} property to those types that support it.
*
*
@@ -158,9 +159,9 @@ public Object newInstance(String propertyName, Node node) {
Property p = getProperty(propertyName);
if (p.getType().isEnum()) {
@SuppressWarnings("unchecked")
- Class eClass = (Class) p.getType();
- String valueText = ScalarNode.class.cast(node).getValue();
- for (Enum e : eClass.getEnumConstants()) {
+ Class> eClass = (Class>) p.getType();
+ String valueText = ((ScalarNode) node).getValue();
+ for (Enum> e : eClass.getEnumConstants()) {
if (e.toString().equals(valueText)) {
return e;
}
@@ -178,9 +179,7 @@ public void addExcludes(String... propNames) {
if (excludes == Collections.emptySet()) {
excludes = new HashSet<>();
}
- for (String propName : propNames) {
- excludes.add(propName);
- }
+ excludes.addAll(Arrays.asList(propNames));
}
/**
@@ -198,13 +197,17 @@ public Class> impl() {
* @return {@code true} if default value property is defined
*/
public boolean hasDefaultProperty() {
- return getPropertyNoEx("defaultValue") != null;
+ return defaultProperty() != null;
}
- Property getPropertyNoEx(String name) {
+ /**
+ * Returns the default property for the type.
+ *
+ * @return the 'default' property for this type; null if none
+ */
+ Property defaultProperty() {
try {
- Property p = getProperty("defaultValue");
- return p;
+ return getProperty("defaultValue");
} catch (YAMLException ex) {
if (ex.getMessage().startsWith("Unable to find property")) {
return null;
@@ -213,38 +216,22 @@ Property getPropertyNoEx(String name) {
}
}
- /**
- * Returns the default property for the type.
- *
- * @return the 'default' property for this type; null if none
- */
- Property defaultProperty() {
- return getPropertyNoEx("defaultValue");
- }
-
private static boolean setupExtensionType(String key, Node valueNode) {
if (isExtension(key)) {
- /*
- * The nodeId in a node is more like node "category" in SnakeYAML. For those OpenAPI interfaces which implement
- * Extensible we need to set the node's type if the extension is a List or Map.
- */
+ // The nodeId in a node is more like node "category" in SnakeYAML. For those OpenAPI interfaces which implement
+ // Extensible we need to set the node's type if the extension is a List or Map.
switch (valueNode.getNodeId()) {
- case sequence:
- valueNode.setType(List.class);
- return true;
-
- case anchor:
- break;
-
- case mapping:
- valueNode.setType(Map.class);
- return true;
-
- case scalar:
- break;
-
- default:
-
+ case sequence -> {
+ valueNode.setType(List.class);
+ return true;
+ }
+ case mapping -> {
+ valueNode.setType(Map.class);
+ return true;
+ }
+ default -> {
+ return false;
+ }
}
}
return false;
@@ -261,13 +248,13 @@ private static boolean isRef(String name) {
/**
* Specific type description for {@code Schema}.
*
- * The {@code Schema} node allows the {@code additionalProperties} subnode to be either
+ * The {@code Schema} node allows the {@code additionalProperties} sub-node to be either
* {@code Boolean} or another {@code Schema}, and the {@code Schema} class exposes getters and setters for
* {@code additionalPropertiesBoolean}, and {@code additionalPropertiesSchema}.
* This type description customizes the handling of {@code additionalProperties} to account for all that.
*
*
- * @see Serializer (specifically doRepresentJavaBeanProperty) for output handling for
+ * @see OpenApiSerializer (specifically doRepresentJavaBeanProperty) for output handling for
* additionalProperties
*/
static final class SchemaTypeDescription extends ExpandedTypeDescription {
@@ -278,8 +265,8 @@ static final class SchemaTypeDescription extends ExpandedTypeDescription {
new MethodProperty(ADDL_PROPS_PROP_DESCRIPTOR) {
@Override
- public void set(Object object, Object value) throws Exception {
- Schema s = Schema.class.cast(object);
+ public void set(Object object, Object value) {
+ Schema s = (Schema) object;
if (value instanceof Schema) {
s.setAdditionalPropertiesSchema((Schema) value);
} else {
@@ -289,7 +276,7 @@ public void set(Object object, Object value) throws Exception {
@Override
public Object get(Object object) {
- Schema s = Schema.class.cast(object);
+ Schema s = (Schema) object;
Boolean b = s.getAdditionalPropertiesBoolean();
return b != null ? b : s.getAdditionalPropertiesSchema();
}
@@ -394,7 +381,7 @@ static
parentType,
@Override
@SuppressWarnings("unchecked")
- public boolean setProperty(Object targetBean, String propertyName, Object value) throws Exception {
+ public boolean setProperty(Object targetBean, String propertyName, Object value) {
P parent = parentType().cast(targetBean);
if (value == null) {
childNameAdder.addChild(parent, propertyName);
@@ -455,11 +442,11 @@ public boolean setProperty(Object targetBean, String propertyName, Object value)
}
/**
- * Property description for an extension subnode.
+ * Property description for an extension sub-node.
*/
static class ExtensionProperty extends Property {
- private static final Class[] EXTENSION_TYPE_ARGS = new Class[0];
+ private static final Class>[] EXTENSION_TYPE_ARGS = new Class>[0];
ExtensionProperty(String name) {
super(name, Object.class);
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilder.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilder.java
new file mode 100644
index 00000000000..8da7600ab4f
--- /dev/null
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilder.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger.Level;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import io.helidon.microprofile.server.JaxRsApplication;
+
+import io.smallrye.openapi.api.OpenApiConfigImpl;
+import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.core.Application;
+import jakarta.ws.rs.core.Feature;
+import jakarta.ws.rs.ext.Provider;
+import org.eclipse.microprofile.config.Config;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.CompositeIndex;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexReader;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.Indexer;
+
+/**
+ * Utility that computes the list of filtered index views, one for each JAX-RS application,
+ * sorted by the Application class name to help keep the list of endpoints in the OpenAPI document in a stable order.
+ */
+class FilteredIndexViewsBuilder {
+
+ private static final System.Logger LOGGER = System.getLogger(FilteredIndexViewsBuilder.class.getName());
+
+ private final Config config;
+ private final FilteredIndexView view;
+ private final List apps;
+ private final Set requiredClasses;
+ private final boolean useJaxRsSemantics;
+
+ FilteredIndexViewsBuilder(Config config,
+ List apps,
+ Set> types,
+ List indexPaths,
+ boolean useJaxRsSemantics) {
+
+ this.config = config;
+ this.view = new FilteredIndexView(indexView(indexPaths, apps, types), new OpenApiConfigImpl(config));
+ this.apps = apps;
+ this.requiredClasses = requiredClassNames(view);
+ this.useJaxRsSemantics = useJaxRsSemantics;
+ }
+
+ /**
+ * Creates a {@link FilteredIndexView} tailored to each JAX-RS application.
+ *
+ * @return the list of filtered index views
+ */
+ List buildViews() {
+ return apps.stream()
+ .filter(app -> app.applicationClass().isPresent())
+ .sorted(Comparator.comparing(app -> app.applicationClass().get().getName()))
+ .map(this::map)
+ .toList();
+ }
+
+ private FilteredIndexView map(JaxRsApplication app) {
+
+ Application application = app.resourceConfig().getApplication();
+
+ @SuppressWarnings("deprecation")
+ Set singletons = application.getSingletons()
+ .stream()
+ .map(Object::getClass)
+ .map(Class::getName)
+ .collect(Collectors.toSet());
+
+ Set classes = application.getClasses()
+ .stream()
+ .map(Class::getName)
+ .collect(Collectors.toSet());
+
+ String appClassName = className(app);
+
+ Set explicitClassNames = new HashSet<>(classes);
+ explicitClassNames.addAll(singletons);
+
+ if (explicitClassNames.isEmpty() && apps.size() == 1) {
+ // No need to do filtering at all.
+ if (LOGGER.isLoggable(Level.TRACE)) {
+ LOGGER.log(Level.TRACE, String.format(
+ "No filtering required for %s which reports no explicitly referenced classes and "
+ + "is the only JAX-RS application",
+ appClassName));
+ }
+ return view;
+ }
+
+ // Note that the MP OpenAPI TCK does not follow JAX-RS behavior wen getSingletons returns a non-empty set.
+ // The TCK incorrectly expects the endpoints defined by other resources as well to appear in the OpenAPI document.
+ if ((classes.isEmpty() && (singletons.isEmpty() || !useJaxRsSemantics)) && apps.size() == 1) {
+ if (LOGGER.isLoggable(Level.TRACE)) {
+ LOGGER.log(Level.TRACE, String.format(
+ "No filtering required for %s because JAX-RS semantics is disabled",
+ appClassName));
+ }
+ // Perform no further filtering if all the following conditions are met:
+ // - there is exactly one application,
+ // - we found no classes from getClasses
+ // - we found no classes from getSingletons or the JAX-RS semantic is disabled.
+ return view;
+ }
+
+ Set excludedClasses = excludedClasses(app, explicitClassNames);
+ FilteringOpenApiConfigImpl filteringOpenApiConfig = new FilteringOpenApiConfigImpl(config, excludedClasses);
+
+ // Create a new filtered index view for this application which excludes the irrelevant classes we just identified.
+ // Its delegate is the previously-created view based only on the MP configuration.
+ FilteredIndexView result = new FilteredIndexView(view, filteringOpenApiConfig);
+
+ if (LOGGER.isLoggable(Level.TRACE)) {
+ LOGGER.log(Level.TRACE, String.format(
+ "FilteredIndexView for %n"
+ + " application class %s%n"
+ + " with explicitly-referenced classes %s%n"
+ + " yields exclude list: %s%n and known classes: %n %s",
+ appClassName,
+ explicitClassNames,
+ excludedClasses,
+ String.join("," + System.lineSeparator() + " ", knownClassNames(result))));
+ }
+
+ return result;
+ }
+
+ private Set excludedClasses(JaxRsApplication app, Set explicitClasses) {
+
+ String appClass = className(app);
+
+ // Start with all other JAX-RS app names.
+ Set result = apps.stream()
+ .map(FilteredIndexViewsBuilder::className)
+ .filter(name -> !name.equals("") && !name.equals(appClass))
+ .collect(Collectors.toSet());
+
+ if (!explicitClasses.isEmpty()) {
+ // This class identified resource, provider, or feature classes it uses.
+ // Ignore all ancillary classes that this app does not explicitly reference.
+ result.addAll(requiredClasses);
+ result.removeAll(explicitClasses);
+ }
+
+ return result;
+ }
+
+ private static String className(JaxRsApplication app) {
+ return app.applicationClass().map(Class::getName).orElse("");
+ }
+
+ private static Set requiredClassNames(IndexView indexView) {
+ Set result = new HashSet<>(annotatedClassNames(indexView, Path.class));
+ result.addAll(annotatedClassNames(indexView, Provider.class));
+ result.addAll(annotatedClassNames(indexView, Feature.class));
+ if (LOGGER.isLoggable(Level.DEBUG)) {
+ LOGGER.log(Level.DEBUG, "Ancillary classes: {0}", result);
+ }
+ return result;
+ }
+
+ private static Set annotatedClassNames(IndexView indexView, Class> annotationClass) {
+ return indexView
+ .getAnnotations(DotName.createSimple(annotationClass.getName()))
+ .stream()
+ .map(AnnotationInstance::target)
+ .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS)
+ .map(AnnotationTarget::asClass)
+ .filter(classInfo -> hasImplementationOrIsIncluded(indexView, classInfo))
+ .map(ClassInfo::toString)
+ .collect(Collectors.toSet());
+ }
+
+ private static boolean hasImplementationOrIsIncluded(IndexView indexView, ClassInfo classInfo) {
+ if (!Modifier.isInterface(classInfo.flags())) {
+ return true;
+ }
+ return indexView.getAllKnownImplementors(classInfo.name()).stream()
+ .anyMatch(info -> !Modifier.isAbstract(info.flags()));
+ }
+
+ private static List knownClassNames(FilteredIndexView filteredIndexView) {
+ return filteredIndexView
+ .getKnownClasses()
+ .stream()
+ .map(ClassInfo::toString)
+ .sorted()
+ .toList();
+ }
+
+ private static IndexView indexView(List indexPaths, List apps, Set> types) {
+ try {
+ List urls = findIndexFiles(indexPaths);
+ if (urls.isEmpty()) {
+ LOGGER.log(Level.INFO, """
+ Could not locate the Jandex index file META-INF/jandex.idx, building an in-memory index...
+ Consider using the Jandex maven plug-in during your build to add it to your app.""");
+ return buildIndex(apps, types);
+ }
+ return loadIndex(indexPaths);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private static IndexView loadIndex(List indexPaths) throws IOException {
+ List indices = new ArrayList<>();
+ for (URL url : findIndexFiles(indexPaths)) {
+ try (InputStream is = url.openStream()) {
+ LOGGER.log(Level.TRACE, "Adding Jandex index at {0}", url.toString());
+ indices.add(new IndexReader(is).read());
+ } catch (Exception ex) {
+ throw new IOException(String.format(
+ "Attempted to read from previously-located index file %s but the index cannot be read",
+ url), ex);
+ }
+ }
+ return indices.size() == 1 ? indices.get(0) : CompositeIndex.create(indices);
+ }
+
+ private static IndexView buildIndex(List apps, Set> types) throws IOException {
+ Indexer indexer = new Indexer();
+ for (Class> c : types) {
+ indexClass(indexer, c);
+ }
+
+ // Some apps might be added dynamically, not via annotation processing.
+ // Add those classes to the index if they are not already present.
+ apps.stream()
+ .map(JaxRsApplication::applicationClass)
+ .flatMap(Optional::stream)
+ .forEach(cls -> indexClass(indexer, cls));
+
+ LOGGER.log(Level.TRACE, "Using internal Jandex index created from CDI bean discovery");
+ Index result = indexer.complete();
+ dumpIndex(result);
+ return result;
+ }
+
+ private static void indexClass(Indexer indexer, Class> c) {
+ try {
+ indexer.indexClass(c);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(
+ String.format("Cannot load bytecode from class %s for annotation processing", c),
+ ex);
+ }
+ }
+
+ private static void dumpIndex(Index index) {
+ if (LOGGER.isLoggable(Level.DEBUG)) {
+ LOGGER.log(Level.DEBUG, "Dump of internal Jandex index:");
+ PrintStream oldStdout = System.out;
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (PrintStream newPS = new PrintStream(baos, true, Charset.defaultCharset())) {
+ System.setOut(newPS);
+ index.printAnnotations();
+ index.printSubclasses();
+ LOGGER.log(Level.DEBUG, baos.toString(Charset.defaultCharset()));
+ } finally {
+ System.setOut(oldStdout);
+ }
+ }
+ }
+
+ private static List findIndexFiles(List paths) {
+ List result = new ArrayList<>();
+ for (String path : paths) {
+ Enumeration urls;
+ try {
+ urls = Thread.currentThread().getContextClassLoader().getResources(path);
+ while (urls.hasMoreElements()) {
+ result.add(urls.nextElement());
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ return result;
+ }
+
+ private static class FilteringOpenApiConfigImpl extends OpenApiConfigImpl {
+
+ private final Set classesToExclude;
+
+ FilteringOpenApiConfigImpl(org.eclipse.microprofile.config.Config config, Set classesToExclude) {
+ super(config);
+ this.classesToExclude = classesToExclude;
+ }
+
+ @Override
+ public Set scanExcludeClasses() {
+ return classesToExclude;
+ }
+ }
+}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/JsonpAnnotationScannerExtension.java
similarity index 71%
rename from microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java
rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/JsonpAnnotationScannerExtension.java
index 62cc0c33290..802aee62af6 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/HelidonAnnotationScannerExtension.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/JsonpAnnotationScannerExtension.java
@@ -34,12 +34,10 @@
/**
* Extension we want SmallRye's OpenAPI implementation to use for parsing the JSON content in Extension annotations.
*/
-class HelidonAnnotationScannerExtension implements AnnotationScannerExtension {
-
- private static final System.Logger LOGGER = System.getLogger(HelidonAnnotationScannerExtension.class.getName());
+class JsonpAnnotationScannerExtension implements AnnotationScannerExtension {
+ private static final System.Logger LOGGER = System.getLogger(JsonpAnnotationScannerExtension.class.getName());
private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Collections.emptyMap());
-
private static final Representer MISSING_FIELD_TOLERANT_REPRESENTER;
static {
@@ -74,27 +72,27 @@ public Object parseValue(String value) {
// See if we should parse the value fully.
switch (value.charAt(0)) {
- case '{', '[', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
- try {
- JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(value));
- JsonValue jsonValue = reader.readValue();
- // readValue will truncate the input to convert to a number if it can. Make sure the value is the same length
- // as the original.
- if (jsonValue.getValueType().equals(JsonValue.ValueType.NUMBER)
- && value.length() != jsonValue.toString().length()) {
- return value;
+ case '{', '[', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> {
+ try {
+ JsonReader reader = JSON_READER_FACTORY.createReader(new StringReader(value));
+ JsonValue jsonValue = reader.readValue();
+ // readValue will truncate the input to convert to a number if it can. Make sure the value is the same length
+ // as the original.
+ if (jsonValue.getValueType().equals(JsonValue.ValueType.NUMBER)
+ && value.length() != jsonValue.toString().length()) {
+ return value;
+ }
+
+ return convertJsonValue(jsonValue);
+ } catch (Exception ex) {
+ LOGGER.log(System.Logger.Level.ERROR,
+ String.format("Error parsing JSON value: %s", value),
+ ex);
+ throw ex;
}
-
- return convertJsonValue(jsonValue);
- } catch (Exception ex) {
- LOGGER.log(System.Logger.Level.ERROR,
- String.format("Error parsing JSON value: %s", value),
- ex);
- throw ex;
}
- }
- default -> {
- }
+ default -> {
+ }
}
// Treat as JSON string.
@@ -103,7 +101,7 @@ public Object parseValue(String value) {
@Override
public Schema parseSchema(String jsonSchema) {
- return OpenApiParser.parse(MpOpenApiFeature.PARSER_HELPER.get().types(),
+ return OpenApiParser.parse(OpenApiHelper.types(),
Schema.class,
new StringReader(jsonSchema),
MISSING_FIELD_TOLERANT_REPRESENTER);
@@ -113,13 +111,13 @@ private static Object convertJsonValue(JsonValue jsonValue) {
return switch (jsonValue.getValueType()) {
case ARRAY -> jsonValue.asJsonArray()
.stream()
- .map(HelidonAnnotationScannerExtension::convertJsonValue)
- .collect(Collectors.toList());
+ .map(JsonpAnnotationScannerExtension::convertJsonValue)
+ .toList();
case FALSE -> Boolean.FALSE;
case TRUE -> Boolean.TRUE;
case NULL -> null;
- case STRING -> JsonString.class.cast(jsonValue).getString();
- case NUMBER -> JsonNumber.class.cast(jsonValue).numberValue();
+ case STRING -> ((JsonString) jsonValue).getString();
+ case NUMBER -> ((JsonNumber) jsonValue).numberValue();
case OBJECT -> jsonValue.asJsonObject()
.entrySet()
.stream()
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java
deleted file mode 100644
index cfd41d7b7f1..00000000000
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MPOpenAPIBuilder.java
+++ /dev/null
@@ -1,487 +0,0 @@
-/*
- * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
- *
- * Licensed 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 io.helidon.microprofile.openapi;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintStream;
-import java.lang.System.Logger.Level;
-import java.lang.reflect.Modifier;
-import java.net.URL;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.Enumeration;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import io.helidon.config.Config;
-import io.helidon.microprofile.server.JaxRsApplication;
-import io.helidon.openapi.OpenApiFeature;
-
-import io.smallrye.openapi.api.OpenApiConfig;
-import io.smallrye.openapi.api.OpenApiConfigImpl;
-import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
-import jakarta.enterprise.inject.spi.CDI;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.core.Application;
-import jakarta.ws.rs.core.Feature;
-import jakarta.ws.rs.ext.Provider;
-import org.jboss.jandex.AnnotationInstance;
-import org.jboss.jandex.AnnotationTarget;
-import org.jboss.jandex.ClassInfo;
-import org.jboss.jandex.CompositeIndex;
-import org.jboss.jandex.DotName;
-import org.jboss.jandex.Index;
-import org.jboss.jandex.IndexReader;
-import org.jboss.jandex.IndexView;
-import org.jboss.jandex.Indexer;
-
-/**
- * Builder for the MP OpenAPI feature.
- */
-class MPOpenAPIBuilder extends OpenApiFeature.Builder {
-
- private static final System.Logger LOGGER = System.getLogger(MPOpenAPIBuilder.class.getName());
-
- // This is the prefix users will use in the config file.
- static final String MP_OPENAPI_CONFIG_PREFIX = "mp." + OpenApiFeature.Builder.CONFIG_KEY;
-
- private static final String USE_JAXRS_SEMANTICS_CONFIG_KEY = "use-jaxrs-semantics";
-
- private static final String USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY =
- "mp.openapi.extensions.helidon." + USE_JAXRS_SEMANTICS_CONFIG_KEY;
- private static final boolean USE_JAXRS_SEMANTICS_DEFAULT = true;
-
- private OpenApiConfig openApiConfig;
- private org.eclipse.microprofile.config.Config mpConfig;
-
- private String[] indexPaths;
- private int indexURLCount;
-
- private boolean useJaxRsSemantics = USE_JAXRS_SEMANTICS_DEFAULT;
-
- MPOpenAPIBuilder() {
- super();
- }
-
- @Override
- public MpOpenApiFeature build() {
- List indexURLs = findIndexFiles(indexPaths);
- indexURLCount = indexURLs.size();
- if (indexURLs.isEmpty()) {
- LOGGER.log(Level.INFO, String.format("""
- OpenAPI feature could not locate the Jandex index file %s so will build an in-memory index.
- This slows your app start-up and, depending on CDI configuration, might omit some type information \
- needed for a complete OpenAPI document.
- Consider using the Jandex maven plug-in during your build to create the index and add it to your app.""",
- OpenApiCdiExtension.INDEX_PATH));
- }
- if (openApiConfig == null) {
- openApiConfig = new OpenApiConfigImpl(mpConfig);
- }
- return new MpOpenApiFeature(this);
- }
-
- @Override
- public MPOpenAPIBuilder config(Config config) {
- super.config(config);
- return identity();
- }
-
- /**
- * Sets the SmallRye OpenAPI configuration.
- *
- * @param openApiConfig the {@link io.smallrye.openapi.api.OpenApiConfig} settings
- * @return updated builder
- */
- public MPOpenAPIBuilder openApiConfig(OpenApiConfig openApiConfig) {
- this.openApiConfig = openApiConfig;
- return this;
- }
-
- /**
- * Returns an {@link org.jboss.jandex.IndexView} for the Jandex index that describes
- * annotated classes for endpoints.
- *
- * @return {@code IndexView} describing discovered classes
- */
- IndexView indexView() {
- try {
- return indexURLCount > 0 ? existingIndexFileReader() : indexFromHarvestedClasses();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Returns the {@link io.smallrye.openapi.api.OpenApiConfig} instance the builder uses.
- *
- * @return {@code OpenApiConfig} instance in use by the builder
- */
- OpenApiConfig openApiConfig() {
- return openApiConfig;
- }
-
- @Override
- protected System.Logger logger() {
- return LOGGER;
- }
-
- MPOpenAPIBuilder config(org.eclipse.microprofile.config.Config mpConfig) {
- this.mpConfig = mpConfig;
- // use-jaxrs-semantics is intended for Helidon's private use in running the TCKs to work around a problem there.
- // We do not document its use.
- useJaxRsSemantics = mpConfig
- .getOptionalValue(USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY, Boolean.class)
- .orElse(USE_JAXRS_SEMANTICS_DEFAULT);
-
- return openApiConfig(new OpenApiConfigImpl(mpConfig));
- }
-
- MPOpenAPIBuilder indexPaths(String... indexPaths) {
- this.indexPaths = indexPaths;
- return identity();
- }
-
- /**
- * Creates a {@link io.smallrye.openapi.runtime.scanner.FilteredIndexView} tailored to the specified JAX-RS application.
- *
- * Use an {@link io.smallrye.openapi.api.OpenApiConfig} instance which (possibly) limits scanning for this application
- * by excluding classes that are not "relevant" to the specified application. For our purposes, the classes "relevant"
- * to an application are those:
- *
- *
returned by the application's {@code getClasses} method, and
- *
inferred from the objects returned from the application's {@code getSingletons} method.
- *
- *
- * If both methods return empty sets (the default implementation in {@link jakarta.ws.rs.core.Application}), then all
- * resources, providers, and features are considered relevant to the application.
- *
- * In constructing the filtered index view for a JAX-RS application, we also exclude the other JAX-RS application classes.
- *
- *
- * @param viewFilteredByConfig filtered index view based only on MP config
- * @param jaxRsApplications all JAX-RS applications discovered
- * @param jaxRsApp the specific JAX-RS application of interest
- * @param ancillaryClassNames names of resource, provider, and feature classes
- * @return the filtered index view suitable for the specified JAX-RS application
- */
- private FilteredIndexView filteredIndexView(FilteredIndexView viewFilteredByConfig,
- List jaxRsApplications,
- JaxRsApplication jaxRsApp,
- Set ancillaryClassNames) {
- Application app = jaxRsApp.resourceConfig().getApplication();
-
- Set classesFromGetSingletons = app.getSingletons().stream()
- .map(Object::getClass)
- .map(Class::getName)
- .collect(Collectors.toSet());
-
- Set classesFromGetClasses = app.getClasses().stream()
- .map(Class::getName)
- .collect(Collectors.toSet());
-
- String appClassName = toClassName(jaxRsApp);
-
- Set classesExplicitlyReferenced = new HashSet<>(classesFromGetClasses);
- classesExplicitlyReferenced.addAll(classesFromGetSingletons);
-
- if (classesExplicitlyReferenced.isEmpty() && jaxRsApplications.size() == 1) {
- // No need to do filtering at all.
- if (LOGGER.isLoggable(Level.TRACE)) {
- LOGGER.log(Level.TRACE, String.format(
- "No filtering required for %s which reports no explicitly referenced classes and "
- + "is the only JAX-RS application",
- appClassName));
- }
- return viewFilteredByConfig;
- }
-
- // Also, perform no further filtering if there is exactly one application and we found no classes from getClasses and,
- // although we found classes from getSingletons, the useJaxRsSemantics setting has been turned off.
- //
- // Note that the MP OpenAPI TCK does not follow JAX-RS behavior if the application class returns a non-empty set from
- // getSingletons; in that case, the TCK incorrectly expects the endpoints defined by other resources as well to appear
- // in the OpenAPI document.
- if ((
- classesFromGetClasses.isEmpty()
- && (classesFromGetSingletons.isEmpty() || !useJaxRsSemantics))
- && jaxRsApplications.size() == 1) {
- if (LOGGER.isLoggable(Level.TRACE)) {
- LOGGER.log(Level.TRACE, String.format("""
- No filtering required for %s; although it returns a non-empty set from getSingletons, JAX-RS semantics \
- has been turned off for OpenAPI processing using %s""",
- appClassName, MPOpenAPIBuilder.USE_JAXRS_SEMANTICS_FULL_CONFIG_KEY));
- }
- return viewFilteredByConfig;
- }
-
- Set excludedClasses = classNamesToIgnore(jaxRsApplications,
- jaxRsApp,
- ancillaryClassNames,
- classesExplicitlyReferenced);
-
- // Create a new filtered index view for this application which excludes the irrelevant classes we just identified. Its
- // delegate is the previously-created view based only on the MP configuration.
- FilteredIndexView result = new FilteredIndexView(viewFilteredByConfig,
- new FilteringOpenApiConfigImpl(mpConfig, excludedClasses));
- if (LOGGER.isLoggable(Level.TRACE)) {
- String knownClassNames = result
- .getKnownClasses()
- .stream()
- .map(ClassInfo::toString)
- .sorted()
- .collect(Collectors.joining("," + System.lineSeparator() + " "));
- LOGGER.log(Level.TRACE,
- String.format("FilteredIndexView for %n"
- + " application class %s%n"
- + " with explicitly-referenced classes %s%n"
- + " yields exclude list: %s%n"
- + " and known classes: %n %s",
- appClassName,
- classesExplicitlyReferenced,
- excludedClasses,
- knownClassNames));
- }
-
- return result;
- }
-
- private static String toClassName(JaxRsApplication jaxRsApplication) {
- return jaxRsApplication.applicationClass()
- .map(Class::getName)
- .orElse("");
- }
-
- private static Set classNamesToIgnore(List jaxRsApplications,
- JaxRsApplication jaxRsApp,
- Set ancillaryClassNames,
- Set classesExplicitlyReferenced) {
-
- String appClassName = toClassName(jaxRsApp);
-
- Set result = // Start with all other JAX-RS app names.
- jaxRsApplications.stream()
- .map(MPOpenAPIBuilder::toClassName)
- .filter(candidateName -> !candidateName.equals("") && !candidateName.equals(appClassName))
- .collect(Collectors.toSet());
-
- if (!classesExplicitlyReferenced.isEmpty()) {
- // This class identified resource, provider, or feature classes it uses. Ignore all ancillary classes that this app
- // does not explicitly reference.
- result.addAll(ancillaryClassNames);
- result.removeAll(classesExplicitlyReferenced);
- }
-
- return result;
- }
-
- private static boolean isConcrete(ClassInfo classInfo) {
- return !Modifier.isAbstract(classInfo.flags());
- }
-
- private static class FilteringOpenApiConfigImpl extends OpenApiConfigImpl {
-
- private final Set classesToExclude;
-
- FilteringOpenApiConfigImpl(org.eclipse.microprofile.config.Config config, Set classesToExclude) {
- super(config);
- this.classesToExclude = classesToExclude;
- }
-
- @Override
- public Set scanExcludeClasses() {
- return classesToExclude;
- }
- }
-
- /**
- * Builds a list of filtered index views, one for each JAX-RS application, sorted by the Application class name to help
- * keep the list of endpoints in the OpenAPI document in a stable order.
- *
- * First, we find all resource, provider, and feature classes present in the index. This is the same for all
- * applications.
- *
- *
- * Each filtered index view is tuned to one JAX-RS application.
- *
- * @return list of {@code FilteredIndexView}s, one per JAX-RS application
- */
- List buildPerAppFilteredIndexViews() {
-
- List jaxRsApplications = MpOpenApiFeature.jaxRsApplicationsToRun().stream()
- .filter(jaxRsApp -> jaxRsApp.applicationClass().isPresent())
- .sorted(Comparator.comparing(jaxRsApplication -> jaxRsApplication.applicationClass()
- .get()
- .getName()))
- .collect(Collectors.toList());
-
- IndexView indexView = indexView();
-
- FilteredIndexView viewFilteredByConfig = new FilteredIndexView(indexView, new OpenApiConfigImpl(mpConfig));
- Set ancillaryClassNames = ancillaryClassNames(viewFilteredByConfig);
-
- /*
- * Filter even for a single-application class in case it implements getClasses or getSingletons.
- */
- return jaxRsApplications.stream()
- .map(jaxRsApp -> filteredIndexView(viewFilteredByConfig,
- jaxRsApplications,
- jaxRsApp,
- ancillaryClassNames))
- .collect(Collectors.toList());
- }
-
- private static Set ancillaryClassNames(IndexView indexView) {
- Set result = new HashSet<>(resourceClassNames(indexView));
- result.addAll(providerClassNames(indexView));
- result.addAll(featureClassNames(indexView));
- if (LOGGER.isLoggable(Level.DEBUG)) {
- LOGGER.log(Level.DEBUG, "Ancillary classes: {0}", result);
- }
- return result;
- }
-
- private static Set resourceClassNames(IndexView indexView) {
- return annotatedClassNames(indexView, Path.class);
- }
-
- private static Set providerClassNames(IndexView indexView) {
- return annotatedClassNames(indexView, Provider.class);
- }
-
- private static Set featureClassNames(IndexView indexView) {
- return annotatedClassNames(indexView, Feature.class);
- }
-
- private static Set annotatedClassNames(IndexView indexView, Class> annotationClass) {
- // Partially inspired by the SmallRye code.
- return indexView
- .getAnnotations(DotName.createSimple(annotationClass.getName()))
- .stream()
- .map(AnnotationInstance::target)
- .filter(target -> target.kind() == AnnotationTarget.Kind.CLASS)
- .map(AnnotationTarget::asClass)
- .filter(classInfo -> hasImplementationOrIsIncluded(indexView, classInfo))
- .map(ClassInfo::toString)
- .collect(Collectors.toSet());
- }
-
- private static boolean hasImplementationOrIsIncluded(IndexView indexView, ClassInfo classInfo) {
- // Partially inspired by the SmallRye code.
- return !Modifier.isInterface(classInfo.flags())
- || indexView.getAllKnownImplementors(classInfo.name()).stream()
- .anyMatch(MPOpenAPIBuilder::isConcrete);
- }
-
- /**
- * Builds an {@code IndexView} from existing Jandex index file(s) on the classpath.
- *
- * @return IndexView from all index files
- * @throws java.io.IOException in case of error attempting to open an index file
- */
- private IndexView existingIndexFileReader() throws IOException {
- List indices = new ArrayList<>();
- /*
- * Do not reuse the previously-computed indexURLs; those values will be incorrect with native images.
- */
- for (URL indexURL : findIndexFiles(indexPaths)) {
- try (InputStream indexIS = indexURL.openStream()) {
- LOGGER.log(Level.TRACE, "Adding Jandex index at {0}", indexURL.toString());
- indices.add(new IndexReader(indexIS).read());
- } catch (Exception ex) {
- throw new IOException("Attempted to read from previously-located index file "
- + indexURL + " but the index cannot be read", ex);
- }
- }
- return indices.size() == 1 ? indices.get(0) : CompositeIndex.create(indices);
- }
-
- private IndexView indexFromHarvestedClasses() throws IOException {
- Indexer indexer = new Indexer();
- annotatedTypes().forEach(c -> addClassToIndexer(indexer, c));
-
- /*
- * Some apps might be added dynamically, not via annotation processing. Add those classes to the index if they are not
- * already present.
- */
- MpOpenApiFeature.jaxRsApplicationsToRun().stream()
- .map(JaxRsApplication::applicationClass)
- .filter(Optional::isPresent)
- .forEach(appClassOpt -> addClassToIndexer(indexer, appClassOpt.get()));
-
- LOGGER.log(Level.TRACE, "Using internal Jandex index created from CDI bean discovery");
- Index result = indexer.complete();
- dumpIndex(Level.DEBUG, result);
- return result;
- }
-
- private void addClassToIndexer(Indexer indexer, Class> c) {
- try (InputStream is = MpOpenApiFeature.contextClassLoader().getResourceAsStream(resourceNameForClass(c))) {
- if (is != null) {
- indexer.index(is);
- }
- } catch (IOException ex) {
- throw new RuntimeException(String.format("Cannot load bytecode from class %s at %s for annotation processing",
- c.getName(), resourceNameForClass(c)), ex);
- }
- }
-
- private static void dumpIndex(Level level, Index index) {
- if (LOGGER.isLoggable(level)) {
- LOGGER.log(level, "Dump of internal Jandex index:");
- PrintStream oldStdout = System.out;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try (PrintStream newPS = new PrintStream(baos, true, Charset.defaultCharset())) {
- System.setOut(newPS);
- index.printAnnotations();
- index.printSubclasses();
- LOGGER.log(level, baos.toString(Charset.defaultCharset()));
- } finally {
- System.setOut(oldStdout);
- }
- }
- }
-
- private static String resourceNameForClass(Class> c) {
- return c.getName().replace('.', '/') + ".class";
- }
-
- private List findIndexFiles(String... indexPaths) {
- List result = new ArrayList<>();
- for (String indexPath : indexPaths) {
- Enumeration urls = null;
- try {
- urls = MpOpenApiFeature.contextClassLoader().getResources(indexPath);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- while (urls.hasMoreElements()) {
- result.add(urls.nextElement());
- }
- }
- return result;
- }
-
- private Set> annotatedTypes() {
- return CDI.current().getBeanManager().getExtension(OpenApiCdiExtension.class).annotatedTypes();
- }
-}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java
deleted file mode 100644
index 8b3b43fc33d..00000000000
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiFeature.java
+++ /dev/null
@@ -1,255 +0,0 @@
-/*
- * Copyright (c) 2023 Oracle and/or its affiliates.
- *
- * Licensed 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 io.helidon.microprofile.openapi;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.StringWriter;
-import java.nio.charset.Charset;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.function.Function;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import io.helidon.common.LazyValue;
-import io.helidon.microprofile.server.JaxRsApplication;
-import io.helidon.microprofile.server.JaxRsCdiExtension;
-import io.helidon.openapi.OpenApiFeature;
-
-import io.smallrye.openapi.api.OpenApiConfig;
-import io.smallrye.openapi.api.OpenApiDocument;
-import io.smallrye.openapi.api.models.OpenAPIImpl;
-import io.smallrye.openapi.api.util.MergeUtil;
-import io.smallrye.openapi.runtime.OpenApiProcessor;
-import io.smallrye.openapi.runtime.OpenApiStaticFile;
-import io.smallrye.openapi.runtime.io.Format;
-import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
-import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner;
-import jakarta.enterprise.inject.spi.CDI;
-import org.eclipse.microprofile.openapi.models.OpenAPI;
-import org.jboss.jandex.IndexView;
-
-/**
- * MP variant of OpenApiFeature.
- */
-public class MpOpenApiFeature extends OpenApiFeature {
-
- /**
- * Creates a new builder for the MP OpenAPI feature.
- *
- * @return new builder
- */
- public static MPOpenAPIBuilder builder() {
- return new MPOpenAPIBuilder();
- }
-
- /**
- * Parser helper.
- */
- static final LazyValue PARSER_HELPER = LazyValue.create(ParserHelper::create);
-
- /**
- * Returns the {@code JaxRsApplication} instances that should be run, according to the JAX-RS CDI extension.
- *
- * @return List of JaxRsApplication instances that should be run
- */
- static List jaxRsApplicationsToRun() {
- JaxRsCdiExtension ext = CDI.current()
- .getBeanManager()
- .getExtension(JaxRsCdiExtension.class);
-
- return ext.applicationsToRun();
- }
-
- private static final System.Logger LOGGER = System.getLogger(MpOpenApiFeature.class.getName());
-
- private final Supplier> filteredIndexViewsSupplier;
-
- private final Lock modelAccess = new ReentrantLock(true);
-
- private final OpenApiConfig openApiConfig;
- private final io.helidon.openapi.OpenApiStaticFile openApiStaticFile;
-
- private final MPOpenAPIBuilder builder;
- private OpenAPI model;
-
- private final Map, ExpandedTypeDescription> implsToTypes;
-
- protected MpOpenApiFeature(MPOpenAPIBuilder builder) {
- super(LOGGER, builder);
- this.builder = builder;
- implsToTypes = buildImplsToTypes();
- openApiConfig = builder.openApiConfig();
- openApiStaticFile = builder.staticFile();
- filteredIndexViewsSupplier = builder::buildPerAppFilteredIndexViews;
- }
-
- @Override
- protected String openApiContent(OpenAPIMediaType openApiMediaType) {
-
- return openApiContent(openApiMediaType, model());
- }
-
- /**
- * Triggers preparation of the model from external code.
- */
- protected void prepareModel() {
- model();
- }
-
- /**
- * Returns the current thread's context class loader.
- *
- * @return class loader in use by the thread
- */
- static ClassLoader contextClassLoader() {
- return Thread.currentThread().getContextClassLoader();
- }
-
- // For testing
- IndexView indexView() {
- return builder.indexView();
- }
-
- Map, ExpandedTypeDescription> buildImplsToTypes() {
- return Collections.unmodifiableMap(PARSER_HELPER.get().types()
- .values()
- .stream()
- .collect(Collectors.toMap(ExpandedTypeDescription::impl,
- Function.identity())));
- }
-
-
- private String openApiContent(OpenAPIMediaType openAPIMediaType, OpenAPI model) {
- StringWriter sw = new StringWriter();
- Serializer.serialize(PARSER_HELPER.get().types(), implsToTypes, model, openAPIMediaType, sw);
- return sw.toString();
- }
-
- /**
- * Prepares the OpenAPI model that later will be used to create the OpenAPI
- * document for endpoints in this application.
- *
- * @param config {@code OpenApiConfig} object describing paths, servers, etc.
- * @param staticFile the static file, if any, to be included in the resulting model
- * @param filteredIndexViews possibly empty list of FilteredIndexViews to use in harvesting definitions from the code
- * @return the OpenAPI model
- * @throws RuntimeException in case of errors reading any existing static
- * OpenAPI document
- */
- private OpenAPI prepareModel(OpenApiConfig config, OpenApiStaticFile staticFile,
- List extends IndexView> filteredIndexViews) {
- try {
- // The write lock guarding the model has already been acquired.
- OpenApiDocument.INSTANCE.reset();
- OpenApiDocument.INSTANCE.config(config);
- OpenApiDocument.INSTANCE.modelFromReader(OpenApiProcessor.modelFromReader(config, contextClassLoader()));
- if (staticFile != null) {
- OpenApiDocument.INSTANCE.modelFromStaticFile(OpenApiParser.parse(PARSER_HELPER.get().types(),
- staticFile.getContent()));
- }
- if (isAnnotationProcessingEnabled(config)) {
- expandModelUsingAnnotations(config, filteredIndexViews);
- } else {
- LOGGER.log(System.Logger.Level.TRACE, "OpenAPI Annotation processing is disabled");
- }
- OpenApiDocument.INSTANCE.filter(OpenApiProcessor.getFilter(config, contextClassLoader()));
- OpenApiDocument.INSTANCE.initialize();
- OpenAPIImpl instance = OpenAPIImpl.class.cast(OpenApiDocument.INSTANCE.get());
-
- // Create a copy, primarily to avoid problems during unit testing.
- // The SmallRye MergeUtil omits the openapi value, so we need to set it explicitly.
- return MergeUtil.merge(new OpenAPIImpl(), instance)
- .openapi(instance.getOpenapi());
- } catch (IOException ex) {
- throw new RuntimeException("Error initializing OpenAPI information", ex);
- }
- }
-
-
- private static Format toFormat(OpenAPIMediaType openAPIMediaType) {
- return openAPIMediaType.equals(OpenAPIMediaType.YAML)
- ? Format.YAML
- : Format.JSON;
- }
-
- private boolean isAnnotationProcessingEnabled(OpenApiConfig config) {
- return !config.scanDisable();
- }
-
- private void expandModelUsingAnnotations(OpenApiConfig config, List extends IndexView> filteredIndexViews) {
- if (filteredIndexViews.isEmpty() || config.scanDisable()) {
- return;
- }
-
- /*
- * Conduct a SmallRye OpenAPI annotation scan for each filtered index view, merging the resulting OpenAPI models into one.
- * The AtomicReference is effectively final so we can update the actual reference from inside the lambda.
- */
- AtomicReference aggregateModelRef = new AtomicReference<>(new OpenAPIImpl()); // Start with skeletal model
- filteredIndexViews.forEach(filteredIndexView -> {
- OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(config, filteredIndexView,
- List.of(new HelidonAnnotationScannerExtension()));
- OpenAPI modelForApp = scanner.scan();
- if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
-
- LOGGER.log(System.Logger.Level.DEBUG, String.format("Intermediate model from filtered index view %s:%n%s",
- filteredIndexView.getKnownClasses(),
- openApiContent(OpenAPIMediaType.YAML, modelForApp)));
- }
- aggregateModelRef.set(
- MergeUtil.merge(aggregateModelRef.get(), modelForApp)
- .openapi(modelForApp.getOpenapi())); // SmallRye's merge skips openapi value.
-
- });
- OpenApiDocument.INSTANCE.modelFromAnnotations(aggregateModelRef.get());
- }
-
- private OpenAPI model() {
- return access(() -> {
- if (model == null) {
- model = prepareModel(openApiConfig, toSmallRye(openApiStaticFile), filteredIndexViewsSupplier.get());
- }
- return model;
- });
- }
-
- private static OpenApiStaticFile toSmallRye(io.helidon.openapi.OpenApiStaticFile staticFile) {
-
- return staticFile == null
- ? null
- : new OpenApiStaticFile(
- new BufferedInputStream(
- new ByteArrayInputStream(staticFile.content()
- .getBytes(Charset.defaultCharset()))),
- toFormat(staticFile.openApiMediaType()));
- }
-
- private T access(Supplier operation) {
- modelAccess.lock();
- try {
- return operation.get();
- } finally {
- modelAccess.unlock();
- }
- }
-}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManager.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManager.java
new file mode 100644
index 00000000000..ee30391f86c
--- /dev/null
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManager.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.lang.System.Logger.Level;
+import java.util.List;
+import java.util.Set;
+
+import io.helidon.common.LazyValue;
+import io.helidon.microprofile.server.JaxRsApplication;
+import io.helidon.microprofile.server.JaxRsCdiExtension;
+import io.helidon.openapi.OpenApiFormat;
+import io.helidon.openapi.OpenApiManager;
+
+import io.smallrye.openapi.api.OpenApiConfig;
+import io.smallrye.openapi.api.OpenApiConfigImpl;
+import io.smallrye.openapi.api.OpenApiDocument;
+import io.smallrye.openapi.api.models.OpenAPIImpl;
+import io.smallrye.openapi.api.util.MergeUtil;
+import io.smallrye.openapi.runtime.OpenApiProcessor;
+import io.smallrye.openapi.runtime.scanner.AnnotationScannerExtension;
+import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
+import io.smallrye.openapi.runtime.scanner.OpenApiAnnotationScanner;
+import jakarta.enterprise.inject.spi.BeanManager;
+import jakarta.enterprise.inject.spi.CDI;
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.jboss.jandex.IndexView;
+
+/**
+ * A {@link OpenApiManager} for MicroProfile.
+ */
+final class MpOpenApiManager implements OpenApiManager {
+
+ private static final System.Logger LOGGER = System.getLogger(MpOpenApiManager.class.getName());
+ private static final String CONFIG_EXT_PREFIX = "mp.openapi.extensions.helidon.";
+
+ /**
+ * Full config key for the {@code JAXRS_SEMANTICS} option.
+ */
+ static final String USE_JAXRS_SEMANTICS_KEY = CONFIG_EXT_PREFIX + "use-jaxrs-semantics";
+
+ private final Config config;
+ private final MpOpenApiManagerConfig managerConfig;
+ private final OpenApiConfig openApiConfig;
+ private final List scannerExtensions = List.of(new JsonpAnnotationScannerExtension());
+ private final LazyValue> filteredIndexViews = LazyValue.create(this::buildFilteredIndexViews);
+
+ MpOpenApiManager(Config config) {
+ this.config = config;
+ this.managerConfig = MpOpenApiManagerConfig.builder()
+ .update(builder -> config.getOptionalValue(USE_JAXRS_SEMANTICS_KEY, Boolean.class)
+ .ifPresent(builder::useJaxRsSemantics))
+ .build();
+ this.openApiConfig = new OpenApiConfigImpl(config);
+ }
+
+ @Override
+ public String name() {
+ return "manager";
+ }
+
+ @Override
+ public String type() {
+ return "mp";
+ }
+
+ @Override
+ public OpenAPI load(String content) {
+ ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
+ OpenApiDocument.INSTANCE.reset();
+ OpenApiDocument.INSTANCE.config(openApiConfig);
+ OpenApiDocument.INSTANCE.modelFromReader(OpenApiProcessor.modelFromReader(openApiConfig, contextClassLoader));
+ if (!content.isBlank()) {
+ OpenAPI document = OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, new StringReader(content));
+ OpenApiDocument.INSTANCE.modelFromStaticFile(document);
+ }
+ if (!openApiConfig.scanDisable()) {
+ processAnnotations();
+ } else {
+ LOGGER.log(Level.TRACE, "OpenAPI Annotation processing is disabled");
+ }
+ OpenApiDocument.INSTANCE.filter(OpenApiProcessor.getFilter(openApiConfig, contextClassLoader));
+ OpenApiDocument.INSTANCE.initialize();
+ OpenAPIImpl instance = (OpenAPIImpl) OpenApiDocument.INSTANCE.get();
+
+ // MergeUtil omits the openapi value, so we need to set it explicitly.
+ return MergeUtil.merge(new OpenAPIImpl(), instance).openapi(instance.getOpenapi());
+ }
+
+ @Override
+ public String format(OpenAPI model, OpenApiFormat format) {
+ StringWriter sw = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), model, format, sw);
+ return sw.toString();
+ }
+
+ /**
+ * Get the filtered index views.
+ *
+ * @return list of filter index views
+ */
+ List filteredIndexViews() {
+ return filteredIndexViews.get();
+ }
+
+ private void processAnnotations() {
+ List indexViews = filteredIndexViews();
+ if (openApiConfig.scanDisable() || indexViews.isEmpty()) {
+ return;
+ }
+
+ // Conduct a SmallRye OpenAPI annotation scan for each filtered index view
+ // merging the resulting OpenAPI models into one.
+ OpenAPI model = new OpenAPIImpl(); // Start with skeletal model
+ for (IndexView indexView : indexViews) {
+ OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(openApiConfig, indexView, scannerExtensions);
+ OpenAPI scanned = scanner.scan();
+ if (LOGGER.isLoggable(Level.DEBUG)) {
+ LOGGER.log(Level.DEBUG, String.format(
+ "Intermediate scanned from filtered index view %s:%n%s",
+ indexView.getKnownClasses(),
+ format(scanned, OpenApiFormat.YAML)));
+ }
+ model = MergeUtil.merge(model, scanned).openapi(scanned.getOpenapi()); // SmallRye's merge skips openapi value.
+ }
+ OpenApiDocument.INSTANCE.modelFromAnnotations(model);
+ }
+
+ private List buildFilteredIndexViews() {
+ BeanManager beanManager = CDI.current().getBeanManager();
+ List jaxRsApps = beanManager.getExtension(JaxRsCdiExtension.class).applicationsToRun();
+ Set> annotatedTypes = beanManager.getExtension(OpenApiCdiExtension.class).annotatedTypes();
+ return new FilteredIndexViewsBuilder(config,
+ jaxRsApps,
+ annotatedTypes,
+ managerConfig.indexPaths(),
+ managerConfig.useJaxRsSemantics()).buildViews();
+ }
+}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManagerConfigBlueprint.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManagerConfigBlueprint.java
new file mode 100644
index 00000000000..62eb7aeb3ce
--- /dev/null
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/MpOpenApiManagerConfigBlueprint.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.util.List;
+
+import io.helidon.builder.api.Prototype;
+import io.helidon.config.metadata.Configured;
+import io.helidon.config.metadata.ConfiguredOption;
+
+/**
+ * {@link MpOpenApiManager} prototype.
+ */
+@Prototype.Blueprint
+@Configured
+interface MpOpenApiManagerConfigBlueprint {
+
+ /**
+ * If {@code true} and the {@code jakarta.ws.rs.core.Application} class returns a non-empty set, endpoints defined by
+ * other resources are not included in the OpenAPI document.
+ *
+ * @return {@code true} if enabled, {@code false} otherwise
+ */
+ @ConfiguredOption(key = MpOpenApiManager.USE_JAXRS_SEMANTICS_KEY)
+ boolean useJaxRsSemantics();
+
+ /**
+ * Specify the set of Jandex index path.
+ *
+ * @return list of Jandex index path
+ */
+ @ConfiguredOption(configured = false, value = "META-INF/jandex.idx")
+ List indexPaths();
+}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java
index f930611e652..fd39bf1e385 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiCdiExtension.java
@@ -42,36 +42,16 @@ public class OpenApiCdiExtension extends HelidonRestCdiExtension {
private static final System.Logger LOGGER = System.getLogger(OpenApiCdiExtension.class.getName());
- /**
- * Normal location of Jandex index files.
- */
- static final String INDEX_PATH = "META-INF/jandex.idx";
-
- private final String[] paths;
-
private final Set> annotatedTypes = new HashSet<>();
-
- private volatile MpOpenApiFeature openApiFeature;
+ private volatile OpenApiFeature feature;
/**
- * Creates a new instance of the index builder.
- *
+ * Creates a new instance.
*/
public OpenApiCdiExtension() {
- this(INDEX_PATH);
- }
-
- OpenApiCdiExtension(String... indexPaths) {
- super(LOGGER, OpenApiFeature.Builder.CONFIG_KEY);
- this.paths = indexPaths;
+ super(LOGGER, "openapi", "mp.openapi");
}
- @Override
- protected void processManagedBean(ProcessManagedBean> processManagedBean) {
- // SmallRye handles annotation processing. We have this method because the abstract superclass requires it.
- }
-
-
/**
* Register the Health observer with server observer feature.
* This is a CDI observer method invoked by CDI machinery.
@@ -83,41 +63,34 @@ public void registerService(@Observes @Priority(LIBRARY_BEFORE + 10) @Initialize
Object event,
ServerCdiExtension server) {
- org.eclipse.microprofile.config.Config mpConfig = ConfigProvider.getConfig();
-
- this.openApiFeature = MpOpenApiFeature.builder()
+ feature = OpenApiFeature.builder()
.config(componentConfig())
- .indexPaths(paths)
- .config(mpConfig)
+ .manager(new MpOpenApiManager(ConfigProvider.getConfig()))
.build();
-
- this.openApiFeature.setup(server.serverRoutingBuilder(), super.routingBuilder(server));
+ feature.setup(server.serverRoutingBuilder(), routingBuilder(server));
}
- // Must run after the server has created the Application instances.
- void buildModel(@Observes @Priority(PLATFORM_AFTER + 100 + 10) @Initialized(ApplicationScoped.class) Object event) {
- this.openApiFeature.prepareModel();
- }
-
- // For testing
- MpOpenApiFeature feature() {
- return openApiFeature;
+ @Override
+ protected void processManagedBean(ProcessManagedBean> processManagedBean) {
+ // SmallRye handles annotation processing. We have this method because the abstract superclass requires it.
}
-
+ /**
+ * Get the annotated types.
+ *
+ * @return annotated types
+ */
Set> annotatedTypes() {
return annotatedTypes;
}
- /**
- * Records each type that is annotated.
- *
- * @param annotated type
- * @param event {@code ProcessAnnotatedType} event
- */
+ // Must run after the server has created the Application instances.
+ private void buildModel(@Observes @Priority(PLATFORM_AFTER + 100 + 10) @Initialized(ApplicationScoped.class) Object event) {
+ feature.initialize();
+ }
+
+ // Records each type that is annotated
private void processAnnotatedType(@Observes ProcessAnnotatedType event) {
- Class> c = event.getAnnotatedType()
- .getJavaClass();
- annotatedTypes.add(c);
+ annotatedTypes.add(event.getAnnotatedType().getJavaClass());
}
}
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiHelper.java
similarity index 71%
rename from microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java
rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiHelper.java
index cd3107c76ee..95fa26f607d 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/ParserHelper.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiHelper.java
@@ -20,6 +20,8 @@
import java.util.Map;
import java.util.Set;
+import io.helidon.common.LazyValue;
+
import org.eclipse.microprofile.openapi.models.Extensible;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
@@ -27,11 +29,12 @@
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
import org.yaml.snakeyaml.TypeDescription;
+import org.yaml.snakeyaml.introspector.Property;
/**
* Wraps generated parser and uses {@link ExpandedTypeDescription} as its type.
*/
-class ParserHelper {
+final class OpenApiHelper {
// Temporary to suppress SnakeYAML warnings.
// As a static we keep a reference to the logger, thereby making sure any changes we make are persistent. (JUL holds
@@ -39,28 +42,18 @@ class ParserHelper {
private static final java.util.logging.Logger SNAKE_YAML_INTROSPECTOR_LOGGER =
java.util.logging.Logger.getLogger(org.yaml.snakeyaml.introspector.PropertySubstitute.class.getPackage().getName());
- /**
- * The SnakeYAMLParserHelper is generated by a maven plug-in.
- */
- private final SnakeYAMLParserHelper generatedHelper;
+ private static final LazyValue INSTANCE = LazyValue.create(OpenApiHelper::new);
- private ParserHelper(SnakeYAMLParserHelper generatedHelper) {
- this.generatedHelper = generatedHelper;
- adjustTypeDescriptions(generatedHelper.types());
- }
+ // The SnakeYAMLParserHelper is generated by a maven plug-in.
+ private final SnakeYAMLParserHelper generatedHelper;
- /**
- * Create a new parser helper.
- *
- * @return a new parser helper
- */
- static ParserHelper create() {
+ private OpenApiHelper() {
boolean warningsEnabled = Boolean.getBoolean("openapi.parsing.warnings.enabled");
if (SNAKE_YAML_INTROSPECTOR_LOGGER.isLoggable(java.util.logging.Level.WARNING) && !warningsEnabled) {
SNAKE_YAML_INTROSPECTOR_LOGGER.setLevel(java.util.logging.Level.SEVERE);
}
- ParserHelper helper = new ParserHelper(SnakeYAMLParserHelper.create(ExpandedTypeDescription::create));
- return helper;
+ this.generatedHelper = SnakeYAMLParserHelper.create(ExpandedTypeDescription::create);
+ adjustTypeDescriptions(generatedHelper.types());
}
/**
@@ -68,41 +61,26 @@ static ParserHelper create() {
*
* @return types of this helper
*/
- public Map, ExpandedTypeDescription> types() {
- return generatedHelper.types();
- }
-
- /**
- * Entries of this helper.
- *
- * @return entry set
- */
- public Set, ExpandedTypeDescription>> entrySet() {
- return generatedHelper.entrySet();
+ static Map, ExpandedTypeDescription> types() {
+ return INSTANCE.get().generatedHelper.types();
}
private static void adjustTypeDescriptions(Map, ExpandedTypeDescription> types) {
- /*
- * We need to adjust the {@code TypeDescription} objects set up by the generated {@code SnakeYAMLParserHelper} class
- * because there are some OpenAPI-specific issues that the general-purpose helper generator cannot know about.
- */
-
- /*
- * In the OpenAPI document, HTTP methods are expressed in lower-case. But the associated Java methods on the PathItem
- * class use the HTTP method names in upper-case. So for each HTTP method, "add" a property to PathItem's type
- * description using the lower-case name but upper-case Java methods and exclude the upper-case property that
- * SnakeYAML's automatic analysis of the class already created.
- */
+ // We need to adjust the {@code TypeDescription} objects set up by the generated {@code SnakeYAMLParserHelper} class
+ // because there are some OpenAPI-specific issues that the general-purpose helper generator cannot know about.
+
+ // In the OpenAPI document, HTTP methods are expressed in lower-case. But the associated Java methods on the PathItem
+ // class use the HTTP method names in upper-case. So for each HTTP method, "add" a property to PathItem's type
+ // description using the lower-case name but upper-case Java methods and exclude the upper-case property that
+ // SnakeYAML's automatic analysis of the class already created.
ExpandedTypeDescription pathItemTD = types.get(PathItem.class);
for (PathItem.HttpMethod m : PathItem.HttpMethod.values()) {
pathItemTD.substituteProperty(m.name().toLowerCase(), Operation.class, getter(m), setter(m));
pathItemTD.addExcludes(m.name());
}
- /*
- * An OpenAPI document can contain a property named "enum" for Schema and ServerVariable, but the related Java methods
- * use "enumeration".
- */
+ // An OpenAPI document can contain a property named "enum" for Schema and ServerVariable, but the related Java methods
+ // use "enumeration".
Set.>of(Schema.class, ServerVariable.class).forEach(c -> {
ExpandedTypeDescription tdWithEnumeration = types.get(c);
tdWithEnumeration.substituteProperty("enum", List.class, "getEnumeration", "setEnumeration");
@@ -110,16 +88,15 @@ private static void adjustTypeDescriptions(Map, ExpandedTypeDescription
tdWithEnumeration.addExcludes("enumeration");
});
- /*
- * SnakeYAML derives properties only from methods declared directly by each OpenAPI interface, not from methods defined
- * on other interfaces which the original one extends. Those we have to handle explicitly.
- */
+ // SnakeYAML derives properties only from methods declared directly by each OpenAPI interface, not from methods defined
+ // on other interfaces which the original one extends. Those we have to handle explicitly.
for (ExpandedTypeDescription td : types.values()) {
if (Extensible.class.isAssignableFrom(td.getType())) {
td.addExtensions();
}
- if (td.hasDefaultProperty()) {
- td.substituteProperty("default", Object.class, "getDefaultValue", "setDefaultValue");
+ Property defaultProperty = td.defaultProperty();
+ if (defaultProperty != null) {
+ td.substituteProperty("default", defaultProperty.getType(), "getDefaultValue", "setDefaultValue");
td.addExcludes("defaultValue");
}
if (isRef(td)) {
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java
index 98fc7c03d0f..ed2d470f199 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiParser.java
@@ -15,14 +15,9 @@
*/
package io.helidon.microprofile.openapi;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.Reader;
-import java.nio.charset.StandardCharsets;
import java.util.Map;
-import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.TypeDescription;
import org.yaml.snakeyaml.Yaml;
@@ -37,20 +32,6 @@ final class OpenApiParser {
private OpenApiParser() {
}
- /**
- * Parse open API.
- *
- * @param types types
- * @param inputStream input stream to parse from
- * @return parsed document
- * @throws IOException in case of I/O problems
- */
- static OpenAPI parse(Map, ExpandedTypeDescription> types, InputStream inputStream) throws IOException {
- try (Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
- return parse(types, OpenAPI.class, reader);
- }
- }
-
/**
* Parse YAML or JSON using the specified types, returning the specified type with input taken from the indicated reader.
*
diff --git a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiSerializer.java
similarity index 74%
rename from microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java
rename to microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiSerializer.java
index 051500bfedb..8d76326634c 100644
--- a/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/Serializer.java
+++ b/microprofile/openapi/src/main/java/io/helidon/microprofile/openapi/OpenApiSerializer.java
@@ -26,7 +26,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import io.helidon.openapi.OpenApiFeature;
+import io.helidon.openapi.OpenApiFormat;
import io.smallrye.openapi.api.models.OpenAPIImpl;
import org.eclipse.microprofile.openapi.models.Extensible;
@@ -35,6 +35,7 @@
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.DumperOptions.ScalarStyle;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.nodes.MappingNode;
@@ -51,14 +52,14 @@
* while suppressing tags that would indicate the SmallRye classes -- we don't want to
* suggest that the output can only be read into the SmallRye implementation.
*/
-public class Serializer {
+final class OpenApiSerializer {
private static final DumperOptions YAML_DUMPER_OPTIONS = new DumperOptions();
private static final DumperOptions JSON_DUMPER_OPTIONS = new DumperOptions();
- private static final System.Logger LOGGER = System.getLogger(Serializer.class.getName());
+ private static final System.Logger LOGGER = System.getLogger(OpenApiSerializer.class.getName());
- private Serializer() {
+ private OpenApiSerializer() {
}
static {
@@ -67,7 +68,7 @@ private Serializer() {
JSON_DUMPER_OPTIONS.setDefaultFlowStyle(DumperOptions.FlowStyle.FLOW);
JSON_DUMPER_OPTIONS.setPrettyFlow(true);
- JSON_DUMPER_OPTIONS.setDefaultScalarStyle(DumperOptions.ScalarStyle.DOUBLE_QUOTED);
+ JSON_DUMPER_OPTIONS.setDefaultScalarStyle(ScalarStyle.DOUBLE_QUOTED);
JSON_DUMPER_OPTIONS.setSplitLines(false);
}
@@ -75,29 +76,26 @@ private Serializer() {
* Serialize using the selected format.
*
* @param types types
- * @param implsToTypes implementations to types
* @param openAPI Open API document to serialize
- * @param openAPIMediaType OpenAPI media type to use
+ * @param format OpenAPI media type to use
* @param writer writer to serialize to
*/
- public static void serialize(Map, ExpandedTypeDescription> types,
- Map, ExpandedTypeDescription> implsToTypes,
- OpenAPI openAPI,
- OpenApiFeature.OpenAPIMediaType openAPIMediaType,
- Writer writer) {
- if (openAPIMediaType.equals(OpenApiFeature.OpenAPIMediaType.JSON)) {
- serialize(types, implsToTypes, openAPI, writer, JSON_DUMPER_OPTIONS, DumperOptions.ScalarStyle.DOUBLE_QUOTED);
+ static void serialize(Map, ExpandedTypeDescription> types,
+ OpenAPI openAPI,
+ OpenApiFormat format,
+ Writer writer) {
+ if (format.equals(OpenApiFormat.JSON)) {
+ serialize(types, openAPI, writer, JSON_DUMPER_OPTIONS, ScalarStyle.DOUBLE_QUOTED);
} else {
- serialize(types, implsToTypes, openAPI, writer, YAML_DUMPER_OPTIONS, DumperOptions.ScalarStyle.PLAIN);
+ serialize(types, openAPI, writer, YAML_DUMPER_OPTIONS, ScalarStyle.PLAIN);
}
}
- private static void serialize(Map, ExpandedTypeDescription> types,
- Map, ExpandedTypeDescription> implsToTypes, OpenAPI openAPI, Writer writer,
+ private static void serialize(Map, ExpandedTypeDescription> types, OpenAPI openAPI, Writer writer,
DumperOptions dumperOptions,
- DumperOptions.ScalarStyle stringStyle) {
+ ScalarStyle stringStyle) {
- Yaml yaml = new Yaml(new CustomRepresenter(types, implsToTypes, dumperOptions, stringStyle), dumperOptions);
+ Yaml yaml = new Yaml(new CustomRepresenter(types, dumperOptions, stringStyle), dumperOptions);
yaml.dump(openAPI, new TagSuppressingWriter(writer));
}
@@ -115,15 +113,11 @@ static class CustomRepresenter extends Representer {
private static final String EXTENSIONS = "extensions";
- private final DumperOptions.ScalarStyle stringStyle;
+ private final ScalarStyle stringStyle;
- private final Map, ExpandedTypeDescription> implsToTypes;
- CustomRepresenter(Map, ExpandedTypeDescription> types,
- Map, ExpandedTypeDescription> implsToTypes, DumperOptions dumperOptions,
- DumperOptions.ScalarStyle stringStyle) {
+ CustomRepresenter(Map, ExpandedTypeDescription> types, DumperOptions dumperOptions, ScalarStyle stringStyle) {
super(dumperOptions);
- this.implsToTypes = implsToTypes;
this.stringStyle = stringStyle;
types.values().stream()
.map(ImplTypeDescription::new)
@@ -131,8 +125,8 @@ static class CustomRepresenter extends Representer {
}
@Override
- protected Node representScalar(Tag tag, String value, DumperOptions.ScalarStyle style) {
- return super.representScalar(tag, value, isExemptedFromQuotes(tag) ? DumperOptions.ScalarStyle.PLAIN : style);
+ protected Node representScalar(Tag tag, String value, ScalarStyle style) {
+ return super.representScalar(tag, value, isExemptedFromQuotes(tag) ? ScalarStyle.PLAIN : style);
}
@Override
@@ -170,15 +164,12 @@ protected NodeTuple representJavaBeanProperty(Object javaBean, Property property
return null;
}
- Property p = property;
Object v = adjustPropertyValue(propertyValue);
- if (propertyValue instanceof Enum) {
- Enum e = (Enum) propertyValue;
+ if (propertyValue instanceof Enum> e) {
v = e.toString();
}
- NodeTuple result = okToProcess(javaBean, property)
- ? doRepresentJavaBeanProperty(javaBean, p, v, customTag) : null;
- return result;
+ return okToProcess(javaBean, property)
+ ? doRepresentJavaBeanProperty(javaBean, property, v, customTag) : null;
}
private NodeTuple doRepresentJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
@@ -187,11 +178,9 @@ private NodeTuple doRepresentJavaBeanProperty(Object javaBean, Property property
return new NodeTuple(representData("$ref"), defaultTuple.getValueNode());
}
if (javaBean instanceof Schema) {
- /*
- * At most one of additionalPropertiesBoolean and additionalPropertiesSchema will return a non-null value.
- * Whichever one does (if either), replace the name with "additionalProperties" for output. Skip whatever is
- * returned from the deprecated additionalProperties method itself.
- */
+ // At most one of additionalPropertiesBoolean and additionalPropertiesSchema will return a non-null value.
+ // Whichever one does (if either), replace the name with "additionalProperties" for output. Skip whatever is
+ // returned from the deprecated additionalProperties method itself.
String propertyName = property.getName();
if (propertyName.equals("additionalProperties")) {
return null;
@@ -203,19 +192,18 @@ private NodeTuple doRepresentJavaBeanProperty(Object javaBean, Property property
}
private Object adjustPropertyValue(Object propertyValue) {
- /* Some MP OpenAPI TCK tests expect an integer-style format, even for BigDecimal types, if the
- * value is an integer. Because the formatting is done in SnakeYAML code based on the type of the value,
- * we need to replace a, for example BigDecimal that happen to be an integer value, with an Integer.
- * See https://github.com/eclipse/microprofile-open-api/issues/412
- */
- if (Number.class.isInstance(propertyValue) && !Boolean.getBoolean("io.helidon.openapi.skipTCKWorkaround")) {
- Number n = (Number) propertyValue;
+ // Some MP OpenAPI TCK tests expect an integer-style format, even for BigDecimal types, if the
+ // value is an integer. Because the formatting is done in SnakeYAML code based on the type of the value,
+ // we need to replace a for example BigDecimal that happen to be an integer value, with an Integer.
+ // See https://github.com/eclipse/microprofile-open-api/issues/412
+ if (propertyValue instanceof Number n && !Boolean.getBoolean("io.helidon.openapi.skipTCKWorkaround")) {
float diff = n.floatValue() - n.intValue();
if (diff == 0) {
- propertyValue = Integer.valueOf(n.intValue());
+ propertyValue = n.intValue();
} else if (Math.abs(diff) < 0.1) {
- LOGGER.log(Level.WARNING,
- String.format("Integer approximation of %f did not match but the difference was only %e", n, diff));
+ LOGGER.log(Level.WARNING, String.format(
+ "Integer approximation of %f did not match but the difference was only %e",
+ n.floatValue(), diff));
}
}
return propertyValue;
@@ -223,23 +211,17 @@ private Object adjustPropertyValue(Object propertyValue) {
@Override
protected MappingNode representJavaBean(Set properties, Object javaBean) {
- /*
- * First, let SnakeYAML prepare the node normally. If the JavaBean is Extensible and has extension properties, the
- * will contain a subnode called "extensions" which itself has one or more subnodes, one for each extension
- * property assigned.
- */
+ // First, let SnakeYAML prepare the node normally. If the JavaBean is Extensible and has extension properties, it
+ // will contain a sub-node called "extensions" which itself has one or more sub-nodes, one for each extension
+ // property assigned.
MappingNode result = super.representJavaBean(properties, javaBean);
- /*
- * Now promote the individual subnodes for each extension property (if any) up one level so that they are peers of the
- * other properties. Also remove the "extensions" node.
- */
+ // Now promote the individual sub-nodes for each extension property (if any) up one level so that they are peers of
+ // the other properties. Also remove the "extensions" node.
processExtensions(result, javaBean);
- /*
- * Clearing representedObjects is an awkward but effective way of preventing SnakeYAML from using anchors and
- * aliases, which apparently the Jackson parser used in the TCK (as of this writing) does not handle properly.
- */
+ // Clearing representedObjects is an awkward but effective way of preventing SnakeYAML from using anchors and
+ // aliases, which apparently the Jackson parser used in the TCK (as of this writing) does not handle properly.
representedObjects.clear();
return result;
}
@@ -298,13 +280,12 @@ private List processExtensions(NodeTuple tuple) {
* @param property the property being serialized
* @return true if the property should be processes; false otherwise
*/
+ @SuppressWarnings("ConstantValue")
private boolean okToProcess(Object javaBean, Property property) {
- /*
- * The following construct might look awkward - and it is. But if SmallRye adds additional properties to its
- * implementation classes that are not in the corresponding interfaces - and therefore we want to skip processing
- * them - then we can just add additional lines like the "reject |= ..." one, testing for the new case, without
- * having to change any other lines in the method.
- */
+ // The following construct might look awkward - and it is. But if SmallRye adds additional properties to its
+ // implementation classes that are not in the corresponding interfaces - and therefore we want to skip processing
+ // them - then we can just add additional lines like the "reject |= ..." one, testing for the new case, without
+ // having to change any other lines in the method.
boolean reject = false;
reject |= Parameter.class.isAssignableFrom(javaBean.getClass()) && property.getName().equals("hidden");
return !reject;
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/AdditionalPropertiesTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/AdditionalPropertiesTest.java
new file mode 100644
index 00000000000..35c5a9d8e50
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/AdditionalPropertiesTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2021, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+
+import io.helidon.openapi.OpenApiFormat;
+
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.eclipse.microprofile.openapi.models.media.Schema;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+import static io.helidon.microprofile.openapi.TestUtil.query;
+import static io.helidon.microprofile.openapi.TestUtil.resource;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+
+class AdditionalPropertiesTest {
+
+ @Test
+ void checkParsingBooleanAdditionalProperties() {
+ OpenAPI openAPI = parse("/withBooleanAddlProps.yml");
+ Schema itemSchema = openAPI.getComponents().getSchemas().get("item");
+
+ Schema additionalPropertiesSchema = itemSchema.getAdditionalPropertiesSchema();
+ Boolean additionalPropertiesBoolean = itemSchema.getAdditionalPropertiesBoolean();
+
+ assertThat(additionalPropertiesSchema, is(nullValue()));
+ assertThat(additionalPropertiesBoolean, is(notNullValue()));
+ assertThat(additionalPropertiesBoolean, is(false));
+ }
+
+ @Test
+ void checkParsingSchemaAdditionalProperties() {
+ OpenAPI openAPI = parse("/withSchemaAddlProps.yml");
+ Schema itemSchema = openAPI.getComponents().getSchemas().get("item");
+
+ Schema additionalPropertiesSchema = itemSchema.getAdditionalPropertiesSchema();
+ Boolean additionalPropertiesBoolean = itemSchema.getAdditionalPropertiesBoolean();
+
+ assertThat(additionalPropertiesBoolean, is(nullValue()));
+ assertThat(additionalPropertiesSchema, is(notNullValue()));
+
+ Map additionalProperties = additionalPropertiesSchema.getProperties();
+ assertThat(additionalProperties, hasKey("code"));
+ assertThat(additionalProperties, hasKey("text"));
+ }
+
+ @Test
+ void checkWritingSchemaAdditionalProperties() {
+ OpenAPI openAPI = parse("/withSchemaAddlProps.yml");
+ String document = format(openAPI);
+
+ // Expected output:
+ // additionalProperties:
+ // type: object
+ // properties:
+ // code:
+ // type: integer
+ // text:
+ // type: string
+ Yaml yaml = new Yaml();
+ Map model = yaml.load(document);
+ Object additionalProperties = query(model, "components.schemas.item.additionalProperties", Object.class);
+
+ assertThat(additionalProperties, is(instanceOf(Map.class)));
+ }
+
+ @Test
+ void checkWritingBooleanAdditionalProperties() {
+ OpenAPI openAPI = parse("/withBooleanAddlProps.yml");
+ String document = format(openAPI);
+
+ assertThat(document, containsString("additionalProperties: false"));
+ }
+
+ private static String format(OpenAPI model) {
+ StringWriter sw = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), model, OpenApiFormat.YAML, sw);
+ return sw.toString();
+ }
+
+ private static OpenAPI parse(String path) {
+ String document = resource(path);
+ return OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, new StringReader(document));
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java
index 274ff47fef5..3b1dee079df 100644
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/BasicServerTest.java
@@ -17,10 +17,9 @@
import java.util.Map;
-import io.helidon.http.Status;
+import io.helidon.common.media.type.MediaTypes;
import io.helidon.microprofile.testing.junit5.AddBean;
import io.helidon.microprofile.testing.junit5.HelidonTest;
-import io.helidon.openapi.OpenApiFeature;
import jakarta.inject.Inject;
import jakarta.ws.rs.client.WebTarget;
@@ -33,63 +32,37 @@
import static org.hamcrest.Matchers.is;
/**
- * Test that MP OpenAPI support works when retrieving the OpenAPI document
- * from the server's /openapi endpoint.
+ * Test model from annotations.
*/
@HelidonTest
@AddBean(TestApp.class)
@AddBean(TestApp3.class)
-public class BasicServerTest {
+class BasicServerTest {
- private static Map yaml;
+ private static final String APPLICATION_OPENAPI_YAML = MediaTypes.APPLICATION_OPENAPI_YAML.text();
@Inject
- WebTarget webTarget;
+ private WebTarget webTarget;
- private static Map retrieveYaml(WebTarget webTarget) {
- try (Response response = webTarget
- .path(OpenApiFeature.DEFAULT_CONTEXT)
- .request(OpenApiFeature.DEFAULT_RESPONSE_MEDIA_TYPE.text())
- .get()) {
- assertThat("Fetch of OpenAPI document from server status", response.getStatus(),
- is(equalTo(Status.OK_200.code())));
- String yamlText = response.readEntity(String.class);
- return new Yaml().load(yamlText);
- }
- }
-
- private static Map yaml(WebTarget webTarget) {
- if (yaml == null) {
- yaml = retrieveYaml(webTarget);
- }
- return yaml;
- }
-
- private Map yaml() {
- return yaml(webTarget);
- }
-
- public BasicServerTest() {
- }
-
- /**
- * Make sure that the annotations in the test app were found and properly
- * incorporated into the OpenAPI document.
- *
- * @throws Exception in case of errors reading the HTTP response
- */
@Test
- public void simpleTest() throws Exception {
- checkPathValue("paths./testapp/go.get.summary", TestApp.GO_SUMMARY);
+ public void simpleTest() {
+ Map document = document();
+ String summary = TestUtil.query(document, "paths./testapp/go.get.summary", String.class);
+ assertThat(summary, is(equalTo(TestApp.GO_SUMMARY)));
}
@Test
public void testMultipleApps() {
- checkPathValue("paths./testapp3/go3.get.summary", TestApp3.GO_SUMMARY);
+ Map document = document();
+ String summary = TestUtil.query(document, "paths./testapp3/go3.get.summary", String.class);
+ assertThat(summary, is(equalTo(TestApp3.GO_SUMMARY)));
}
- private void checkPathValue(String pathExpression, String expected) {
- String result = TestUtil.fromYaml(yaml(), pathExpression, String.class);
- assertThat(pathExpression, result, is(equalTo(expected)));
+ private Map document() {
+ try (Response response = webTarget.path("/openapi").request(APPLICATION_OPENAPI_YAML).get()) {
+ assertThat(response.getStatus(), is(200));
+ String yamlText = response.readEntity(String.class);
+ return new Yaml().load(yamlText);
+ }
}
}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilderTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilderTest.java
new file mode 100644
index 00000000000..47c5e105ee1
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/FilteredIndexViewsBuilderTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.util.List;
+import java.util.Set;
+
+import io.helidon.microprofile.openapi.other.TestApp2;
+import io.helidon.microprofile.server.JaxRsApplication;
+
+import io.smallrye.openapi.runtime.scanner.FilteredIndexView;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.microprofile.openapi.TestUtil.config;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Tests {@link FilteredIndexViewsBuilder}.
+ */
+class FilteredIndexViewsBuilderTest {
+
+ @Test
+ void testMultipleIndexFiles() {
+
+ // The pom builds two differently-named test Jandex files, as an approximation
+ // to handling multiple same-named index files in the class path.
+
+ List indexPaths = List.of("META-INF/jandex.idx", "META-INF/other.idx");
+
+ List apps = List.of(
+ JaxRsApplication.create(new TestApp()),
+ JaxRsApplication.create(new TestApp2()));
+
+ List indexViews = new FilteredIndexViewsBuilder(
+ config(), apps, Set.of(), indexPaths, false).buildViews();
+
+ List filteredIndexViews = indexViews.stream()
+ .flatMap(view -> view.getKnownClasses().stream())
+ .toList();
+
+ DotName testAppName = DotName.createSimple(TestApp.class.getName());
+ DotName testApp2Name = DotName.createSimple(TestApp2.class.getName());
+
+ ClassInfo testAppInfo = filteredIndexViews.stream()
+ .filter(classInfo -> classInfo.name().equals(testAppName))
+ .findFirst()
+ .orElse(null);
+ assertThat(testAppInfo, notNullValue());
+
+ ClassInfo testApp2Info = filteredIndexViews.stream()
+ .filter(classInfo -> classInfo.name().equals(testApp2Name))
+ .findFirst()
+ .orElse(null);
+ assertThat(testApp2Info, notNullValue());
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiConfigTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiConfigTest.java
new file mode 100644
index 00000000000..ff8a70918a2
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiConfigTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.util.Map;
+import java.util.StringJoiner;
+
+import io.smallrye.openapi.api.OpenApiConfig;
+import io.smallrye.openapi.api.OpenApiConfigImpl;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.microprofile.openapi.TestUtil.config;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.is;
+
+/**
+ * Tests {@link io.smallrye.openapi.api.OpenApiConfig}.
+ */
+class OpenApiConfigTest {
+
+ private static final Map SCHEMA_OVERRIDE_VALUES = Map.of(
+ "name", "EpochMillis",
+ "type", "number",
+ "format", "int64",
+ "description", "Milliseconds since January 1, 1970, 00:00:00 GMT");
+
+ private static final String SCHEMA_OVERRIDE_JSON = prepareSchemaOverrideJSON();
+
+ private static final String SCHEMA_OVERRIDE_CONFIG_FQCN = "java.util.Date";
+
+ private static final Map SIMPLE_CONFIG = Map.of(
+ "mp.openapi.model.reader", "io.helidon.microprofile.openapi.test.MyModelReader",
+ "mp.openapi.filter", "io.helidon.microprofile.openapi.test.MySimpleFilter",
+ "mp.openapi.servers", "s1,s2",
+ "mp.openapi.servers.path.path1", "p1s1,p1s2",
+ "mp.openapi.servers.path.path2", "p2s1,p2s2",
+ "mp.openapi.servers.operation.op1", "o1s1,o1s2",
+ "mp.openapi.servers.operation.op2", "o2s1,o2s2",
+ "mp.openapi.scan.disable", "true"
+ );
+
+ private static final Map SCHEMA_OVERRIDE_CONFIG = Map.of(
+ "mp.openapi.schema." + SCHEMA_OVERRIDE_CONFIG_FQCN, SCHEMA_OVERRIDE_JSON
+ );
+
+ private static String prepareSchemaOverrideJSON() {
+ StringJoiner sj = new StringJoiner(",\n", "{\n", "\n}");
+ SCHEMA_OVERRIDE_VALUES.forEach((key, value) -> sj.add("\"" + key + "\": \"" + value + "\""));
+ return sj.toString();
+ }
+
+ @Test
+ public void simpleConfigTest() {
+ OpenApiConfig openApiConfig = openApiConfig(SIMPLE_CONFIG);
+
+ assertThat(openApiConfig.modelReader(), is("io.helidon.microprofile.openapi.test.MyModelReader"));
+ assertThat(openApiConfig.filter(), is("io.helidon.microprofile.openapi.test.MySimpleFilter"));
+ assertThat(openApiConfig.scanDisable(), is(true));
+ assertThat(openApiConfig.servers(), containsInAnyOrder("s1", "s2"));
+ assertThat(openApiConfig.pathServers("path1"), containsInAnyOrder("p1s1", "p1s2"));
+ assertThat(openApiConfig.pathServers("path2"), containsInAnyOrder("p2s1", "p2s2"));
+ }
+
+ @Test
+ void checkSchemaConfig() {
+ OpenApiConfig openApiConfig = openApiConfig(SIMPLE_CONFIG, SCHEMA_OVERRIDE_CONFIG);
+ Map schemas = openApiConfig.getSchemas();
+
+ assertThat(schemas, hasKey(SCHEMA_OVERRIDE_CONFIG_FQCN));
+ assertThat(schemas.get(SCHEMA_OVERRIDE_CONFIG_FQCN), is(SCHEMA_OVERRIDE_JSON));
+ }
+
+ @SafeVarargs
+ private static OpenApiConfig openApiConfig(Map... configSources) {
+ return new OpenApiConfigImpl(config(configSources));
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiParserTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiParserTest.java
new file mode 100644
index 00000000000..31f89d32154
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiParserTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.StringReader;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.eclipse.microprofile.openapi.models.Paths;
+import org.eclipse.microprofile.openapi.models.parameters.Parameter;
+import org.eclipse.microprofile.openapi.models.servers.Server;
+import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.common.testing.junit5.OptionalMatcher.optionalValue;
+import static io.helidon.microprofile.openapi.TestUtil.resource;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Tests {@link OpenApiParser}.
+ */
+class OpenApiParserTest {
+
+ @Test
+ void testParserUsingYAML() {
+ OpenAPI openAPI = parse("/petstore.yaml");
+ assertThat(openAPI.getOpenapi(), is("3.0.0"));
+ assertThat(openAPI.getPaths().getPathItem("/pets").getGET().getParameters().get(0).getIn(),
+ is(Parameter.In.QUERY));
+ }
+
+ @Test
+ void testExtensions() {
+ OpenAPI openAPI = parse("/openapi-greeting.yml");
+ Object xMyPersonalMap = openAPI.getExtensions().get("x-my-personal-map");
+ assertThat(xMyPersonalMap, is(instanceOf(Map.class)));
+ Map,?> map = (Map,?>) xMyPersonalMap;
+ Object owner = map.get("owner");
+ Object value1 = map.get("value-1");
+ assertThat(value1, is(instanceOf(Double.class)));
+ Double d = (Double) value1;
+ assertThat(d, equalTo(2.3));
+
+ assertThat(owner, is(instanceOf(Map.class)));
+ map = (Map,?>) owner;
+ assertThat(map.get("first"), equalTo("Me"));
+ assertThat(map.get("last"), equalTo("Myself"));
+
+ Object xBoolean = openAPI.getExtensions().get("x-boolean");
+ assertThat(xBoolean, is(instanceOf(Boolean.class)));
+ Boolean b = (Boolean) xBoolean;
+ assertThat(b, is(true));
+
+ Object xInt = openAPI.getExtensions().get("x-int");
+ assertThat(xInt, is(instanceOf(Integer.class)));
+ Integer i = (Integer) xInt;
+ assertThat(i, is(117));
+
+ Object xStrings = openAPI.getExtensions().get("x-string-array");
+ assertThat(xStrings, is(instanceOf(List.class)));
+ List> list = (List>) xStrings;
+ Object first = list.get(0);
+ assertThat(first, is(instanceOf(String.class)));
+ String f = (String) first;
+ assertThat(f, is(equalTo("one")));
+ }
+
+
+ @Test
+ void testYamlRef() {
+ OpenAPI openAPI = parse("/petstore.yaml");
+ Paths paths = openAPI.getPaths();
+ String ref = paths.getPathItem("/pets")
+ .getGET()
+ .getResponses()
+ .getAPIResponse("200")
+ .getContent()
+ .getMediaType("application/json")
+ .getSchema()
+ .getRef();
+
+ assertThat("ref value", ref, is(equalTo("#/components/schemas/Pets")));
+ }
+
+ @Test
+ void testJsonRef() {
+ OpenAPI openAPI = parse("/petstore.json");
+ Paths paths = openAPI.getPaths();
+ String ref = paths.getPathItem("/user")
+ .getPOST()
+ .getRequestBody()
+ .getContent()
+ .getMediaType("application/json")
+ .getSchema()
+ .getRef();
+
+ assertThat("ref value", ref, is(equalTo("#/components/schemas/User")));
+ }
+
+ @Test
+ void testParserUsingJSON() {
+ OpenAPI openAPI = parse("/petstore.json");
+ assertThat(openAPI.getOpenapi(), is("3.0.0"));
+
+ // TODO - uncomment the following once full $ref support is in place
+ // assertThat(openAPI.getPaths().getPathItem("/pet").getPUT().getRequestBody().getDescription(),
+ // containsString("needs to be added to the store"));
+ }
+
+ @Test
+ @SuppressWarnings("HttpUrlsUsage")
+ void testComplicatedPetstoreDocument() {
+ OpenAPI openAPI = parse("/petstore-with-fake-endpoints-models.yaml");
+ assertThat(openAPI.getOpenapi(), is("3.0.0"));
+ assertThat("Default for server variable 'port'",
+ openAPI.getPaths()
+ .getPathItem("/pet")
+ .getServers()
+ .stream()
+ .filter(server -> server.getUrl().equals("http://{server}.swagger.io:{port}/v2"))
+ .map(Server::getVariables)
+ .map(map -> map.get("server"))
+ .map(ServerVariable::getDefaultValue)
+ .findFirst(),
+ optionalValue(is("petstore")));
+ }
+
+ private static OpenAPI parse(String path) {
+ String document = resource(path);
+ return OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, new StringReader(document));
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiSerializerTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiSerializerTest.java
new file mode 100644
index 00000000000..624bb70af15
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/OpenApiSerializerTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.IOException;
+import java.io.LineNumberReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import io.helidon.openapi.OpenApiFormat;
+
+import jakarta.json.Json;
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonStructure;
+import jakarta.json.JsonValue;
+import org.eclipse.microprofile.openapi.models.OpenAPI;
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.microprofile.openapi.TestUtil.resource;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+class OpenApiSerializerTest {
+
+ @Test
+ public void testJSONSerialization() {
+ OpenAPI openAPI = parse("/openapi-greeting.yml");
+ Writer writer = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), openAPI, OpenApiFormat.JSON, writer);
+
+ JsonStructure json = readJson(writer.toString());
+
+ assertThat(json.getValue("/x-my-personal-map/owner/last").toString(), is("\"Myself\""));
+ JsonValue otherItem = json.getValue("/x-other-item");
+ assertThat(otherItem.getValueType(), is(JsonValue.ValueType.NUMBER));
+ assertThat(Double.valueOf(otherItem.toString()), is(10.0));
+
+ JsonValue seq = json.getValue("/info/x-my-personal-seq");
+ assertThat(seq.getValueType(), is(JsonValue.ValueType.ARRAY));
+ JsonArray seqArray = seq.asJsonArray();
+ JsonValue first = seqArray.get(0);
+ assertThat(first.getValueType(), is(JsonValue.ValueType.OBJECT));
+ JsonObject firstObj = first.asJsonObject();
+ checkJsonPathStringValue(firstObj, "/who", "Prof. Plum");
+ checkJsonPathStringValue(firstObj, "/why", "felt like it");
+
+ JsonValue second = seqArray.get(1);
+ assertThat(second.getValueType(), is(JsonValue.ValueType.OBJECT));
+ JsonObject secondObj = second.asJsonObject();
+ checkJsonPathStringValue(secondObj, "/when", "yesterday");
+ checkJsonPathStringValue(secondObj, "/how", "with the lead pipe");
+
+ JsonValue xInt = json.getValue("/x-int");
+ assertThat(xInt.getValueType(), is(JsonValue.ValueType.NUMBER));
+ assertThat(Integer.valueOf(xInt.toString()), is(117));
+
+ JsonValue xBoolean = json.getValue("/x-boolean");
+ assertThat(xBoolean.getValueType(), is(JsonValue.ValueType.TRUE));
+
+ JsonValue xStrings = json.getValue("/x-string-array");
+ assertThat(xStrings.getValueType(), is(JsonValue.ValueType.ARRAY));
+ JsonArray xStringArray = xStrings.asJsonArray();
+ assertThat(xStringArray.size(), is(2));
+ checkJsonStringValue(xStringArray.get(0), "one");
+ checkJsonStringValue(xStringArray.get(1), "two");
+
+ JsonValue xObjects = json.getValue("/x-object-array");
+ assertThat(xObjects.getValueType(), is(JsonValue.ValueType.ARRAY));
+ JsonArray xObjectArray = xObjects.asJsonArray();
+ assertThat(xObjectArray.size(), is(2));
+ first = xObjectArray.get(0);
+ assertThat(first.getValueType(), is(JsonValue.ValueType.OBJECT));
+ firstObj = first.asJsonObject();
+ checkJsonPathStringValue(firstObj, "/name", "item-1");
+ checkJsonPathIntValue(firstObj, "/value", 16);
+ second = xObjectArray.get(1);
+ assertThat(second.getValueType(), is(JsonValue.ValueType.OBJECT));
+ secondObj = second.asJsonObject();
+ checkJsonPathStringValue(secondObj, "/name", "item-2");
+ checkJsonPathIntValue(secondObj, "/value", 18);
+
+ }
+
+ @Test
+ public void testYAMLSerialization() throws IOException {
+ OpenAPI openAPI = parse("/openapi-greeting.yml");
+ Writer writer = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), openAPI, OpenApiFormat.YAML, writer);
+ try (Reader reader = new StringReader(writer.toString())) {
+ openAPI = OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, reader);
+ }
+ Object candidateMap = openAPI.getExtensions()
+ .get("x-my-personal-map");
+ assertThat(candidateMap, is(instanceOf(Map.class)));
+
+ Map, ?> map = (Map, ?>) candidateMap;
+ Object candidateOwnerMap = map.get("owner");
+ assertThat(candidateOwnerMap, is(instanceOf(Map.class)));
+
+ Map, ?> ownerMap = (Map, ?>) candidateOwnerMap;
+ assertThat(ownerMap.get("last"), is("Myself"));
+
+ List required = openAPI.getPaths().getPathItem("/greet/greeting")
+ .getPUT()
+ .getRequestBody()
+ .getContent()
+ .getMediaType("application/json")
+ .getSchema()
+ .getRequired();
+ assertThat(required, hasItem("greeting"));
+ }
+
+ @Test
+ void testRefSerializationAsOpenAPI() throws IOException {
+ OpenAPI openAPI = parse("/petstore.yaml");
+ Writer writer = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), openAPI, OpenApiFormat.YAML, writer);
+
+ try (Reader reader = new StringReader(writer.toString())) {
+ openAPI = OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, reader);
+ }
+
+ String ref = openAPI.getPaths()
+ .getPathItem("/pets")
+ .getGET()
+ .getResponses()
+ .getDefaultValue()
+ .getContent()
+ .getMediaType("application/json")
+ .getSchema()
+ .getRef();
+
+ assertThat(ref, is(equalTo("#/components/schemas/Error")));
+ }
+
+ @Test
+ void testRefSerializationAsText() throws IOException {
+ // This test basically replicates the other ref test but without parsing again, just in case there might be
+ // compensating bugs in the parsing and the serialization.
+ Pattern refPattern = Pattern.compile("\\s\\$ref: '([^']+)");
+
+ OpenAPI openAPI = parse("/petstore.yaml");
+ Writer writer = new StringWriter();
+ OpenApiSerializer.serialize(OpenApiHelper.types(), openAPI, OpenApiFormat.YAML, writer);
+
+ try (LineNumberReader reader = new LineNumberReader(new StringReader(writer.toString()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher refMatcher = refPattern.matcher(line);
+ if (refMatcher.matches()) {
+ assertThat(refMatcher.group(1), startsWith("#/components"));
+ }
+ }
+ }
+ }
+
+ private static void checkJsonPathStringValue(JsonObject jsonObject, String pointer, String expected) {
+ checkJsonStringValue(jsonObject.getValue(pointer), expected);
+ }
+
+ private static void checkJsonStringValue(JsonValue jsonValue, String expected) {
+ assertThat(jsonValue.getValueType(), is(JsonValue.ValueType.STRING));
+ assertThat(jsonValue.toString(), is("\"" + expected + "\""));
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static void checkJsonPathIntValue(JsonObject jsonObject, String pointer, int expected) {
+ checkJsonIntValue(jsonObject.getValue(pointer), expected);
+ }
+
+ private static void checkJsonIntValue(JsonValue val, int expected) {
+ assertThat(val.getValueType(), is(JsonValue.ValueType.NUMBER));
+ assertThat(Integer.valueOf(val.toString()), is(expected));
+ }
+
+ private static JsonStructure readJson(String str) {
+ try (JsonReader jsonReader = Json.createReader(new StringReader(str))) {
+ return jsonReader.read();
+ }
+ }
+
+ private static OpenAPI parse(String path) {
+ String document = resource(path);
+ return OpenApiParser.parse(OpenApiHelper.types(), OpenAPI.class, new StringReader(document));
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerConfigTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerConfigTest.java
new file mode 100644
index 00000000000..00582a363f6
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerConfigTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.util.Map;
+
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.microprofile.testing.junit5.AddBean;
+import io.helidon.microprofile.testing.junit5.AddConfig;
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+import org.junit.jupiter.api.Test;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@HelidonTest
+@AddConfig(key = "openapi.web-context", value = "/alt-openapi")
+@AddBean(TestApp.class)
+class ServerConfigTest {
+
+ private static final String APPLICATION_OPENAPI_YAML = MediaTypes.APPLICATION_OPENAPI_YAML.text();
+
+ @Inject
+ private WebTarget webTarget;
+
+ @Test
+ public void testAlternatePath() {
+ Map document = document();
+ String summary = TestUtil.query(document, "paths./testapp/go.get.summary", String.class);
+ assertThat(summary, is(TestApp.GO_SUMMARY));
+ }
+
+ private Map document() {
+ try (Response response = webTarget.path("/alt-openapi").request(APPLICATION_OPENAPI_YAML).get()) {
+ assertThat(response.getStatus(), is(200));
+ String yamlText = response.readEntity(String.class);
+ return new Yaml().load(yamlText);
+ }
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerModelReaderTest.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerModelReaderTest.java
new file mode 100644
index 00000000000..21bbea1ebe0
--- /dev/null
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/ServerModelReaderTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2019, 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.microprofile.openapi;
+
+import java.io.StringReader;
+
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.microprofile.testing.junit5.AddBean;
+import io.helidon.microprofile.testing.junit5.AddConfig;
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+import io.helidon.microprofile.openapi.test.MyModelReader;
+
+import jakarta.inject.Inject;
+import jakarta.json.Json;
+import jakarta.json.JsonException;
+import jakarta.json.JsonReader;
+import jakarta.json.JsonString;
+import jakarta.json.JsonStructure;
+import jakarta.json.JsonValue;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Makes sure that the app-supplied model reader participates in constructing
+ * the OpenAPI model.
+ */
+@HelidonTest
+@AddConfig(key = "mp.openapi.model.reader", value = "io.helidon.microprofile.openapi.test.MyModelReader")
+@AddConfig(key = "mp.openapi.filter", value = "io.helidon.microprofile.openapi.test.MySimpleFilter")
+@AddBean(TestApp.class)
+class ServerModelReaderTest {
+
+ private static final String APPLICATION_OPENAPI_JSON = MediaTypes.APPLICATION_OPENAPI_JSON.text();
+
+ @Inject
+ private WebTarget webTarget;
+
+ @Test
+ void checkCustomModelReader() {
+ try (Response response = webTarget.path("/openapi").request(APPLICATION_OPENAPI_JSON).get()) {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getMediaType().toString(), is(APPLICATION_OPENAPI_JSON));
+ String text = response.readEntity(String.class);
+ JsonStructure json = readJson(text);
+
+ // The model reader adds the following key/value (among others) to the model.
+ JsonValue v = json.getValue(String.format("/paths/%s/get/summary",
+ escapeJsonPointer(MyModelReader.MODEL_READER_PATH)));
+ assertThat(v.getValueType(), is(JsonValue.ValueType.STRING));
+ assertThat(((JsonString) v).getString(), is(MyModelReader.SUMMARY));
+ }
+ }
+
+ @Test
+ void makeSureFilteredPathIsMissing() {
+ try (Response response = webTarget.path("/openapi").request(APPLICATION_OPENAPI_JSON).get()) {
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getMediaType().toString(), is(APPLICATION_OPENAPI_JSON));
+ String text = response.readEntity(String.class);
+ JsonStructure json = readJson(text);
+
+ // Although the model reader adds this path, the filter should have removed it.
+ JsonException ex = assertThrows(JsonException.class, () ->
+ json.getValue(String.format("/paths/%s/get/summary", escapeJsonPointer(MyModelReader.DOOMED_PATH))));
+
+ assertThat(ex.getMessage(), containsString(
+ String.format("contains no mapping for the name '%s'", MyModelReader.DOOMED_PATH)));
+ }
+ }
+
+ private static JsonStructure readJson(String str) {
+ try (JsonReader jsonReader = Json.createReader(new StringReader(str))) {
+ return jsonReader.read();
+ }
+ }
+
+ private static String escapeJsonPointer(String pointer) {
+ return pointer.replaceAll("~", "~0").replaceAll("/", "~1");
+ }
+}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp.java
index e61dc798cdb..7f1896a8494 100644
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp.java
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp.java
@@ -30,13 +30,10 @@
import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
-/**
- * Test JAX-RS app for MP OpenAPI testing.
- */
-
-public class TestApp extends Application {
+class TestApp extends Application {
static final String GO_SUMMARY = "Returns a fixed string";
+
@Override
public Set> getClasses() {
return Set.of(TestResources.class);
@@ -48,10 +45,9 @@ public static class TestResources {
@Path("/go")
@GET
@Operation(summary = GO_SUMMARY,
- description = "Provides a single, fixed string as the response")
+ description = "Provides a single, fixed string as the response")
@APIResponse(description = "Simple text string",
- content = @Content(mediaType = "text/plain")
- )
+ content = @Content(mediaType = "text/plain"))
@Produces(MediaType.TEXT_PLAIN)
public Response go() {
return Response.ok("Test").build();
@@ -60,15 +56,12 @@ public Response go() {
@Path("/send")
@PUT
@Operation(summary = "Sends a simple string",
- description = "Permits the client to send a string to the server"
- )
+ description = "Permits the client to send a string to the server")
@RequestBody(
- name = "message",
- description = "Conveys the simple string message",
- content = @Content(
- mediaType = "text/plain"
- )
- )
+ name = "message",
+ description = "Conveys the simple string message",
+ content = @Content(mediaType = "text/plain")
+ )
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public Response send(String message) {
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp3.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp3.java
index b6a5ef2c0f1..f518b6435e3 100644
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp3.java
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestApp3.java
@@ -27,9 +27,10 @@
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
-public class TestApp3 extends Application {
+class TestApp3 extends Application {
static final String GO_SUMMARY = "Returns a fixed string 3";
+
@Override
public Set> getClasses() {
return Set.of(TestResources.class);
@@ -41,10 +42,9 @@ public static class TestResources {
@Path("/go3")
@GET
@Operation(summary = GO_SUMMARY,
- description = "Provides a single, fixed string as the response")
+ description = "Provides a single, fixed string as the response")
@APIResponse(description = "Simple text string",
- content = @Content(mediaType = "text/plain")
- )
+ content = @Content(mediaType = "text/plain"))
@Produces(MediaType.TEXT_PLAIN)
public Response go() {
return Response.ok("Test").build();
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java
deleted file mode 100644
index 1c92aaf8621..00000000000
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestMultiJandex.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
- *
- * Licensed 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 io.helidon.microprofile.openapi;
-
-import java.io.IOException;
-
-import io.helidon.microprofile.openapi.other.TestApp2;
-
-import org.jboss.jandex.ClassInfo;
-import org.jboss.jandex.DotName;
-import org.jboss.jandex.IndexView;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-@Disabled
-public class TestMultiJandex {
-
- @Test
- public void testMultipleIndexFiles() throws IOException {
-
- /*
- * The pom builds two differently-named test Jandex files, as an approximation
- * to handling multiple same-named index files in the class path.
- */
- OpenApiCdiExtension ext = new OpenApiCdiExtension("META-INF/jandex.idx", "META-INF/other.idx");
- IndexView indexView = ext.feature().indexView();
-
-
- DotName testAppName = DotName.createSimple(TestApp.class.getName());
- DotName testApp2Name = DotName.createSimple(TestApp2.class.getName());
-
- ClassInfo testAppInfo = indexView.getClassByName(testAppName);
- assertThat("Expected index entry for TestApp not found", testAppInfo, notNullValue());
-
- ClassInfo testApp2Info = indexView.getClassByName(testApp2Name);
- assertThat("Expected index entry for TestApp2 not found", testApp2Info, notNullValue());
- }
-}
\ No newline at end of file
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestServerWithConfig.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestServerWithConfig.java
deleted file mode 100644
index be666aa1e16..00000000000
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestServerWithConfig.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (c) 2020, 2023 Oracle and/or its affiliates.
- *
- * Licensed 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 io.helidon.microprofile.openapi;
-
-import java.net.HttpURLConnection;
-import java.util.Map;
-
-import io.helidon.http.HttpMediaType;
-import io.helidon.common.media.type.MediaTypes;
-import io.helidon.config.ClasspathConfigSource;
-import io.helidon.config.Config;
-import io.helidon.microprofile.server.Server;
-
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.CoreMatchers.is;
-
-class TestServerWithConfig {
-
- private static final String ALTERNATE_OPENAPI_PATH = "/otheropenapi";
-
- private static Server server;
-
- private static HttpURLConnection cnx;
-
- private static Map yaml;
-
- public TestServerWithConfig() {
- }
-
- @BeforeAll
- public static void startServer() throws Exception {
- Config helidonConfig = Config.builder().addSource(ClasspathConfigSource.create("/serverConfig.yml")).build();
- server = TestUtil.startServer(helidonConfig, TestApp.class);
- cnx = TestUtil.getURLConnection(
- server.port(),
- "GET",
- ALTERNATE_OPENAPI_PATH,
- HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML));
- yaml = TestUtil.yamlFromResponse(cnx);
- }
-
- @AfterAll
- public static void stopServer() {
- TestUtil.cleanup(server, cnx);
- }
-
- @Test
- public void testAlternatePath() throws Exception {
- String goSummary = TestUtil.fromYaml(yaml, "paths./testapp/go.get.summary", String.class);
- assertThat(goSummary, is(TestApp.GO_SUMMARY));
- }
-}
diff --git a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java
index 103dd4889e4..e204d3f09c5 100644
--- a/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java
+++ b/microprofile/openapi/src/test/java/io/helidon/microprofile/openapi/TestUtil.java
@@ -17,191 +17,85 @@
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.CharBuffer;
-import java.nio.charset.Charset;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
import java.util.Map;
-import io.helidon.http.HttpMediaType;
-import io.helidon.common.media.type.MediaTypes;
-import io.helidon.config.Config;
-import io.helidon.microprofile.server.Server;
+import io.helidon.config.mp.MpConfigSources;
-import jakarta.ws.rs.core.Application;
-import org.yaml.snakeyaml.Yaml;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.jupiter.api.Assertions.fail;
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
+import org.eclipse.microprofile.config.spi.ConfigSource;
/**
- * Useful utility methods during testing.
+ * Utility to query a tree structure using a dotted path notation.
*/
-public class TestUtil {
+class TestUtil {
- /**
- * Starts the MP server running the specific application.
- *
- * The server will use the default MP config.
- *
- * @param helidonConfig the Helidon configuration to use in preparing the server
- * @param appClasses application classes to serve
- * @return the started MP {@code Server} instance
- */
- public static Server startServer(Config helidonConfig, Class extends Application>... appClasses) {
- Server.Builder builder = Server.builder()
- .port(0)
- .config(helidonConfig);
- for (Class extends Application> appClass : appClasses) {
- builder.addApplication(appClass);
- }
- return builder
- .build()
- .start();
+ private TestUtil() {
}
/**
- * Cleans up, stopping the server and disconnecting the connection.
- *
- * @param server the {@code Server} to stop
- * @param cnx the connection to disconnect
+ * Get a class-path resource.
+ * @param path resource path
+ * @return resource content as a string
*/
- public static void cleanup(Server server, HttpURLConnection cnx) {
- if (cnx != null) {
- cnx.disconnect();
- }
- if (server != null) {
- server.stop();
- }
- }
-
- /**
- * Returns a {@code HttpURLConnection} for the requested method and path and
- * {code @MediaType} from the specified location.
- *
- * @param port port to connect to
- * @param method HTTP method to use in building the connection
- * @param path path to the resource in the web server
- * @param mediaType {@code MediaType} to be Accepted
- * @return the connection to the server and path
- * @throws Exception in case of errors creating the connection
- */
- public static HttpURLConnection getURLConnection(
- int port,
- String method,
- String path,
- HttpMediaType mediaType) throws Exception {
- URL url = new URL("http://localhost:" + port + path);
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setRequestMethod(method);
- if (mediaType != null) {
- conn.setRequestProperty("Accept", mediaType.text());
- }
- System.out.println("Connecting: " + method + " " + url);
- return conn;
- }
-
- /**
- * Returns the {@code MediaType} instance conforming to the HTTP response
- * content type.
- *
- * @param cnx the HttpURLConnection from which to get the content type
- * @return the MediaType corresponding to the content type in the response
- */
- public static HttpMediaType mediaTypeFromResponse(HttpURLConnection cnx) {
- HttpMediaType returnedMediaType = HttpMediaType.create(cnx.getContentType());
- if (returnedMediaType.charset().isEmpty()) {
- return returnedMediaType.withCharset(Charset.defaultCharset().name());
+ static String resource(String path) {
+ try (InputStream is = TestUtil.class.getResourceAsStream(path)) {
+ if (is == null) {
+ throw new IllegalArgumentException("Resource not found: " + path);
+ }
+ return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
}
- return returnedMediaType;
- }
-
- /**
- * Represents the HTTP response payload as a String.
- *
- * @param cnx the HttpURLConnection from which to get the response payload
- * @return String representation of the OpenAPI document as a String
- * @throws IOException in case of errors reading the HTTP response payload
- */
- public static String stringYAMLFromResponse(HttpURLConnection cnx) throws IOException {
- HttpMediaType returnedMediaType = mediaTypeFromResponse(cnx);
- assertThat("Unexpected returned media type",
- HttpMediaType.create(MediaTypes.APPLICATION_OPENAPI_YAML).test(returnedMediaType), is(true));
- return stringFromResponse(cnx, returnedMediaType);
}
/**
- * Returns a {@code String} resulting from interpreting the response payload
- * in the specified connection according to the expected {@code MediaType}.
+ * Create a new instance of {@link Config} with the given maps as config sources.
*
- * @param cnx {@code HttpURLConnection} with the response
- * @param mediaType {@code MediaType} to use in interpreting the response
- * payload
- * @return {@code String} of the payload interpreted according to the
- * specified {@code MediaType}
- * @throws IOException in case of errors reading the response payload
+ * @param configSources config sources
+ * @return config
*/
- public static String stringFromResponse(HttpURLConnection cnx, HttpMediaType mediaType) throws IOException {
- try (final InputStreamReader isr = new InputStreamReader(
- cnx.getInputStream(), mediaType.charset().get())) {
- StringBuilder sb = new StringBuilder();
- CharBuffer cb = CharBuffer.allocate(1024);
- while (isr.read(cb) != -1) {
- cb.flip();
- sb.append(cb);
- }
- return sb.toString();
- }
+ @SafeVarargs
+ static Config config(Map... configSources) {
+ return ConfigProviderResolver.instance()
+ .getBuilder()
+ .withSources(configSources(configSources))
+ .build();
}
- /**
- * Returns the response payload from the specified connection as a snakeyaml
- * {@code Yaml} object.
- *
- * @param cnx the {@code HttpURLConnection} containing the response
- * @return the YAML {@code Map} (created by snakeyaml) from
- * the HTTP response payload
- * @throws IOException in case of errors reading the response
- */
- @SuppressWarnings(value = "unchecked")
- public static Map yamlFromResponse(HttpURLConnection cnx) throws IOException {
- HttpMediaType returnedMediaType = mediaTypeFromResponse(cnx);
- Yaml yaml = new Yaml();
- Charset cs = Charset.defaultCharset();
- if (returnedMediaType.charset().isPresent()) {
- cs = Charset.forName(returnedMediaType.charset().get());
- }
- try (InputStream is = cnx.getInputStream(); InputStreamReader isr = new InputStreamReader(is, cs)) {
- return (Map) yaml.load(isr);
- }
+ @SafeVarargs
+ private static ConfigSource[] configSources(Map... configSources) {
+ return Arrays.stream(configSources)
+ .map(MpConfigSources::create)
+ .toArray(ConfigSource[]::new);
}
/**
- * Treats the provided {@code Map} as a YAML map and navigates through it
+ * Treats the provided {@code Map} as a tree and navigates through it
* using the dotted-name convention as expressed in the {@code dottedPath}
* argument, finally casting the value retrieved from the last segment of
* the path as the specified type and returning that cast value.
*
- * @param type to which the final value will be cast
- * @param map the YAML-inspired map
- * @param dottedPath navigation path to the item of interest in the YAML
- * maps-of-maps; note that the {@code dottedPath} must not use dots except
- * as path segment separators
- * @param cl {@code Class} for the return type {@code }
- * @return value from the lowest-level map retrieved using the last path
- * segment, cast to the specified type
+ * @param type to which the final value will be cast
+ * @param map the tree
+ * @param dottedPath navigation path to the item of interest ;
+ * note that the {@code dottedPath} must not use dots except as path segment separators
+ * @param cl {@code Class} for the return type {@code }
+ * @return value from the lowest-level map retrieved using the last path segment, cast to the specified type
*/
@SuppressWarnings(value = "unchecked")
- public static T fromYaml(Map map, String dottedPath, Class cl) {
+ public static T query(Map map, String dottedPath, Class cl) {
Map originalMap = map;
String[] segments = dottedPath.split("\\.");
for (int i = 0; i < segments.length - 1; i++) {
map = (Map) map.get(segments[i]);
if (map == null) {
- fail("Traversing dotted path " + dottedPath + " segment " + segments[i] + " not found in parsed map "
- + originalMap);
+ throw new AssertionError(String.format(
+ "Traversing dotted path %s segment %s not found in parsed map %s",
+ dottedPath, segments[i], originalMap));
}
}
return cl.cast(map.get(segments[segments.length - 1]));
diff --git a/microprofile/openapi/src/test/resources/petstore-with-fake-endpoints-models.yaml b/microprofile/openapi/src/test/resources/petstore-with-fake-endpoints-models.yaml
new file mode 100644
index 00000000000..b786185e654
--- /dev/null
+++ b/microprofile/openapi/src/test/resources/petstore-with-fake-endpoints-models.yaml
@@ -0,0 +1,1956 @@
+#
+# Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+#
+# Licensed 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.
+#
+
+# Inspired by the corresponding file in the OpenAPITools generator.
+openapi: 3.0.0
+info:
+ description: >-
+ This spec is mainly for testing Petstore server and contains fake endpoints,
+ models. Please do not use this for any other purpose. Special characters: "
+ \
+ version: 1.0.0
+ title: OpenAPI Petstore
+ license:
+ name: Apache-2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
+tags:
+ - name: pet
+ description: Everything about your Pets
+ - name: store
+ description: Access to Petstore orders
+ - name: user
+ description: Operations about user
+paths:
+ /foo:
+ get:
+ responses:
+ default:
+ description: response
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ string:
+ $ref: '#/components/schemas/Foo'
+ /pet:
+ servers:
+ - url: 'http://petstore.swagger.io/v2'
+ - url: 'http://path-server-test.petstore.local/v2'
+ - url: 'http://{server}.swagger.io:{port}/v2'
+ description: test server with variables
+ variables:
+ server:
+ description: target server
+ enum:
+ - 'petstore'
+ - 'qa-petstore'
+ - 'dev-petstore'
+ default: 'petstore'
+ port:
+ enum:
+ - 80
+ - 8080
+ default: 80
+ post:
+ tags:
+ - pet
+ summary: Add a new pet to the store
+ description: ''
+ operationId: addPet
+ responses:
+ '200':
+ description: Successful operation
+ '405':
+ description: Invalid input
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ put:
+ tags:
+ - pet
+ summary: Update an existing pet
+ description: ''
+ operationId: updatePet
+ x-webclient-blocking: true
+ responses:
+ '200':
+ description: Successful operation
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ '405':
+ description: Validation exception
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ /pet/findByStatus:
+ get:
+ tags:
+ - pet
+ summary: Finds Pets by status
+ description: Multiple status values can be provided with comma separated strings
+ operationId: findPetsByStatus
+ x-webclient-blocking: true
+ parameters:
+ - name: status
+ in: query
+ description: Status values that need to be considered for filter
+ required: true
+ style: form
+ explode: false
+ deprecated: true
+ schema:
+ type: array
+ items:
+ type: string
+ enum:
+ - available
+ - pending
+ - sold
+ default: available
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid status value
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ /pet/findByTags:
+ get:
+ tags:
+ - pet
+ summary: Finds Pets by tags
+ description: >-
+ Multiple tags can be provided with comma separated strings. Use tag1,
+ tag2, tag3 for testing.
+ operationId: findPetsByTags
+ x-webclient-blocking: true
+ parameters:
+ - name: tags
+ in: query
+ description: Tags to filter by
+ required: true
+ style: form
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ uniqueItems: true
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ uniqueItems: true
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/Pet'
+ uniqueItems: true
+ '400':
+ description: Invalid tag value
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ deprecated: true
+ '/pet/{petId}':
+ get:
+ tags:
+ - pet
+ summary: Find pet by ID
+ description: Returns a single pet
+ operationId: getPetById
+ x-webclient-blocking: true
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to return
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Pet not found
+ security:
+ - api_key: []
+ post:
+ tags:
+ - pet
+ summary: Updates a pet in the store with form data
+ description: ''
+ operationId: updatePetWithForm
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet that needs to be updated
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Successful operation
+ '405':
+ description: Invalid input
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ name:
+ description: Updated name of the pet
+ type: string
+ status:
+ description: Updated status of the pet
+ type: string
+ delete:
+ tags:
+ - pet
+ summary: Deletes a pet
+ description: ''
+ operationId: deletePet
+ parameters:
+ - name: api_key
+ in: header
+ required: false
+ schema:
+ type: string
+ - name: petId
+ in: path
+ description: Pet id to delete
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Successful operation
+ '400':
+ description: Invalid pet value
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ '/pet/{petId}/uploadImage':
+ post:
+ tags:
+ - pet
+ summary: uploads an image
+ description: ''
+ operationId: uploadFile
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to update
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiResponse'
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ additionalMetadata:
+ description: Additional data to pass to server
+ type: string
+ file:
+ description: file to upload
+ type: string
+ format: binary
+ /store/inventory:
+ get:
+ tags:
+ - store
+ summary: Returns pet inventories by status
+ description: Returns a map of status codes to quantities
+ operationId: getInventory
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: integer
+ format: int32
+ security:
+ - api_key: []
+ /store/order:
+ post:
+ tags:
+ - store
+ summary: Place an order for a pet
+ description: ''
+ operationId: placeOrder
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Order'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ '400':
+ description: Invalid Order
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ description: order placed for purchasing the pet
+ required: true
+ '/store/order/{order_id}':
+ get:
+ tags:
+ - store
+ summary: Find purchase order by ID
+ description: >-
+ For valid response try integer IDs with value <= 5 or > 10. Other values
+ will generated exceptions
+ operationId: getOrderById
+ parameters:
+ - name: order_id
+ in: path
+ description: ID of pet that needs to be fetched
+ required: true
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ maximum: 5
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Order'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Order'
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Order not found
+ delete:
+ tags:
+ - store
+ summary: Delete purchase order by ID
+ description: >-
+ For valid response try integer IDs with value < 1000. Anything above
+ 1000 or nonintegers will generate API errors
+ operationId: deleteOrder
+ parameters:
+ - name: order_id
+ in: path
+ description: ID of the order that needs to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid ID supplied
+ '404':
+ description: Order not found
+ /user:
+ post:
+ tags:
+ - user
+ summary: Create user
+ description: This can only be done by the logged in user.
+ operationId: createUser
+ responses:
+ default:
+ description: successful operation
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ description: Created user object
+ required: true
+ /user/createWithArray:
+ post:
+ tags:
+ - user
+ summary: Creates list of users with given input array
+ description: ''
+ operationId: createUsersWithArrayInput
+ responses:
+ default:
+ description: successful operation
+ requestBody:
+ $ref: '#/components/requestBodies/UserArray'
+ /user/createWithList:
+ post:
+ tags:
+ - user
+ summary: Creates list of users with given input array
+ description: ''
+ operationId: createUsersWithListInput
+ responses:
+ default:
+ description: successful operation
+ requestBody:
+ $ref: '#/components/requestBodies/UserArray'
+ /user/login:
+ get:
+ tags:
+ - user
+ summary: Logs user into the system
+ description: ''
+ operationId: loginUser
+ parameters:
+ - name: username
+ in: query
+ description: The user name for login
+ required: true
+ schema:
+ type: string
+ - name: password
+ in: query
+ description: The password for login in clear text
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers:
+ X-Rate-Limit:
+ description: calls per hour allowed by the user
+ schema:
+ type: integer
+ format: int32
+ X-Expires-After:
+ description: date in UTC when token expires
+ schema:
+ type: string
+ format: date-time
+ content:
+ application/xml:
+ schema:
+ type: string
+ application/json:
+ schema:
+ type: string
+ '400':
+ description: Invalid username/password supplied
+ /user/logout:
+ get:
+ tags:
+ - user
+ summary: Logs out current logged in user session
+ description: ''
+ operationId: logoutUser
+ responses:
+ default:
+ description: successful operation
+ '/user/{username}':
+ get:
+ tags:
+ - user
+ summary: Get user by user name
+ description: ''
+ operationId: getUserByName
+ parameters:
+ - name: username
+ in: path
+ description: The name that needs to be fetched. Use user1 for testing.
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/User'
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ '400':
+ description: Invalid username supplied
+ '404':
+ description: User not found
+ put:
+ tags:
+ - user
+ summary: Updated user
+ description: This can only be done by the logged in user.
+ operationId: updateUser
+ parameters:
+ - name: username
+ in: path
+ description: name that need to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid user supplied
+ '404':
+ description: User not found
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ description: Updated user object
+ required: true
+ delete:
+ tags:
+ - user
+ summary: Delete user
+ description: This can only be done by the logged in user.
+ operationId: deleteUser
+ parameters:
+ - name: username
+ in: path
+ description: The name that needs to be deleted
+ required: true
+ schema:
+ type: string
+ responses:
+ '400':
+ description: Invalid username supplied
+ '404':
+ description: User not found
+ /fake_classname_test:
+ patch:
+ tags:
+ - 'fake_classname_tags 123#$%^'
+ summary: To test class name in snake case
+ description: To test class name in snake case
+ operationId: testClassname
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Client'
+ security:
+ - api_key_query: []
+ requestBody:
+ $ref: '#/components/requestBodies/Client'
+ /fake:
+ patch:
+ tags:
+ - fake
+ summary: To test "client" model
+ description: To test "client" model
+ operationId: testClientModel
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Client'
+ requestBody:
+ $ref: '#/components/requestBodies/Client'
+ get:
+ tags:
+ - fake
+ summary: To test enum parameters
+ description: To test enum parameters
+ operationId: testEnumParameters
+ parameters:
+ - name: enum_header_string_array
+ in: header
+ description: Header parameter enum test (string array)
+ schema:
+ type: array
+ items:
+ type: string
+ default: $
+ enum:
+ - '>'
+ - $
+ - name: enum_header_string
+ in: header
+ description: Header parameter enum test (string)
+ schema:
+ type: string
+ enum:
+ - _abc
+ - '-efg'
+ - (xyz)
+ default: '-efg'
+ - name: enum_query_string_array
+ in: query
+ description: Query parameter enum test (string array)
+ schema:
+ type: array
+ items:
+ type: string
+ default: $
+ enum:
+ - '>'
+ - $
+ - name: enum_query_string
+ in: query
+ description: Query parameter enum test (string)
+ schema:
+ type: string
+ enum:
+ - _abc
+ - '-efg'
+ - (xyz)
+ default: '-efg'
+ - name: enum_query_integer
+ in: query
+ description: Query parameter enum test (double)
+ schema:
+ type: integer
+ format: int32
+ enum:
+ - 1
+ - -2
+ - name: enum_query_double
+ in: query
+ description: Query parameter enum test (double)
+ schema:
+ type: number
+ format: double
+ enum:
+ - 1.1
+ - -1.2
+ - name: enum_query_model_array
+ in: query
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/EnumClass'
+ responses:
+ '400':
+ description: Invalid request
+ '404':
+ description: Not found
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ enum_form_string_array:
+ description: Form parameter enum test (string array)
+ type: array
+ items:
+ type: string
+ default: $
+ enum:
+ - '>'
+ - $
+ enum_form_string:
+ description: Form parameter enum test (string)
+ type: string
+ enum:
+ - _abc
+ - '-efg'
+ - (xyz)
+ default: '-efg'
+ post:
+ tags:
+ - fake
+ summary: |
+ Fake endpoint for testing various parameters
+ 假端點
+ 偽のエンドポイント
+ 가짜 엔드 포인트
+ description: |
+ Fake endpoint for testing various parameters
+ 假端點
+ 偽のエンドポイント
+ 가짜 엔드 포인트
+ operationId: testEndpointParameters
+ responses:
+ '400':
+ description: Invalid username supplied
+ '404':
+ description: User not found
+ security:
+ - http_basic_test: []
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ integer:
+ description: None
+ type: integer
+ minimum: 10
+ maximum: 100
+ int32:
+ description: None
+ type: integer
+ format: int32
+ minimum: 20
+ maximum: 200
+ int64:
+ description: None
+ type: integer
+ format: int64
+ number:
+ description: None
+ type: number
+ minimum: 32.1
+ maximum: 543.2
+ float:
+ description: None
+ type: number
+ format: float
+ maximum: 987.6
+ double:
+ description: None
+ type: number
+ format: double
+ minimum: 67.8
+ maximum: 123.4
+ string:
+ description: None
+ type: string
+ pattern: '/[a-z]/i'
+ pattern_without_delimiter:
+ description: None
+ type: string
+ pattern: '^[A-Z].*'
+ byte:
+ description: None
+ type: string
+ format: byte
+ binary:
+ description: None
+ type: string
+ format: binary
+ date:
+ description: None
+ type: string
+ format: date
+ dateTime:
+ description: None
+ type: string
+ format: date-time
+ password:
+ description: None
+ type: string
+ format: password
+ minLength: 10
+ maxLength: 64
+ callback:
+ description: None
+ type: string
+ required:
+ - number
+ - double
+ - pattern_without_delimiter
+ - byte
+ delete:
+ tags:
+ - fake
+ security:
+ - bearer_test: []
+ summary: Fake endpoint to test group parameters (optional)
+ description: Fake endpoint to test group parameters (optional)
+ operationId: testGroupParameters
+ x-group-parameters: true
+ parameters:
+ - name: required_string_group
+ in: query
+ description: Required String in group parameters
+ required: true
+ schema:
+ type: integer
+ - name: required_boolean_group
+ in: header
+ description: Required Boolean in group parameters
+ required: true
+ schema:
+ type: boolean
+ - name: required_int64_group
+ in: query
+ description: Required Integer in group parameters
+ required: true
+ schema:
+ type: integer
+ format: int64
+ - name: string_group
+ in: query
+ description: String in group parameters
+ schema:
+ type: integer
+ - name: boolean_group
+ in: header
+ description: Boolean in group parameters
+ schema:
+ type: boolean
+ - name: int64_group
+ in: query
+ description: Integer in group parameters
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '400':
+ description: Someting wrong
+ /fake/outer/number:
+ post:
+ tags:
+ - fake
+ description: Test serialization of outer number types
+ operationId: fakeOuterNumberSerialize
+ responses:
+ '200':
+ description: Output number
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/OuterNumber'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OuterNumber'
+ description: Input number as post body
+ /fake/property/enum-int:
+ post:
+ tags:
+ - fake
+ description: Test serialization of enum (int) properties with examples
+ operationId: fakePropertyEnumIntegerSerialize
+ responses:
+ '200':
+ description: Output enum (int)
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/OuterObjectWithEnumProperty'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OuterObjectWithEnumProperty'
+ description: Input enum (int) as post body
+ /fake/outer/string:
+ post:
+ tags:
+ - fake
+ description: Test serialization of outer string types
+ operationId: fakeOuterStringSerialize
+ responses:
+ '200':
+ description: Output string
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/OuterString'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OuterString'
+ description: Input string as post body
+ /fake/outer/boolean:
+ post:
+ tags:
+ - fake
+ description: Test serialization of outer boolean types
+ operationId: fakeOuterBooleanSerialize
+ responses:
+ '200':
+ description: Output boolean
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/OuterBoolean'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OuterBoolean'
+ description: Input boolean as post body
+ /fake/outer/composite:
+ post:
+ tags:
+ - fake
+ description: Test serialization of object with outer number type
+ operationId: fakeOuterCompositeSerialize
+ responses:
+ '200':
+ description: Output composite
+ content:
+ '*/*':
+ schema:
+ $ref: '#/components/schemas/OuterComposite'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OuterComposite'
+ description: Input composite as post body
+ /fake/jsonFormData:
+ get:
+ tags:
+ - fake
+ summary: test json serialization of form data
+ description: ''
+ operationId: testJsonFormData
+ responses:
+ '200':
+ description: successful operation
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ type: object
+ properties:
+ param:
+ description: field1
+ type: string
+ param2:
+ description: field2
+ type: string
+ required:
+ - param
+ - param2
+ /fake/inline-additionalProperties:
+ post:
+ tags:
+ - fake
+ summary: test inline additionalProperties
+ description: ''
+ operationId: testInlineAdditionalProperties
+ responses:
+ '200':
+ description: successful operation
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: string
+ description: request body
+ required: true
+ /fake/body-with-query-params:
+ put:
+ tags:
+ - fake
+ operationId: testBodyWithQueryParams
+ parameters:
+ - name: query
+ in: query
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Success
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/User'
+ required: true
+ /another-fake/dummy:
+ patch:
+ tags:
+ - $another-fake?
+ summary: To test special tags
+ description: To test special tags and operation ID starting with number
+ operationId: '123_test_@#$%_special_tags'
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Client'
+ requestBody:
+ $ref: '#/components/requestBodies/Client'
+ /fake/body-with-file-schema:
+ put:
+ tags:
+ - fake
+ description: >-
+ For this test, the body for this request must reference a schema named
+ `File`.
+ operationId: testBodyWithFileSchema
+ responses:
+ '200':
+ description: Success
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/FileSchemaTestClass'
+ required: true
+ /fake/body-with-binary:
+ put:
+ tags:
+ - fake
+ description: >-
+ For this test, the body has to be a binary file.
+ operationId: testBodyWithBinary
+ responses:
+ '200':
+ description: Success
+ requestBody:
+ content:
+ image/png:
+ schema:
+ type: string
+ nullable: true
+ format: binary
+ description: image to upload
+ required: true
+ /fake/test-query-parameters:
+ put:
+ tags:
+ - fake
+ description: To test the collection format in query parameters
+ operationId: testQueryParameterCollectionFormat
+ parameters:
+ - name: pipe
+ in: query
+ required: true
+ style: pipeDelimited
+ schema:
+ type: array
+ items:
+ type: string
+ - name: ioutil
+ in: query
+ required: true
+ style: form
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ - name: http
+ in: query
+ required: true
+ style: spaceDelimited
+ schema:
+ type: array
+ items:
+ type: string
+ - name: url
+ in: query
+ required: true
+ style: form
+ explode: false
+ schema:
+ type: array
+ items:
+ type: string
+ - name: context
+ in: query
+ required: true
+ explode: true
+ schema:
+ type: array
+ items:
+ type: string
+ - name: language
+ in: query
+ required: false
+ schema:
+ type: object
+ additionalProperties:
+ type: string
+ format: string
+ - name: allowEmpty
+ in: query
+ required: true
+ allowEmptyValue: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success
+ '/fake/{petId}/uploadImageWithRequiredFile':
+ post:
+ tags:
+ - pet
+ summary: uploads an image (required)
+ description: ''
+ operationId: uploadFileWithRequiredFile
+ parameters:
+ - name: petId
+ in: path
+ description: ID of pet to update
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiResponse'
+ security:
+ - petstore_auth:
+ - 'write:pets'
+ - 'read:pets'
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ additionalMetadata:
+ description: Additional data to pass to server
+ type: string
+ requiredFile:
+ description: file to upload
+ type: string
+ format: binary
+ required:
+ - requiredFile
+ /fake/health:
+ get:
+ tags:
+ - fake
+ summary: Health check endpoint
+ responses:
+ 200:
+ description: The instance started successfully
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HealthCheckResult'
+ /fake/http-signature-test:
+ get:
+ tags:
+ - fake
+ summary: test http signature authentication
+ operationId: fake-http-signature-test
+ parameters:
+ - name: query_1
+ in: query
+ description: query parameter
+ required: optional
+ schema:
+ type: string
+ - name: header_1
+ in: header
+ description: header parameter
+ required: optional
+ schema:
+ type: string
+ security:
+ - http_signature_test: []
+ requestBody:
+ $ref: '#/components/requestBodies/Pet'
+ responses:
+ 200:
+ description: The instance started successfully
+servers:
+ - url: 'http://{server}.swagger.io:{port}/v2'
+ description: petstore server
+ variables:
+ server:
+ enum:
+ - 'petstore'
+ - 'qa-petstore'
+ - 'dev-petstore'
+ default: 'petstore'
+ port:
+ enum:
+ - 80
+ - 8080
+ default: 80
+ - url: https://localhost:8080/{version}
+ description: The local server
+ variables:
+ version:
+ enum:
+ - 'v1'
+ - 'v2'
+ default: 'v2'
+ - url: https://127.0.0.1/no_varaible
+ description: The local server without variables
+components:
+ requestBodies:
+ UserArray:
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/User'
+ description: List of user object
+ required: true
+ Client:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Client'
+ description: client model
+ required: true
+ Pet:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ application/xml:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ description: Pet object that needs to be added to the store
+ required: true
+ securitySchemes:
+ petstore_auth:
+ type: oauth2
+ flows:
+ implicit:
+ authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog'
+ scopes:
+ 'write:pets': modify pets in your account
+ 'read:pets': read your pets
+ api_key:
+ type: apiKey
+ name: api_key
+ in: header
+ api_key_query:
+ type: apiKey
+ name: api_key_query
+ in: query
+ http_basic_test:
+ type: http
+ scheme: basic
+ bearer_test:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ http_signature_test:
+ type: http
+ scheme: signature
+ schemas:
+ Foo:
+ type: object
+ properties:
+ bar:
+ $ref: '#/components/schemas/Bar'
+ Bar:
+ type: string
+ default: bar
+ Order:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ petId:
+ type: integer
+ format: int64
+ quantity:
+ type: integer
+ format: int32
+ shipDate:
+ type: string
+ format: date-time
+ status:
+ type: string
+ description: Order Status
+ enum:
+ - placed
+ - approved
+ - delivered
+ complete:
+ type: boolean
+ default: false
+ xml:
+ name: Order
+ Category:
+ type: object
+ required:
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ default: default-name
+ xml:
+ name: Category
+ User:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ x-is-unique: true
+ username:
+ type: string
+ firstName:
+ type: string
+ lastName:
+ type: string
+ email:
+ type: string
+ password:
+ type: string
+ phone:
+ type: string
+ userStatus:
+ type: integer
+ format: int32
+ description: User Status
+ xml:
+ name: User
+ Tag:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ xml:
+ name: Tag
+ Pet:
+ type: object
+ required:
+ - name
+ - photoUrls
+ properties:
+ id:
+ type: integer
+ format: int64
+ x-is-unique: true
+ category:
+ $ref: '#/components/schemas/Category'
+ name:
+ type: string
+ example: doggie
+ photoUrls:
+ type: array
+ xml:
+ name: photoUrl
+ wrapped: true
+ items:
+ type: string
+ uniqueItems: true
+ tags:
+ type: array
+ xml:
+ name: tag
+ wrapped: true
+ items:
+ $ref: '#/components/schemas/Tag'
+ status:
+ type: string
+ description: pet status in the store
+ enum:
+ - available
+ - pending
+ - sold
+ xml:
+ name: Pet
+ ApiResponse:
+ type: object
+ properties:
+ code:
+ type: integer
+ format: int32
+ type:
+ type: string
+ message:
+ type: string
+ Return:
+ description: Model for testing reserved words
+ properties:
+ return:
+ type: integer
+ format: int32
+ xml:
+ name: Return
+ Name:
+ description: Model for testing model name same as property name
+ required:
+ - name
+ properties:
+ name:
+ type: integer
+ format: int32
+ snake_case:
+ readOnly: true
+ type: integer
+ format: int32
+ property:
+ type: string
+ 123Number:
+ type: integer
+ readOnly: true
+ xml:
+ name: Name
+ 200_response:
+ description: Model for testing model name starting with number
+ properties:
+ name:
+ type: integer
+ format: int32
+ class:
+ type: string
+ xml:
+ name: Name
+ ClassModel:
+ description: Model for testing model with "_class" property
+ properties:
+ _class:
+ type: string
+ Dog:
+ allOf:
+ - $ref: '#/components/schemas/Animal'
+ - type: object
+ properties:
+ breed:
+ type: string
+ Cat:
+ allOf:
+ - $ref: '#/components/schemas/Animal'
+ - type: object
+ properties:
+ declawed:
+ type: boolean
+ Animal:
+ type: object
+ discriminator:
+ propertyName: className
+ required:
+ - className
+ properties:
+ className:
+ type: string
+ color:
+ type: string
+ default: red
+ AnimalFarm:
+ type: array
+ items:
+ $ref: '#/components/schemas/Animal'
+ format_test:
+ type: object
+ required:
+ - number
+ - byte
+ - date
+ - password
+ properties:
+ integer:
+ type: integer
+ maximum: 100
+ minimum: 10
+ int32:
+ type: integer
+ format: int32
+ maximum: 200
+ minimum: 20
+ int64:
+ type: integer
+ format: int64
+ number:
+ maximum: 543.2
+ minimum: 32.1
+ type: number
+ float:
+ type: number
+ format: float
+ maximum: 987.6
+ minimum: 54.3
+ double:
+ type: number
+ format: double
+ maximum: 123.4
+ minimum: 67.8
+ decimal:
+ type: string
+ format: number
+ string:
+ type: string
+ pattern: '/[a-z]/i'
+ byte:
+ type: string
+ format: byte
+ binary:
+ type: string
+ format: binary
+ date:
+ type: string
+ format: date
+ dateTime:
+ type: string
+ format: date-time
+ uuid:
+ type: string
+ format: uuid
+ example: 72f98069-206d-4f12-9f12-3d1e525a8e84
+ password:
+ type: string
+ format: password
+ maxLength: 64
+ minLength: 10
+ pattern_with_digits:
+ description: A string that is a 10 digit number. Can have leading zeros.
+ type: string
+ pattern: '^\d{10}$'
+ pattern_with_digits_and_delimiter:
+ description: A string starting with 'image_' (case insensitive) and one to three digits following i.e. Image_01.
+ type: string
+ pattern: '/^image_\d{1,3}$/i'
+ EnumClass:
+ type: string
+ default: '-efg'
+ enum:
+ - _abc
+ - '-efg'
+ - (xyz)
+ Enum_Test:
+ type: object
+ required:
+ - enum_string_required
+ properties:
+ enum_string:
+ type: string
+ enum:
+ - UPPER
+ - lower
+ - ''
+ enum_string_required:
+ type: string
+ enum:
+ - UPPER
+ - lower
+ - ''
+ enum_integer:
+ type: integer
+ format: int32
+ enum:
+ - 1
+ - -1
+ enum_number:
+ type: number
+ format: double
+ enum:
+ - 1.1
+ - -1.2
+ outerEnum:
+ $ref: '#/components/schemas/OuterEnum'
+ outerEnumInteger:
+ $ref: '#/components/schemas/OuterEnumInteger'
+ outerEnumDefaultValue:
+ $ref: '#/components/schemas/OuterEnumDefaultValue'
+ outerEnumIntegerDefaultValue:
+ $ref: '#/components/schemas/OuterEnumIntegerDefaultValue'
+ AdditionalPropertiesClass:
+ type: object
+ properties:
+ map_property:
+ type: object
+ additionalProperties:
+ type: string
+ map_of_map_property:
+ type: object
+ additionalProperties:
+ type: object
+ additionalProperties:
+ type: string
+ MixedPropertiesAndAdditionalPropertiesClass:
+ type: object
+ properties:
+ uuid:
+ type: string
+ format: uuid
+ dateTime:
+ type: string
+ format: date-time
+ map:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/Animal'
+ List:
+ type: object
+ properties:
+ 123-list:
+ type: string
+ Client:
+ type: object
+ properties:
+ client:
+ type: string
+ ReadOnlyFirst:
+ type: object
+ properties:
+ bar:
+ type: string
+ readOnly: true
+ baz:
+ type: string
+ hasOnlyReadOnly:
+ type: object
+ properties:
+ bar:
+ type: string
+ readOnly: true
+ foo:
+ type: string
+ readOnly: true
+ Capitalization:
+ type: object
+ properties:
+ smallCamel:
+ type: string
+ CapitalCamel:
+ type: string
+ small_Snake:
+ type: string
+ Capital_Snake:
+ type: string
+ SCA_ETH_Flow_Points:
+ type: string
+ ATT_NAME:
+ description: |
+ Name of the pet
+ type: string
+ MapTest:
+ type: object
+ properties:
+ map_map_of_string:
+ type: object
+ additionalProperties:
+ type: object
+ additionalProperties:
+ type: string
+ map_of_enum_string:
+ type: object
+ additionalProperties:
+ type: string
+ enum:
+ - UPPER
+ - lower
+ direct_map:
+ type: object
+ additionalProperties:
+ type: boolean
+ indirect_map:
+ $ref: '#/components/schemas/StringBooleanMap'
+ ArrayTest:
+ type: object
+ properties:
+ array_of_string:
+ type: array
+ items:
+ type: string
+ minItems: 0
+ maxItems: 3
+ array_array_of_integer:
+ type: array
+ items:
+ type: array
+ items:
+ type: integer
+ format: int64
+ array_array_of_model:
+ type: array
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/ReadOnlyFirst'
+ NumberOnly:
+ type: object
+ properties:
+ JustNumber:
+ type: number
+ ArrayOfNumberOnly:
+ type: object
+ properties:
+ ArrayNumber:
+ type: array
+ items:
+ type: number
+ ArrayOfArrayOfNumberOnly:
+ type: object
+ properties:
+ ArrayArrayNumber:
+ type: array
+ items:
+ type: array
+ items:
+ type: number
+ EnumArrays:
+ type: object
+ properties:
+ just_symbol:
+ type: string
+ enum:
+ - '>='
+ - $
+ array_enum:
+ type: array
+ items:
+ type: string
+ enum:
+ - fish
+ - crab
+ OuterEnum:
+ nullable: true
+ type: string
+ enum:
+ - placed
+ - approved
+ - delivered
+ OuterEnumInteger:
+ type: integer
+ enum:
+ - 0
+ - 1
+ - 2
+ example: 2
+ OuterEnumDefaultValue:
+ type: string
+ enum:
+ - placed
+ - approved
+ - delivered
+ default: placed
+ OuterEnumIntegerDefaultValue:
+ type: integer
+ enum:
+ - 0
+ - 1
+ - 2
+ default: 0
+ OuterComposite:
+ type: object
+ properties:
+ my_number:
+ $ref: '#/components/schemas/OuterNumber'
+ my_string:
+ $ref: '#/components/schemas/OuterString'
+ my_boolean:
+ $ref: '#/components/schemas/OuterBoolean'
+ OuterNumber:
+ type: number
+ OuterString:
+ type: string
+ OuterBoolean:
+ type: boolean
+ x-codegen-body-parameter-name: boolean_post_body
+ StringBooleanMap:
+ additionalProperties:
+ type: boolean
+ FileSchemaTestClass:
+ type: object
+ properties:
+ file:
+ $ref: '#/components/schemas/File'
+ files:
+ type: array
+ items:
+ $ref: '#/components/schemas/File'
+ File:
+ type: object
+ description: Must be named `File` for test.
+ properties:
+ sourceURI:
+ description: Test capitalization
+ type: string
+ _special_model.name_:
+ properties:
+ '$special[property.name]':
+ type: integer
+ format: int64
+ xml:
+ name: '$special[model.name]'
+ HealthCheckResult:
+ type: object
+ properties:
+ NullableMessage:
+ nullable: true
+ type: string
+ description: Just a string to inform instance is up and running. Make it nullable in hope to get it as pointer in generated model.
+ NullableClass:
+ type: object
+ properties:
+ integer_prop:
+ type: integer
+ nullable: true
+ number_prop:
+ type: number
+ nullable: true
+ boolean_prop:
+ type: boolean
+ nullable: true
+ string_prop:
+ type: string
+ nullable: true
+ date_prop:
+ type: string
+ format: date
+ nullable: true
+ datetime_prop:
+ type: string
+ format: date-time
+ nullable: true
+ array_nullable_prop:
+ type: array
+ nullable: true
+ items:
+ type: object
+ array_and_items_nullable_prop:
+ type: array
+ nullable: true
+ items:
+ type: object
+ nullable: true
+ array_items_nullable:
+ type: array
+ items:
+ type: object
+ nullable: true
+ object_nullable_prop:
+ type: object
+ nullable: true
+ additionalProperties:
+ type: object
+ object_and_items_nullable_prop:
+ type: object
+ nullable: true
+ additionalProperties:
+ type: object
+ nullable: true
+ object_items_nullable:
+ type: object
+ additionalProperties:
+ type: object
+ nullable: true
+ additionalProperties:
+ type: object
+ nullable: true
+ OuterObjectWithEnumProperty:
+ type: object
+ example:
+ value: 2
+ required:
+ - value
+ properties:
+ value:
+ $ref: '#/components/schemas/OuterEnumInteger'
+ DeprecatedObject:
+ type: object
+ deprecated: true
+ properties:
+ name:
+ type: string
+ ObjectWithDeprecatedFields:
+ type: object
+ properties:
+ uuid:
+ type: string
+ id:
+ type: number
+ deprecated: true
+ deprecatedRef:
+ $ref: '#/components/schemas/DeprecatedObject'
+ bars:
+ type: array
+ deprecated: true
+ items:
+ $ref: '#/components/schemas/Bar'
+ AllOfWithSingleRef:
+ type: object
+ properties:
+ username:
+ type: string
+ SingleRefType:
+ allOf:
+ - $ref: '#/components/schemas/SingleRefType'
+ SingleRefType:
+ type: string
+ title: SingleRefType
+ enum:
+ - admin
+ - user
diff --git a/microprofile/openapi/src/test/resources/serverCORSRestricted.yaml b/microprofile/openapi/src/test/resources/serverCORSRestricted.yaml
deleted file mode 100644
index 30700d1fadf..00000000000
--- a/microprofile/openapi/src/test/resources/serverCORSRestricted.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# Copyright (c) 2020, 2023 Oracle and/or its affiliates.
-#
-# Licensed 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.
-#
-openapi:
- cors:
- allow-origins: ["http://foo.bar", "http://bar.foo"]
diff --git a/microprofile/openapi/src/test/resources/serverConfig.yml b/microprofile/openapi/src/test/resources/serverConfig.yml
deleted file mode 100644
index f924a73fa7f..00000000000
--- a/microprofile/openapi/src/test/resources/serverConfig.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-#
-# Copyright (c) 2020, 2023 Oracle and/or its affiliates.
-#
-# Licensed 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.
-#
-
-openapi:
- web-context: /otheropenapi
- port: 0
diff --git a/microprofile/openapi/src/test/resources/serverNoCORS.properties b/microprofile/openapi/src/test/resources/serverNoCORS.properties
deleted file mode 100644
index 7295615d45a..00000000000
--- a/microprofile/openapi/src/test/resources/serverNoCORS.properties
+++ /dev/null
@@ -1,16 +0,0 @@
-#
-# Copyright (c) 2020, 2023 Oracle and/or its affiliates.
-#
-# Licensed 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.
-#
-openapi.cors.enabled: false
diff --git a/microprofile/openapi/src/test/resources/serverTest.properties b/microprofile/openapi/src/test/resources/serverTest.properties
deleted file mode 100644
index 6fb062921a0..00000000000
--- a/microprofile/openapi/src/test/resources/serverTest.properties
+++ /dev/null
@@ -1,24 +0,0 @@
-#
-# Copyright (c) 2019, 2023 Oracle and/or its affiliates.
-#
-# Licensed 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.
-#
-openapi.model.reader: io.helidon.microprofile.openapi.test.MyModelReader
-openapi.filter: io.helidon.openapi.microprofile.MySimpleFilter
-openapi.servers: s1,s2
-openapi.servers.path.path1: p1s1,p1s2
-openapi.servers.path.path2: p2s1,p2s2
-openapi.servers.operation.op1: o1s1,o1s2
-openapi.servers.operation.op2: o2s1,o2s2
-openapi.scan.disable: false
-
diff --git a/microprofile/openapi/src/test/resources/simple.properties b/microprofile/openapi/src/test/resources/simple.properties
deleted file mode 100644
index cdf682ad3e4..00000000000
--- a/microprofile/openapi/src/test/resources/simple.properties
+++ /dev/null
@@ -1,23 +0,0 @@
-#
-# Copyright (c) 2019, 2023 Oracle and/or its affiliates.
-#
-# Licensed 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.
-#
-openapi.model.reader: io.helidon.microprofile.openapi.test.MyModelReader
-openapi.filter: io.helidon.microprofile.openapi.test.MySimpleFilter
-openapi.servers: s1,s2
-openapi.servers.path.path1: p1s1,p1s2
-openapi.servers.path.path2: p2s1,p2s2
-openapi.servers.operation.op1: o1s1,o1s2
-openapi.servers.operation.op2: o2s1,o2s2
-openapi.scan.disable: false
diff --git a/openapi/openapi-ui/pom.xml b/openapi/openapi-ui/pom.xml
new file mode 100644
index 00000000000..8511bbeccf2
--- /dev/null
+++ b/openapi/openapi-ui/pom.xml
@@ -0,0 +1,134 @@
+
+
+
+ 4.0.0
+
+ io.helidon.openapi
+ helidon-openapi-project
+ 4.0.0-SNAPSHOT
+
+
+ helidon-openapi-ui
+ Helidon OpenAPI
+
+
+ Helidon OpenAPI UI implementation
+
+
+
+
+ io.helidon.openapi
+ helidon-openapi
+
+
+ io.helidon.common.features
+ helidon-common-features-api
+ true
+
+
+ io.helidon.config
+ helidon-config-metadata
+ true
+
+
+ io.smallrye
+ smallrye-open-api-ui
+ ${version.lib.smallrye-openapi}
+
+
+ io.helidon.webserver
+ helidon-webserver-static-content
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+ io.helidon.common.testing
+ helidon-common-testing-junit5
+ test
+
+
+ io.helidon.webserver.testing.junit5
+ helidon-webserver-testing-junit5
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ io.helidon.common.features
+ helidon-common-features-processor
+ ${helidon.version}
+
+
+ io.helidon.config
+ helidon-config-metadata-processor
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-processor
+ ${helidon.version}
+
+
+ io.helidon.common.processor
+ helidon-common-processor-helidon-copyright
+ ${helidon.version}
+
+
+
+
+
+ io.helidon.common.features
+ helidon-common-features-processor
+ ${helidon.version}
+
+
+ io.helidon.builder
+ helidon-builder-processor
+ ${helidon.version}
+
+
+ io.helidon.config
+ helidon-config-metadata-processor
+ ${helidon.version}
+
+
+ io.helidon.common.processor
+ helidon-common-processor-helidon-copyright
+ ${helidon.version}
+
+
+
+
+
+
diff --git a/openapi/openapi-ui/src/main/java/io/helidon/openapi/ui/OpenApiUi.java b/openapi/openapi-ui/src/main/java/io/helidon/openapi/ui/OpenApiUi.java
new file mode 100644
index 00000000000..775b15843e7
--- /dev/null
+++ b/openapi/openapi-ui/src/main/java/io/helidon/openapi/ui/OpenApiUi.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2023 Oracle and/or its affiliates.
+ *
+ * Licensed 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 io.helidon.openapi.ui;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import io.helidon.builder.api.RuntimeType;
+import io.helidon.common.LazyValue;
+import io.helidon.common.media.type.MediaType;
+import io.helidon.common.media.type.MediaTypes;
+import io.helidon.http.HeaderNames;
+import io.helidon.http.HttpMediaType;
+import io.helidon.http.ServerRequestHeaders;
+import io.helidon.http.Status;
+import io.helidon.openapi.OpenApiService;
+import io.helidon.webserver.http.HttpRules;
+import io.helidon.webserver.http.ServerRequest;
+import io.helidon.webserver.http.ServerResponse;
+import io.helidon.webserver.staticcontent.StaticContentService;
+
+import io.smallrye.openapi.ui.IndexHtmlCreator;
+import io.smallrye.openapi.ui.Option;
+
+/**
+ * An {@link OpenApiService} that serves OpenApi UI.
+ */
+@RuntimeType.PrototypedBy(OpenApiUiConfig.class)
+public final class OpenApiUi implements OpenApiService, RuntimeType.Api {
+
+ /**
+ * Returns a new builder.
+ *
+ * @return new builder
+ */
+ public static OpenApiUiConfig.Builder builder() {
+ return OpenApiUiConfig.builder();
+ }
+
+ /**
+ * Create a new instance with default configuration.
+ *
+ * @return new instance
+ */
+ public static OpenApiUi create() {
+ return builder().build();
+ }
+
+ /**
+ * Create a new instance from typed configuration.
+ *
+ * @param config typed configuration
+ * @return new instance
+ */
+ static OpenApiUi create(OpenApiUiConfig config) {
+ return new OpenApiUi(config);
+ }
+
+ /**
+ * Create a new instance with custom configuration.
+ *
+ * @param builderConsumer consumer of configuration builder
+ * @return new instance
+ */
+ public static OpenApiUi create(Consumer builderConsumer) {
+ OpenApiUiConfig.Builder b = OpenApiUiConfig.builder();
+ builderConsumer.accept(b);
+ return b.build();
+ }
+
+ private static final String LOGO_RESOURCE = "logo.svg";
+ private static final String HELIDON_IO_LINK = "https://helidon.io";
+
+ private static final MediaType[] ACCEPTED_MEDIA_TYPES = new MediaType[] {
+ MediaTypes.APPLICATION_JSON,
+ MediaTypes.TEXT_YAML,
+ MediaTypes.TEXT_PLAIN,
+ MediaTypes.TEXT_HTML,
+ };
+
+ private static final HttpMediaType TEXT_HTML = HttpMediaType.create(MediaTypes.TEXT_HTML);
+
+ private static final Map