Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to set group specific operation extensions.
Browse files Browse the repository at this point in the history
Fixed #1470
altro3 committed Mar 8, 2024
1 parent 575ea8c commit 9044421
Showing 8 changed files with 315 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -17,19 +17,23 @@

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import io.micronaut.context.annotation.AliasFor;
import io.swagger.v3.oas.annotations.extensions.Extension;

import static java.lang.annotation.RetentionPolicy.SOURCE;

/**
* With this annotation, you can specify one or more groups that this endpoint will be included in,
* as well as specify groups from which this endpoint should be excluded.
* as well as specify groups from which this endpoint should be excluded. Also, you can set
* specific endpoint extensions for each group
*
* @since 4.10.0
*/
@Repeatable(OpenAPIGroups.class)
@Retention(SOURCE)
@Documented
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
@@ -51,4 +55,12 @@
* @return The names of the OpenAPi groups to exclude endpoints from.
*/
String[] exclude() default {};

/**
* The list of optional extensions only for these groups
*
* @return an optional array of extensions
* @since 6.7.0
*/
Extension[] extensions() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.micronaut.openapi.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.SOURCE;

/**
* Allows {@link OpenAPIGroup} to be repeatable.
*
* @since 6.7.0
*/
@Documented
@Retention(SOURCE)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD})
public @interface OpenAPIGroups {

/**
* @return A group of {@link OpenAPIGroup}
*/
OpenAPIGroup[] value() default {};
}
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@
import io.micronaut.openapi.javadoc.JavadocDescription;
import io.micronaut.openapi.swagger.core.util.PrimitiveType;
import io.micronaut.openapi.visitor.group.EndpointInfo;
import io.micronaut.openapi.visitor.group.GroupInfo;
import io.micronaut.openapi.visitor.group.GroupProperties;
import io.micronaut.openapi.visitor.group.GroupProperties.PackageProperties;
import io.micronaut.openapi.visitor.group.RouterVersioningProperties;
@@ -88,6 +89,7 @@
import io.swagger.v3.oas.annotations.enums.Explode;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.tags.Tags;
import io.swagger.v3.oas.models.Components;
@@ -129,6 +131,7 @@
import static io.micronaut.openapi.visitor.SchemaUtils.TYPE_OBJECT;
import static io.micronaut.openapi.visitor.SchemaUtils.getOperationOnPathItem;
import static io.micronaut.openapi.visitor.SchemaUtils.isIgnoredHeader;
import static io.micronaut.openapi.visitor.SchemaUtils.processExtensions;
import static io.micronaut.openapi.visitor.SchemaUtils.setOperationOnPathItem;
import static io.micronaut.openapi.visitor.Utils.DEFAULT_MEDIA_TYPES;
import static io.micronaut.openapi.visitor.Utils.getMediaType;
@@ -1810,8 +1813,8 @@ private void processMicronautVersionAndGroup(io.swagger.v3.oas.models.Operation
+ '#' + CollectionUtils.toString(CollectionUtils.isEmpty(producesMediaTypes) ? DEFAULT_MEDIA_TYPES : producesMediaTypes);

Map<String, GroupProperties> groupPropertiesMap = getGroupsPropertiesMap(context);
List<String> groups = new ArrayList<>();
List<String> excludedGroups = new ArrayList<>();
var groups = new HashMap<String, GroupInfo>();
var excludedGroups = new ArrayList<String>();

ClassElement classEl = methodEl.getDeclaringType();
PackageElement packageEl = classEl.getPackage();
@@ -1828,7 +1831,7 @@ private void processMicronautVersionAndGroup(io.swagger.v3.oas.models.Operation
for (PackageProperties groupPackage : groupProperties.getPackages()) {
boolean isInclude = groupPackage.isIncludeSubpackages() ? packageName.startsWith(groupPackage.getName()) : packageName.equals(groupPackage.getName());
if (isInclude) {
groups.add(groupProperties.getName());
groups.put(groupProperties.getName(), new GroupInfo(groupProperties.getName()));
}
}
}
@@ -1881,28 +1884,52 @@ private void processMicronautVersionAndGroup(io.swagger.v3.oas.models.Operation
excludedGroups));
}

private void processGroups(List<String> groups, List<String> excludedGroups, List<AnnotationValue<OpenAPIGroup>> annotationValues, Map<String, GroupProperties> groupPropertiesMap) {
private void processGroups(Map<String, GroupInfo> groups,
List<String> excludedGroups,
List<AnnotationValue<OpenAPIGroup>> annotationValues,
Map<String, GroupProperties> groupPropertiesMap) {
if (CollectionUtils.isEmpty(annotationValues)) {
return;
}
for (AnnotationValue<OpenAPIGroup> annValue : annotationValues) {
groups.addAll(List.of(annValue.stringValues("value")));
excludedGroups.addAll(List.of(annValue.stringValues("exclude")));

var extensionAnns = annValue.getAnnotations("extensions");
for (var groupName : annValue.stringValues("value")) {
var extensions = new HashMap<CharSequence, Object>();
if (CollectionUtils.isNotEmpty(extensionAnns)) {
for (Object extensionAnn : extensionAnns) {
processExtensions(extensions, (AnnotationValue<Extension>) extensionAnn);
}
}
var groupInfo = groups.get(groupName);
if (groupInfo == null) {
groupInfo = new GroupInfo(groupName);
groups.put(groupName, groupInfo);
}

groupInfo.getExtensions().putAll(extensions);
}
}
Set<String> allKnownGroups = Utils.getAllKnownGroups();
allKnownGroups.addAll(groups);
allKnownGroups.addAll(groups.keySet());
allKnownGroups.addAll(excludedGroups);
}

private void processGroupsFromIncludedEndpoints(List<String> groups, List<String> excludedGroups, String className, Map<String, GroupProperties> groupPropertiesMap) {
private void processGroupsFromIncludedEndpoints(Map<String, GroupInfo> groups, List<String> excludedGroups, String className, Map<String, GroupProperties> groupPropertiesMap) {
if (CollectionUtils.isEmpty(Utils.getIncludedClassesGroups()) && CollectionUtils.isEmpty(Utils.getIncludedClassesGroupsExcluded())) {
return;
}

List<String> classGroups = Utils.getIncludedClassesGroups() != null ? Utils.getIncludedClassesGroups().get(className) : Collections.emptyList();
List<String> classExcludedGroups = Utils.getIncludedClassesGroupsExcluded() != null ? Utils.getIncludedClassesGroupsExcluded().get(className) : Collections.emptyList();

groups.addAll(classGroups);
for (var classGroup : classGroups) {
if (groups.containsKey(classGroup)) {
continue;
}
groups.put(classGroup, new GroupInfo(classGroup));
}
excludedGroups.addAll(classExcludedGroups);

Set<String> allKnownGroups = Utils.getAllKnownGroups();
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@
import io.micronaut.openapi.postprocessors.OpenApiOperationsPostProcessor;
import io.micronaut.openapi.view.OpenApiViewConfig;
import io.micronaut.openapi.visitor.group.EndpointInfo;
import io.micronaut.openapi.visitor.group.GroupInfo;
import io.micronaut.openapi.visitor.group.GroupProperties;
import io.micronaut.openapi.visitor.group.OpenApiInfo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
@@ -499,18 +500,18 @@ private Map<Pair<String, String>, OpenApiInfo> divideOpenapiByGroupsAndVersions(
commonEndpoints.add(endpointInfo);
continue;
}
for (String group : endpointInfo.getGroups()) {
if (CollectionUtils.isNotEmpty(endpointInfo.getExcludedGroups()) && endpointInfo.getExcludedGroups().contains(group)) {
for (GroupInfo groupInfo : endpointInfo.getGroups().values()) {
if (CollectionUtils.isNotEmpty(endpointInfo.getExcludedGroups()) && endpointInfo.getExcludedGroups().contains(groupInfo)) {
continue;
}
OpenAPI newOpenApi = addOpenApiInfo(group, endpointInfo.getVersion(), openApi, result, context);
addOperation(endpointInfo, newOpenApi);
OpenAPI newOpenApi = addOpenApiInfo(groupInfo.getName(), endpointInfo.getVersion(), openApi, result, context);
addOperation(endpointInfo, newOpenApi, groupInfo, context);
}

// if we have only versions without groups
if (CollectionUtils.isEmpty(endpointInfo.getGroups())) {
OpenAPI newOpenApi = addOpenApiInfo(null, endpointInfo.getVersion(), openApi, result, context);
addOperation(endpointInfo, newOpenApi);
addOperation(endpointInfo, newOpenApi, null, context);
}
}
}
@@ -530,14 +531,14 @@ private Map<Pair<String, String>, OpenApiInfo> divideOpenapiByGroupsAndVersions(
if (CollectionUtils.isNotEmpty(commonEndpoint.getExcludedGroups()) && commonEndpoint.getExcludedGroups().contains(group)) {
continue;
}
addOperation(commonEndpoint, groupOpenApi);
addOperation(commonEndpoint, groupOpenApi, null, context);
}
}

return result;
}

private void addOperation(EndpointInfo endpointInfo, OpenAPI openApi) {
private void addOperation(EndpointInfo endpointInfo, OpenAPI openApi, @Nullable GroupInfo groupInfo, VisitorContext context) {
if (openApi == null) {
return;
}
@@ -549,33 +550,55 @@ private void addOperation(EndpointInfo endpointInfo, OpenAPI openApi) {
PathItem pathItem = paths.computeIfAbsent(endpointInfo.getUrl(), (pathUrl) -> new PathItem());
Operation operation = getOperationOnPathItem(pathItem, endpointInfo.getHttpMethod());
if (operation == null) {
setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), endpointInfo.getOperation());
Operation opCopy = null;
try {
opCopy = OpenApiUtils.getJsonMapper().treeToValue(OpenApiUtils.getJsonMapper().valueToTree(endpointInfo.getOperation()), Operation.class);
if (groupInfo != null) {
addExtensions(opCopy, groupInfo.getExtensions());
}
} catch (JsonProcessingException e) {
warn("Error\n" + Utils.printStackTrace(e), context);
}
setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), opCopy != null ? opCopy : endpointInfo.getOperation());
return;
}
setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), SchemaUtils.mergeOperations(operation, endpointInfo.getOperation()));
var mergedOp = SchemaUtils.mergeOperations(operation, endpointInfo.getOperation());
if (groupInfo != null) {
addExtensions(mergedOp, groupInfo.getExtensions());
}
setOperationOnPathItem(pathItem, endpointInfo.getHttpMethod(), mergedOp);
}

private void addExtensions(Operation operation, Map<CharSequence, Object> extensions) {
if (CollectionUtils.isEmpty(extensions)) {
return;
}
for (var ext : extensions.entrySet()) {
operation.addExtension(ext.getKey().toString(), ext.getValue());
}
}

private OpenAPI addOpenApiInfo(String group, String version, OpenAPI openApi,
private OpenAPI addOpenApiInfo(String groupName, String version, OpenAPI openApi,
Map<Pair<String, String>, OpenApiInfo> openApiInfoMap,
VisitorContext context) {
GroupProperties groupProperties = getGroupProperties(group, context);
GroupProperties groupProperties = getGroupProperties(groupName, context);
boolean hasGroupProperties = groupProperties != null;

var key = Pair.of(group, version);
var key = Pair.of(groupName, version);
OpenApiInfo openApiInfo = openApiInfoMap.get(key);
OpenAPI newOpenApi;
if (openApiInfo == null) {

Map<String, OpenAPI> knownOpenApis = Utils.getOpenApis();
if (CollectionUtils.isNotEmpty(knownOpenApis) && knownOpenApis.containsKey(group)) {
newOpenApi = knownOpenApis.get(group);
if (CollectionUtils.isNotEmpty(knownOpenApis) && knownOpenApis.containsKey(groupName)) {
newOpenApi = knownOpenApis.get(groupName);
} else {
newOpenApi = new OpenAPI();
}

openApiInfo = new OpenApiInfo(
version,
group,
groupName,
hasGroupProperties ? groupProperties.getDisplayName() : null,
hasGroupProperties ? groupProperties.getFilename() : null,
!hasGroupProperties || groupProperties.getAdocEnabled() == null || groupProperties.getAdocEnabled(),
@@ -593,12 +616,13 @@ private OpenAPI addOpenApiInfo(String group, String version, OpenAPI openApi,
return null;
}

if (CollectionUtils.isEmpty(knownOpenApis) || !knownOpenApis.containsKey(group)) {
if (CollectionUtils.isEmpty(knownOpenApis) || !knownOpenApis.containsKey(groupName)) {
newOpenApi.setTags(openApiCopy.getTags());
newOpenApi.setServers(openApiCopy.getServers());
newOpenApi.setInfo(openApiCopy.getInfo());
newOpenApi.setSecurity(openApiCopy.getSecurity());
newOpenApi.setExternalDocs(openApiCopy.getExternalDocs());
newOpenApi.setExtensions(openApiCopy.getExtensions());
}

// if we have SecuritySchemes specified only for group
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@
package io.micronaut.openapi.visitor.group;

import java.util.List;
import java.util.Map;

import io.micronaut.core.annotation.Internal;
import io.micronaut.http.HttpMethod;
@@ -36,10 +37,13 @@ public final class EndpointInfo {
private final MethodElement method;
private final Operation operation;
private final String version;
private final List<String> groups;
private final Map<String, GroupInfo> groups;
private final List<String> excludedGroups;

public EndpointInfo(String url, HttpMethod httpMethod, MethodElement method, Operation operation, String version, List<String> groups, List<String> excludedGroups) {
public EndpointInfo(String url, HttpMethod httpMethod, MethodElement method,
Operation operation, String version,
Map<String, GroupInfo> groups,
List<String> excludedGroups) {
this.url = url;
this.httpMethod = httpMethod;
this.method = method;
@@ -69,7 +73,7 @@ public String getVersion() {
return version;
}

public List<String> getGroups() {
public Map<String, GroupInfo> getGroups() {
return groups;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.micronaut.openapi.visitor.group;

import java.util.HashMap;
import java.util.Map;

import io.micronaut.core.annotation.Internal;

@Internal
public final class GroupInfo {

private final String name;
private final Map<CharSequence, Object> extensions = new HashMap<>();

public GroupInfo(String name) {
this.name = name;
}

public String getName() {
return name;
}

public Map<CharSequence, Object> getExtensions() {
return extensions;
}
}
Original file line number Diff line number Diff line change
@@ -85,4 +85,50 @@ class MyBean {}
Files.exists(outputDir.resolve("openapi-explorer").resolve("res").resolve("highlight.min.js"))
Files.exists(outputDir.resolve("openapi-explorer").resolve("res").resolve("openapi-explorer.min.js"))
}

void "test env variables"() {

given:
Path outputDir = Paths.get("output")
System.setProperty(MICRONAUT_OPENAPI_VIEWS_DEST_DIR, outputDir.toString())

when:
buildBeanDefinition('test.MyBean', '''
package test;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.serde.annotation.Serdeable;
@Controller
class PetController {
@Get("/pet")
Pet getPet() {
return new Pet("John");
}
}
/**
*
* @param name The name of the pet
* @author gkrocher
*/
@Serdeable
record Pet(
@NotBlank
@Size(max = 200)
String name
) {}
@jakarta.inject.Singleton
class MyBean {}
''')

then:
Utils.testReference == null
}
}
Original file line number Diff line number Diff line change
@@ -105,4 +105,129 @@ public class MyBean {}
apiPublic.components.securitySchemes.authorizer
apiPublic.components.securitySchemes.authorizer.type == SecurityScheme.Type.APIKEY
}

void "test group specific operation extensions"() {

when:
buildBeanDefinition("test.MyBean", '''
package test;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.openapi.annotation.OpenAPIGroup;
import io.micronaut.openapi.annotation.OpenAPIGroupInfo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
@Controller
class MyController {
@OpenAPIGroup(
names = "private",
extensions = {
@Extension(name = "amazon-apigateway-integration", properties = {
@ExtensionProperty(name = "uri", value = "${amz.private.lambda-integration.uri}"),
@ExtensionProperty(name = "httpMethod", value = "${amz.private.lambda-integration.http-method}")
}),
@Extension(name = "google-apigateway-integration", properties = {
@ExtensionProperty(name = "uri", value = "${google.private.lambda-integration.uri}"),
@ExtensionProperty(name = "httpMethod", value = "${google.private.lambda-integration.http-method}")
})
}
)
@OpenAPIGroup(
names = "public",
extensions = {
@Extension(name = "amazon-apigateway-integration", properties = {
@ExtensionProperty(name = "uri", value = "${amz.public.lambda-integration.uri}"),
@ExtensionProperty(name = "httpMethod", value = "${amz.public.lambda-integration.http-method}")
}),
@Extension(name = "google-apigateway-integration", properties = {
@ExtensionProperty(name = "uri", value = "${google.public.lambda-integration.uri}"),
@ExtensionProperty(name = "httpMethod", value = "${google.public.lambda-integration.http-method}")
})
}
)
@Get("/id/{id}")
String get(String id) {
return null;
}
@OpenAPIGroup("public")
@Get("/name/{name}")
String getByName(String name) {
return null;
}
// common
@Get("/all")
String getAll() {
return null;
}
}
@OpenAPIGroupInfo(
names = "private",
info = @OpenAPIDefinition(
info = @Info(
title = "Private api"
)
)
)
@OpenAPIGroupInfo(
names = "public",
info = @OpenAPIDefinition(
info = @Info(
title = "Public api"
)
)
)
@SecurityScheme(
name = "common",
type = SecuritySchemeType.HTTP,
scheme = "basic",
in = SecuritySchemeIn.HEADER
)
class Application {
}
@jakarta.inject.Singleton
public class MyBean {}
''')

then:
def openApis = Utils.testReferences
openApis
openApis.size() == 2

def apiPrivate = openApis.get(Pair.of("private", null)).getOpenApi()
def apiPublic = openApis.get(Pair.of("public", null)).getOpenApi()

def opPrivateExt = apiPrivate.paths.'/id/{id}'.get.extensions
def opPublicExt = apiPublic.paths.'/id/{id}'.get.extensions

opPrivateExt
opPrivateExt.size() == 2
opPrivateExt.'x-amazon-apigateway-integration'
((Map<String, String>) opPrivateExt.'x-amazon-apigateway-integration').uri == '${amz.private.lambda-integration.uri}'
((Map<String, String>) opPrivateExt.'x-amazon-apigateway-integration').httpMethod == '${amz.private.lambda-integration.http-method}'
opPrivateExt.'x-google-apigateway-integration'
((Map<String, String>) opPrivateExt.'x-google-apigateway-integration').uri == '${google.private.lambda-integration.uri}'
((Map<String, String>) opPrivateExt.'x-google-apigateway-integration').httpMethod == '${google.private.lambda-integration.http-method}'

opPublicExt
opPublicExt.size() == 2
opPublicExt.'x-amazon-apigateway-integration'
((Map<String, String>) opPublicExt.'x-amazon-apigateway-integration').uri == '${amz.public.lambda-integration.uri}'
((Map<String, String>) opPublicExt.'x-amazon-apigateway-integration').httpMethod == '${amz.public.lambda-integration.http-method}'
opPublicExt.'x-google-apigateway-integration'
((Map<String, String>) opPublicExt.'x-google-apigateway-integration').uri == '${google.public.lambda-integration.uri}'
((Map<String, String>) opPublicExt.'x-google-apigateway-integration').httpMethod == '${google.public.lambda-integration.http-method}'
}
}

0 comments on commit 9044421

Please sign in to comment.