Skip to content

Commit

Permalink
Dynamically register /v3/api-docs request mapping, prepare (optional)…
Browse files Browse the repository at this point in the history
… YAML support
  • Loading branch information
neiser committed Dec 5, 2020
1 parent 2801bb7 commit a70ac19
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*-
* #%L
* OpenAPI Generator for Spring Boot :: API
* %%
* Copyright (C) 2020 QAware GmbH
* %%
* 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.
* #L%
*/

package de.qaware.openapigeneratorforspring.common.supplier;

import de.qaware.openapigeneratorforspring.model.OpenApi;


@FunctionalInterface
public interface OpenApiYamlMapper {
String map(OpenApi openApi);
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

import static de.qaware.openapigeneratorforspring.common.util.OpenApiConstants.CONFIG_PROPERTIES_PREFIX;
import static de.qaware.openapigeneratorforspring.common.util.OpenApiConstants.OPEN_API_DOCS_DEFAULT_PATH;

@ConfigurationProperties(prefix = CONFIG_PROPERTIES_PREFIX)
@Getter
Expand All @@ -37,9 +36,10 @@ public class OpenApiConfigurationProperties {
*/
private boolean enabled = true;
/**
* Path to OpenAPI spec. Default is <code>/v3/api-docs</code>.
* Path to OpenAPI spec.
*/
private String apiDocsPath = OPEN_API_DOCS_DEFAULT_PATH;
@SuppressWarnings("java:S1075") // suppress hard coded URL warning
private String apiDocsPath = "/v3/api-docs";

public static class EnabledCondition extends ConfigurationPropertyCondition<OpenApiConfigurationProperties> {
public EnabledCondition() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,4 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class OpenApiConstants {
public static final String CONFIG_PROPERTIES_PREFIX = "openapi-generator-for-spring";
@SuppressWarnings("java:S1075") // suppress hard coded URL warning
public static final String OPEN_API_DOCS_DEFAULT_PATH = "/v3/api-docs";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

package de.qaware.openapigeneratorforspring.autoconfigure;

import de.qaware.openapigeneratorforspring.common.OpenApiConfigurationProperties;
import de.qaware.openapigeneratorforspring.common.OpenApiGenerator;
import de.qaware.openapigeneratorforspring.common.annotation.AnnotationsSupplierFactory;
import de.qaware.openapigeneratorforspring.common.operation.parameter.converter.DefaultParameterMethodConverterFromPathVariableAnnotation;
import de.qaware.openapigeneratorforspring.common.operation.parameter.converter.DefaultParameterMethodConverterFromRequestHeaderAnnotation;
Expand All @@ -29,16 +31,32 @@
import de.qaware.openapigeneratorforspring.common.paths.SpringWebHandlerMethodBuilder;
import de.qaware.openapigeneratorforspring.common.paths.SpringWebRequestMethodEnumMapper;
import de.qaware.openapigeneratorforspring.common.schema.resolver.type.extension.spring.SpringWebResponseEntityInitialTypeBuilder;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiObjectMapperSupplier;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiYamlMapper;
import de.qaware.openapigeneratorforspring.common.web.OpenApiResource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

import java.util.Optional;

@Import({
OpenApiGeneratorWebMethodMergerAutoConfiguration.class,
OpenApiGeneratorWebMethodAutoConfiguration.class
})
public class OpenApiGeneratorWebAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public OpenApiResource openApiResource(
OpenApiGenerator openApiGenerator,
OpenApiConfigurationProperties openApiConfigurationProperties,
OpenApiObjectMapperSupplier openApiObjectMapperSupplier,
Optional<OpenApiYamlMapper> openApiYamlMapper
) {
return new OpenApiResource(openApiGenerator, openApiConfigurationProperties, openApiObjectMapperSupplier, openApiYamlMapper.orElse(null));
}

@Bean
@ConditionalOnMissingBean
public SpringWebHandlerMethodBuilder springWebHandlerMethodBuilder(AnnotationsSupplierFactory annotationsSupplierFactory) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*-
* #%L
* OpenAPI Generator for Spring Boot :: Common
* %%
* Copyright (C) 2020 QAware GmbH
* %%
* 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.
* #L%
*/

package de.qaware.openapigeneratorforspring.common.web;

import com.fasterxml.jackson.core.JsonProcessingException;
import de.qaware.openapigeneratorforspring.common.OpenApiConfigurationProperties;
import de.qaware.openapigeneratorforspring.common.OpenApiGenerator;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiObjectMapperSupplier;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiYamlMapper;
import de.qaware.openapigeneratorforspring.model.OpenApi;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.Arrays;

import static de.qaware.openapigeneratorforspring.common.supplier.OpenApiObjectMapperSupplier.Purpose.OPEN_API_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@RequiredArgsConstructor
@Hidden // exclude from OpenApi spec
@ResponseBody // all handler methods return response bodies, not view names
public class OpenApiResource {

private static final String[] YAML_MEDIA_TYPES = {
// see https://stackoverflow.com/q/332129
// there seems to be no larger consensus what Accept: header tells we shall send YAML
"application/yaml",
"application/x-yaml",
"text/yaml",
"text/x-yaml",
"text/vnd.yaml",
};

private final OpenApiGenerator openApiGenerator;
private final OpenApiConfigurationProperties properties;
private final OpenApiObjectMapperSupplier objectMapperSupplier;
@Nullable
private final OpenApiYamlMapper openApiYamlMapper;

public <T> void registerMapping(RequestMappingInfoBuilder<T> requestMappingInfoBuilder,
RequestMappingRegistrar<T> requestMappingRegistrar,
Object handler) {
requestMappingRegistrar.register(
requestMappingInfoBuilder.build(properties.getApiDocsPath(), APPLICATION_JSON_VALUE),
handler,
findMethod(handler.getClass(), "getOpenApiAsJson")
);
requestMappingRegistrar.register(
requestMappingInfoBuilder.build(properties.getApiDocsPath(), YAML_MEDIA_TYPES),
handler,
findMethod(handler.getClass(), "getOpenApiAsYaml")
);
}

private static Method findMethod(Class<?> clazz, String methodName) {
return Arrays.stream(clazz.getMethods())
.filter(method -> method.getName().equals(methodName))
.reduce((a, b) -> {
throw new IllegalStateException("Found more than one method with name " + methodName + " on " + clazz);
})
.orElseThrow(() -> new IllegalStateException("No method found with name " + methodName + " on " + clazz));
}

public String getOpenApiAsJson() throws JsonProcessingException {
OpenApi openApi = openApiGenerator.generateOpenApi();
return objectMapperSupplier.get(OPEN_API_JSON).writeValueAsString(openApi);
}

public String getOpenApiAsYaml() throws OpenApiYamlNotSupportedException {
if (openApiYamlMapper == null) {
throw new OpenApiYamlNotSupportedException();
}
OpenApi openApi = openApiGenerator.generateOpenApi();
return openApiYamlMapper.map(openApi);
}

public static class OpenApiYamlNotSupportedException extends Exception {

}

public interface RequestMappingInfoBuilder<T> {
T build(String path, String... produces);
}

public interface RequestMappingRegistrar<T> {
void register(T mapping, Object handler, Method method);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,51 @@
package de.qaware.openapigeneratorforspring.autoconfigure;

import de.qaware.openapigeneratorforspring.common.OpenApiConfigurationProperties;
import de.qaware.openapigeneratorforspring.common.OpenApiGenerator;
import de.qaware.openapigeneratorforspring.common.paths.HandlerMethodsProvider;
import de.qaware.openapigeneratorforspring.common.paths.SpringWebHandlerMethodBuilder;
import de.qaware.openapigeneratorforspring.common.paths.SpringWebRequestMethodEnumMapper;
import de.qaware.openapigeneratorforspring.common.schema.resolver.type.TypeResolverForFlux;
import de.qaware.openapigeneratorforspring.common.schema.resolver.type.initial.InitialSchemaBuilderForFlux;
import de.qaware.openapigeneratorforspring.common.schema.resolver.type.initial.InitialTypeBuilderForMono;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiObjectMapperSupplier;
import de.qaware.openapigeneratorforspring.common.web.OpenApiResource;
import de.qaware.openapigeneratorforspring.webflux.HandlerMethodsProviderForWebFlux;
import de.qaware.openapigeneratorforspring.webflux.OpenApiBaseUriSupplierForWebFlux;
import de.qaware.openapigeneratorforspring.webflux.OpenApiRequestAwareProviderForWebFlux;
import de.qaware.openapigeneratorforspring.webflux.OpenApiResourceForWebFlux;
import de.qaware.openapigeneratorforspring.webflux.function.RouterFunctionHandlerMethodWithInfoBuilder;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;

import java.util.Map;

import static org.springframework.web.bind.annotation.RequestMethod.GET;

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@Import(OpenApiGeneratorWebFluxRouterFunctionAutoConfiguration.class)
public class OpenApiGeneratorWebFluxAutoConfiguration {

@Bean
@ConditionalOnMissingBean
@Conditional(OpenApiConfigurationProperties.EnabledCondition.class)
public OpenApiResourceForWebFlux openApiResource(OpenApiGenerator openApiGenerator, OpenApiObjectMapperSupplier openApiObjectMapperSupplier) {
return new OpenApiResourceForWebFlux(openApiGenerator, openApiObjectMapperSupplier);
public InitializingBean openApiResourceRegistration(
OpenApiResourceForWebFlux openApiResourceForWebFlux,
RequestMappingHandlerMapping requestMappingHandlerMapping
) {
return () -> openApiResourceForWebFlux.registerMapping(
(path, produces) -> RequestMappingInfo.paths(path).methods(GET).produces(produces).build(),
requestMappingHandlerMapping::registerMapping
);
}

@Bean
public OpenApiResourceForWebFlux openApiResourceForWebFlux(OpenApiResource openApiResource) {
return new OpenApiResourceForWebFlux(openApiResource);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,16 @@
package de.qaware.openapigeneratorforspring.webflux;

import com.fasterxml.jackson.core.JsonProcessingException;
import de.qaware.openapigeneratorforspring.common.AbstractOpenApiResource;
import de.qaware.openapigeneratorforspring.common.OpenApiGenerator;
import de.qaware.openapigeneratorforspring.common.supplier.OpenApiObjectMapperSupplier;
import org.springframework.http.MediaType;
import de.qaware.openapigeneratorforspring.common.web.OpenApiResource;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ResponseBody;

@RestController
public class OpenApiResourceForWebFlux extends AbstractOpenApiResource {
@RequiredArgsConstructor
@Hidden // exclude from OpenApi spec
@ResponseBody // all handler methods return response bodies, not view names
public class OpenApiResourceForWebFlux {

/**
* Using this thread local only works if the OpenApi model is built within
Expand All @@ -42,13 +42,22 @@ public class OpenApiResourceForWebFlux extends AbstractOpenApiResource {
*/
static final ThreadLocal<ServerHttpRequest> SERVER_HTTP_REQUEST_THREAD_LOCAL = new ThreadLocal<>();

public OpenApiResourceForWebFlux(OpenApiGenerator openApiGenerator, OpenApiObjectMapperSupplier objectMapperSupplier) {
super(openApiGenerator, objectMapperSupplier);
private final OpenApiResource openApiResource;

public <T> void registerMapping(OpenApiResource.RequestMappingInfoBuilder<T> requestMappingInfoBuilder,
OpenApiResource.RequestMappingRegistrar<T> requestMappingRegistrar) {
openApiResource.registerMapping(requestMappingInfoBuilder, requestMappingRegistrar, this);
}

@GetMapping(value = API_DOCS_PATH_SPEL, produces = MediaType.APPLICATION_JSON_VALUE)
@SuppressWarnings("unused") // is used via reflection in registerMapping
public String getOpenApiAsJson(ServerHttpRequest serverHttpRequest) throws JsonProcessingException {
SERVER_HTTP_REQUEST_THREAD_LOCAL.set(serverHttpRequest);
return super.getOpenApiAsJson();
return openApiResource.getOpenApiAsJson();
}

@SuppressWarnings("unused") // is used via reflection in registerMapping
public String getOpenApiAsYaml(ServerHttpRequest serverHttpRequest) throws OpenApiResource.OpenApiYamlNotSupportedException {
SERVER_HTTP_REQUEST_THREAD_LOCAL.set(serverHttpRequest);
return openApiResource.getOpenApiAsYaml();
}
}

0 comments on commit a70ac19

Please sign in to comment.