Skip to content

Commit

Permalink
Fix openapi 3.1 document generation
Browse files Browse the repository at this point in the history
  • Loading branch information
justin-tay committed Jun 28, 2024
1 parent c880e7d commit 5e9072f
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import com.yahoo.elide.graphql.GraphQLSettings;
import com.yahoo.elide.jsonapi.JsonApiSettings;
import com.yahoo.elide.swagger.OpenApiBuilder;
import com.yahoo.elide.swagger.OpenApiDocument;
import com.yahoo.elide.swagger.resources.ApiDocsEndpoint;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.hibernate.Session;
Expand Down Expand Up @@ -131,8 +130,7 @@ public List<ApiDocsEndpoint.ApiDocsRegistration> buildSwagger(Elide elide) {
OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion(apiVersion);
String moduleBasePath = "/apiDocs/";
OpenAPI openApi = builder.build().info(info).addServersItem(new Server().url(moduleBasePath));
docs.add(new ApiDocsEndpoint.ApiDocsRegistration("api", () -> openApi,
OpenApiDocument.Version.OPENAPI_3_0.getValue(), apiVersion));
docs.add(new ApiDocsEndpoint.ApiDocsRegistration("api", () -> openApi, apiVersion));
});

return docs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
* Basic customization of the OpenAPI document.
Expand All @@ -48,9 +49,15 @@ public void customize(OpenAPI openApi) {
}

if (openApi.getInfo() == null) {
openApi.info(new Info().title(OpenApiDocument.DEFAULT_TITLE));
} else if (openApi.getInfo().getTitle() == null || openApi.getInfo().getTitle().isBlank()) {
openApi.getInfo().setTitle(OpenApiDocument.DEFAULT_TITLE);
// Version is a required field
openApi.info(new Info().title(OpenApiDocument.DEFAULT_TITLE).version(""));
} else {
if (openApi.getInfo().getTitle() == null || openApi.getInfo().getTitle().isBlank()) {
openApi.getInfo().setTitle(OpenApiDocument.DEFAULT_TITLE);
}
if (openApi.getInfo().getVersion() == null) {
openApi.getInfo().setVersion("");
}
}

sort(openApi);
Expand Down Expand Up @@ -79,12 +86,10 @@ protected Map<String, io.swagger.v3.oas.models.security.SecurityScheme> getSecur
new io.swagger.v3.oas.models.security.SecurityScheme();
model.setIn(getIn(annotation.in()));
model.setType(getType(annotation.type()));
model.setBearerFormat(annotation.bearerFormat());
model.setScheme(annotation.scheme());
model.setOpenIdConnectUrl(annotation.openIdConnectUrl());
if (annotation.ref() != null && !annotation.ref().isBlank()) {
model.set$ref(annotation.ref());
}
copyNonBlank(annotation.bearerFormat(), model::setBearerFormat);
copyNonBlank(annotation.scheme(), model::setScheme);
copyNonBlank(annotation.openIdConnectUrl(), model::setOpenIdConnectUrl);
copyNonBlank(annotation.ref(), model::set$ref);
model.setName(annotation.name());
securitySchemes.put(annotation.name(), model);
}
Expand All @@ -103,7 +108,20 @@ protected OpenAPIDefinition getOpenApiDefinition() {
}

public static void applyDefinition(OpenAPI openApi, OpenAPIDefinition openApiDefinition) {
AnnotationsUtils.getInfo(openApiDefinition.info()).ifPresent(openApi::setInfo);
AnnotationsUtils.getInfo(openApiDefinition.info()).ifPresent(info -> {
if (openApi.getInfo() == null) {
openApi.setInfo(info);
} else {
// Copy non null
copyNonNull(info.getTitle(), openApi.getInfo()::setTitle);
copyNonNull(info.getDescription(), openApi.getInfo()::setDescription);
copyNonNull(info.getTermsOfService(), openApi.getInfo()::setTermsOfService);
copyNonNull(info.getContact(), openApi.getInfo()::setContact);
copyNonNull(info.getLicense(), openApi.getInfo()::setLicense);
copyNonNull(info.getVersion(), openApi.getInfo()::setVersion);
copyNonNull(info.getExtensions(), openApi.getInfo()::setExtensions);
}
});
AnnotationsUtils.getExternalDocumentation(openApiDefinition.externalDocs()).ifPresent(openApi::setExternalDocs);
AnnotationsUtils.getTags(openApiDefinition.tags(), false).ifPresent(tags -> tags.forEach(openApi::addTagsItem));

Expand All @@ -117,6 +135,18 @@ public static void applyDefinition(OpenAPI openApi, OpenAPIDefinition openApiDef
openApi.addSecurityItem(model);
}

protected static <T> void copyNonNull(T value, Consumer<T> target) {
if (value != null) {
target.accept(value);
}
}

protected static void copyNonBlank(String value, Consumer<String> target) {
if (value != null && !value.isBlank()) {
target.accept(value);
}
}

protected io.swagger.v3.oas.models.security.SecurityScheme.In getIn(SecuritySchemeIn value) {
if (value == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.Parameter;
Expand Down Expand Up @@ -1115,8 +1116,11 @@ public static ApiDocsController.ApiDocsRegistrations buildApiDocsRegistrations(R
List<ApiDocsRegistration> registrations = new ArrayList<>();
dictionary.getApiVersions().stream().forEach(apiVersion -> {
Supplier<OpenAPI> document = () -> {
OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion(apiVersion)
.supportLegacyFilterDialect(false);
OpenApiBuilder builder = new OpenApiBuilder(dictionary, openApi -> {
if (ApiDocsControllerProperties.Version.OPENAPI_3_1.equals(settings.getApiDocs().getVersion())) {
openApi.specVersion(SpecVersion.V31).openapi("3.1.0");
}
}).apiVersion(apiVersion).supportLegacyFilterDialect(false);
if (!EntityDictionary.NO_VERSION.equals(apiVersion)) {
if (settings.getApiVersioningStrategy().getPath().isEnabled()) {
// Path needs to be set
Expand Down Expand Up @@ -1152,8 +1156,7 @@ public static ApiDocsController.ApiDocsRegistrations buildApiDocsRegistrations(R
customizer.customize(openApi);
return openApi;
};
registrations.add(new ApiDocsRegistration("", SingletonSupplier.of(document),
settings.getApiDocs().getVersion().getValue(), apiVersion));
registrations.add(new ApiDocsRegistration("", SingletonSupplier.of(document), apiVersion));
});
return new ApiDocsController.ApiDocsRegistrations(registrations);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,8 @@ public class ApiDocsController {
@Data
@AllArgsConstructor
public static class ApiDocsRegistrations {

public ApiDocsRegistrations(Supplier<OpenAPI> doc, String version, String apiVersion) {
registrations = List.of(new ApiDocsRegistration("", doc, version, apiVersion));
public ApiDocsRegistrations(Supplier<OpenAPI> doc, String apiVersion) {
registrations = List.of(new ApiDocsRegistration("", doc, apiVersion));
}

List<ApiDocsRegistration> registrations;
Expand All @@ -81,11 +80,6 @@ public static class ApiDocsRegistration {
private String path;
private Supplier<OpenAPI> document;

/**
* The OpenAPI Specification Version.
*/
private String version;

/**
* The API version.
*/
Expand All @@ -109,8 +103,7 @@ public ApiDocsController(ApiDocsRegistrations docs, RouteResolver routeResolver,
apiVersion = apiVersion == null ? NO_VERSION : apiVersion;
String apiPath = doc.path;

this.documents.put(Pair.of(apiVersion, apiPath),
new OpenApiDocument(doc.document, OpenApiDocument.Version.from(doc.version)));
this.documents.put(Pair.of(apiVersion, apiPath), new OpenApiDocument(doc.document));
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class BasicOpenApiDocumentCustomizerTest {
@OpenAPIDefinition(info = @Info(title = "My Title"))
public static class UserDefinitionOpenApiConfiguration {
@Bean
public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
OpenApiDocumentCustomizer openApiDocumentCustomizer() {
return new BasicOpenApiDocumentCustomizer();
}
}
Expand All @@ -43,15 +43,15 @@ public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
@OpenAPIDefinition(info = @Info(description = "My Description"))
public static class UserDefinitionNoTitleOpenApiConfiguration {
@Bean
public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
OpenApiDocumentCustomizer openApiDocumentCustomizer() {
return new BasicOpenApiDocumentCustomizer();
}
}

@Configuration
public static class UserNoDefinitionOpenApiConfiguration {
@Bean
public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
OpenApiDocumentCustomizer openApiDocumentCustomizer() {
return new BasicOpenApiDocumentCustomizer();
}
}
Expand All @@ -66,7 +66,7 @@ public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
)
public static class UserSecurityOpenApiConfiguration {
@Bean
public OpenApiDocumentCustomizer openApiDocumentCustomizer() {
OpenApiDocumentCustomizer openApiDocumentCustomizer() {
return new BasicOpenApiDocumentCustomizer();
}
}
Expand Down Expand Up @@ -123,4 +123,28 @@ void shouldSortTags() {
assertThat(openApi.getTags()).extracting("name").containsExactly("1-test", "a-test", "b-test", "z-test");
});
}

@Test
void shouldUseOpenApiDefinitionExceptNonNullExisting() {
contextRunner.withUserConfiguration(UserDefinitionOpenApiConfiguration.class).run(context -> {
OpenAPI openApi = new OpenAPI();
openApi.setInfo(new io.swagger.v3.oas.models.info.Info().title("Should Be Overridden").version("v2"));
context.getBean(OpenApiDocumentCustomizer.class).customize(openApi);
assertThat(openApi.getInfo().getTitle()).isEqualTo("My Title");
assertThat(openApi.getInfo().getVersion()).isEqualTo("v2");
});
}

/**
* The OpenAPI document /info/version is a required property.
*/
@Test
void shouldNotHaveNullVersion() {
contextRunner.withUserConfiguration(UserDefinitionOpenApiConfiguration.class).run(context -> {
OpenAPI openApi = new OpenAPI();
context.getBean(OpenApiDocumentCustomizer.class).customize(openApi);
assertThat(openApi.getInfo().getVersion()).isNotNull();
assertThat(openApi.getInfo().getVersion()).isEqualTo("");
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import graphql.execution.SimpleDataFetcherExceptionHandler;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.SpecVersion;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.servers.Server;
import jakarta.persistence.EntityManager;
Expand Down Expand Up @@ -471,15 +472,19 @@ default List<ApiDocsEndpoint.ApiDocsRegistration> buildApiDocs(EntityDictionary
Info info = new Info()
.title(getApiTitle())
.version(apiVersion);
OpenApiBuilder builder = new OpenApiBuilder(dictionary).apiVersion(apiVersion);
OpenApiBuilder builder = new OpenApiBuilder(dictionary, openApi -> {
OpenApiVersion openApiVersion = getOpenApiVersion();
if (OpenApiVersion.OPENAPI_3_1.equals(openApiVersion)) {
openApi.specVersion(SpecVersion.V31).openapi("3.1.0");
}
}).apiVersion(apiVersion);
if (!EntityDictionary.NO_VERSION.equals(apiVersion)) {
// Path needs to be set
builder.basePath("/" + "v" + apiVersion);
}
String moduleBasePath = getJsonApiPathSpec().replace("/*", "");
OpenAPI openApi = builder.build().info(info).addServersItem(new Server().url(moduleBasePath));
docs.add(new ApiDocsEndpoint.ApiDocsRegistration("", () -> openApi, getOpenApiVersion().getValue(),
apiVersion));
docs.add(new ApiDocsEndpoint.ApiDocsRegistration("", () -> openApi, apiVersion));
});

return docs;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -684,10 +684,13 @@ private boolean lineageContainsType(PathMetaData other) {

/**
* Constructor.
* <p>
* The customizer can be used to set the OpenAPI SpecVersion.
*
* @param dictionary The entity dictionary.
* @param dictionary The entity dictionary.
* @param openApiCustomizer The OpenAPI customizer.
*/
public OpenApiBuilder(EntityDictionary dictionary) {
public OpenApiBuilder(EntityDictionary dictionary, Consumer<OpenAPI> openApiCustomizer) {
this.dictionary = dictionary;
this.supportLegacyFilterDialect = true;
this.supportRSQLFilterDialect = true;
Expand All @@ -698,6 +701,18 @@ public OpenApiBuilder(EntityDictionary dictionary) {
Operator.POSTFIX, Operator.GE, Operator.GT, Operator.LE, Operator.LT, Operator.ISNULL,
Operator.NOTNULL);
this.openApi = new OpenAPI();
if (openApiCustomizer != null) {
openApiCustomizer.accept(this.openApi);
}
}

/**
* Constructor.
*
* @param dictionary The entity dictionary.
*/
public OpenApiBuilder(EntityDictionary dictionary) {
this(dictionary, null);
}

/**
Expand Down Expand Up @@ -1095,8 +1110,10 @@ protected Set<PathMetaData> find(Type<?> rootClass) {
}

protected String getSchemaName(Type<?> type) {
// Should be the same as JsonApiModelResolver#getSchemaName
String schemaName = dictionary.getJsonAliasFor(type);
if (!EntityDictionary.NO_VERSION.equals(this.apiVersion)) {
String apiVersion = EntityDictionary.getModelVersion(type);
if (!EntityDictionary.NO_VERSION.equals(apiVersion)) {
schemaName = "v" + this.apiVersion + "_" + schemaName;
}
return schemaName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.core.util.Yaml31;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.SpecVersion;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -32,44 +33,15 @@ private MediaType() {
public static final String APPLICATION_YAML = "application/yaml";
}

/**
* The OpenAPI Specification Version.
*/
public enum Version {
OPENAPI_3_0("3.0"),
OPENAPI_3_1("3.1");

private final String value;

Version(String value) {
this.value = value;
}

public String getValue() {
return this.value;
}

public static Version from(String version) {
if (version.startsWith(OPENAPI_3_1.getValue())) {
return OPENAPI_3_1;
} else if (version.startsWith(OPENAPI_3_0.getValue())) {
return OPENAPI_3_0;
}
throw new IllegalArgumentException("Invalid OpenAPI version. Only versions 3.0 and 3.1 are supported.");
}
}

private final Map<String, String> documents = new ConcurrentHashMap<>(2);
private final Supplier<OpenAPI> openApi;
private final Version version;

public OpenApiDocument(OpenAPI openApi, Version version) {
this(() -> openApi, version);
public OpenApiDocument(OpenAPI openApi) {
this(() -> openApi);
}

public OpenApiDocument(Supplier<OpenAPI> openApi, Version version) {
public OpenApiDocument(Supplier<OpenAPI> openApi) {
this.openApi = openApi;
this.version = version;
}

public String ofMediaType(String mediaType) {
Expand All @@ -79,24 +51,23 @@ public String ofMediaType(String mediaType) {
} else {
key = MediaType.APPLICATION_JSON;
}
return this.documents.computeIfAbsent(key, type -> of(this.openApi.get(), this.version, type));
return this.documents.computeIfAbsent(key, type -> of(this.openApi.get(), type));
}

/**
* Converts a OpenAPI document to human-formatted JSON/YAML.
*
* @param openApi OpenAPI document
* @param version OpenAPI version
* @param mediaType Either application/json or application/yaml
* @return Pretty printed 'OpenAPI' document in JSON.
*/
public static String of(OpenAPI openApi, Version version, String mediaType) {
public static String of(OpenAPI openApi, String mediaType) {
if (MediaType.APPLICATION_YAML.equalsIgnoreCase(mediaType)) {
if (Version.OPENAPI_3_1.equals(version)) {
if (SpecVersion.V31.equals(openApi.getSpecVersion())) {
return Yaml31.pretty(openApi);
}
return Yaml.pretty(openApi);
} else if (Version.OPENAPI_3_1.equals(version)) {
} else if (SpecVersion.V31.equals(openApi.getSpecVersion())) {
return Json31.pretty(openApi);
}
return Json.pretty(openApi);
Expand Down
Loading

0 comments on commit 5e9072f

Please sign in to comment.