Skip to content

Commit

Permalink
Add ability to set group specific operation extensions.
Browse files Browse the repository at this point in the history
Fixed #1470
  • Loading branch information
altro3 committed Mar 8, 2024
1 parent 575ea8c commit 5523d67
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand All @@ -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,39 @@
/*
* Copyright 2017-2024 original authors
*
* 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
*
* https://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.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
Expand Up @@ -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.EndpointGroupInfo;
import io.micronaut.openapi.visitor.group.GroupProperties;
import io.micronaut.openapi.visitor.group.GroupProperties.PackageProperties;
import io.micronaut.openapi.visitor.group.RouterVersioningProperties;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, EndpointGroupInfo>();
var excludedGroups = new ArrayList<String>();

ClassElement classEl = methodEl.getDeclaringType();
PackageElement packageEl = classEl.getPackage();
Expand All @@ -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 EndpointGroupInfo(groupProperties.getName()));
}
}
}
Expand Down Expand Up @@ -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, EndpointGroupInfo> 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 EndpointGroupInfo(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, EndpointGroupInfo> 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 EndpointGroupInfo(classGroup));
}
excludedGroups.addAll(classExcludedGroups);

Set<String> allKnownGroups = Utils.getAllKnownGroups();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.EndpointGroupInfo;
import io.micronaut.openapi.visitor.group.GroupProperties;
import io.micronaut.openapi.visitor.group.OpenApiInfo;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
Expand Down Expand Up @@ -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 (EndpointGroupInfo endpointGroupInfo : endpointInfo.getGroups().values()) {
if (CollectionUtils.isNotEmpty(endpointInfo.getExcludedGroups()) && endpointInfo.getExcludedGroups().contains(endpointGroupInfo)) {
continue;
}
OpenAPI newOpenApi = addOpenApiInfo(group, endpointInfo.getVersion(), openApi, result, context);
addOperation(endpointInfo, newOpenApi);
OpenAPI newOpenApi = addOpenApiInfo(endpointGroupInfo.getName(), endpointInfo.getVersion(), openApi, result, context);
addOperation(endpointInfo, newOpenApi, endpointGroupInfo, 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);
}
}
}
Expand All @@ -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 EndpointGroupInfo endpointGroupInfo, VisitorContext context) {
if (openApi == null) {
return;
}
Expand All @@ -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 (endpointGroupInfo != null) {
addExtensions(opCopy, endpointGroupInfo.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 (endpointGroupInfo != null) {
addExtensions(mergedOp, endpointGroupInfo.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(),
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2017-2024 original authors
*
* 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
*
* https://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.micronaut.openapi.visitor.group;

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

import io.micronaut.core.annotation.Internal;

/**
* Entity to storage information about group with specific properties for this operation-group.
*
* @since 6.7.0
*/
@Internal
public final class EndpointGroupInfo {

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

public EndpointGroupInfo(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
Expand Up @@ -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;
Expand All @@ -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, EndpointGroupInfo> 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, EndpointGroupInfo> groups,
List<String> excludedGroups) {
this.url = url;
this.httpMethod = httpMethod;
this.method = method;
Expand Down Expand Up @@ -69,7 +73,7 @@ public String getVersion() {
return version;
}

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

Expand Down
Loading

0 comments on commit 5523d67

Please sign in to comment.