From 74ccc1a9b6437be7eb5af660ab05bad1f53a5aae Mon Sep 17 00:00:00 2001 From: SiBorea <108953913+SiBorea@users.noreply.github.com> Date: Sun, 24 Nov 2024 18:55:53 +0800 Subject: [PATCH] Handle multiple tags in @Api (#21) * Handle multiple tags in @Api * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Apply formatter and fix compilation * Adopt JavaTemplate to create `@Tags` annotation * Handle cases with a single tag * Remove unnecessary import for singular tags --------- Co-authored-by: Tim te Beek Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tim te Beek --- .../openapi/swagger/MigrateApiToTag.java | 198 ++++++++++++++++++ .../resources/META-INF/rewrite/swagger-2.yml | 17 -- .../openapi/swagger/MigrateApiToTagTest.java | 122 +++++++++++ 3 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java create mode 100644 src/test/java/org/openrewrite/openapi/swagger/MigrateApiToTagTest.java diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java new file mode 100644 index 0000000..214d240 --- /dev/null +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java @@ -0,0 +1,198 @@ +/* + * Copyright 2024 the original author or 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 org.openrewrite.openapi.swagger; + +import org.intellij.lang.annotations.Language; +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.*; +import org.openrewrite.java.search.UsesType; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Collections.emptyMap; +import static java.util.Comparator.comparing; +import static java.util.Objects.requireNonNull; + +public class MigrateApiToTag extends Recipe { + + private static final String FQN_API = "io.swagger.annotations.Api"; + private static final String FQN_TAG = "io.swagger.v3.oas.annotations.tags.Tag"; + private static final String FQN_TAGS = "io.swagger.v3.oas.annotations.tags.Tags"; + + @Language("java") + private static final String TAGS_CLASS = "package io.swagger.v3.oas.annotations.tags;\n" + + "import java.lang.annotation.ElementType;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.RetentionPolicy;\n" + + "import java.lang.annotation.Target;\n" + + "@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})\n" + + "@Retention(RetentionPolicy.RUNTIME)\n" + + "public @interface Tags {\n" + + " Tag[] value() default {};\n" + + "}"; + + @Language("java") + private static final String TAG_CLASS = "package io.swagger.v3.oas.annotations.tags;\n" + + "import java.lang.annotation.ElementType;\n" + + "import java.lang.annotation.Repeatable;\n" + + "import java.lang.annotation.Retention;\n" + + "import java.lang.annotation.RetentionPolicy;\n" + + "import java.lang.annotation.Target;\n" + + "@Target({ElementType.METHOD, ElementType.TYPE, ElementType.ANNOTATION_TYPE})\n" + + "@Retention(RetentionPolicy.RUNTIME)\n" + + "@Repeatable(Tags.class)\n" + + "public @interface Tag {\n" + + " String name();\n" + + " String description() default \"\";\n" + + "}"; + + @Override + public String getDisplayName() { + return "Migrate from `@Api` to `@Tag`"; + } + + @Override + public String getDescription() { + return "Converts `@Api` to `@Tag` annotation and converts the directly mappable attributes and removes the others."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesType<>(FQN_API, false), + new JavaIsoVisitor() { + private final AnnotationMatcher apiMatcher = new AnnotationMatcher(FQN_API); + + @Override + public J.@Nullable Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation ann = super.visitAnnotation(annotation, ctx); + if (apiMatcher.matches(ann)) { + Map annotationArgumentAssignments = extractAnnotationArgumentAssignments(ann); + if (annotationArgumentAssignments.get("tags") != null) { + // Remove @Api and add @Tag or @Tags at class level + getCursor().putMessageOnFirstEnclosing(J.ClassDeclaration.class, FQN_API, annotationArgumentAssignments); + maybeRemoveImport(FQN_API); + return null; + } + doAfterVisit(new ChangeAnnotationAttributeName(FQN_API, "value", "name").getVisitor()); + doAfterVisit(new ChangeType(FQN_API, FQN_TAG, true).getVisitor()); + } + return ann; + } + + private Map extractAnnotationArgumentAssignments(J.Annotation apiAnnotation) { + if (apiAnnotation.getArguments() == null || + apiAnnotation.getArguments().isEmpty() || + apiAnnotation.getArguments().get(0) instanceof J.Empty) { + return emptyMap(); + } + Map map = new HashMap<>(); + for (Expression expression : apiAnnotation.getArguments()) { + if (expression instanceof J.Assignment) { + J.Assignment a = (J.Assignment) expression; + String simpleName = ((J.Identifier) a.getVariable()).getSimpleName(); + map.put(simpleName, a.getAssignment()); + } + } + return map; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + Map annotationArguments = getCursor().getMessage(FQN_API); + if (annotationArguments == null) { + return cd; + } + + Expression descriptionAssignment = annotationArguments.get("description"); + Expression tagsAssignment = annotationArguments.get("tags"); + + if (tagsAssignment instanceof J.NewArray) { + J.NewArray newArray = (J.NewArray) tagsAssignment; + List initializer = requireNonNull(newArray.getInitializer()); + if (initializer.size() == 1) { + cd = addTagAnnotation(cd, initializer.get(0), descriptionAssignment); + } else { + cd = addTagsAnnotation(cd, initializer, descriptionAssignment); + } + } else { + cd = addTagAnnotation(cd, tagsAssignment, descriptionAssignment); + } + return maybeAutoFormat(classDecl, cd, cd.getName(), ctx, getCursor().getParentTreeCursor()); + } + + private J.ClassDeclaration addTagsAnnotation(J.ClassDeclaration cd, List tagsAssignments, @Nullable Expression descriptionAssignment) { + // Create template for @Tags annotation + StringBuilder template = new StringBuilder("@Tags({"); + List templateArgs = new ArrayList<>(); + for (Expression expression : tagsAssignments) { + if (!templateArgs.isEmpty()) { + template.append(","); + } + template.append("\n@Tag(name = #{any()}"); + templateArgs.add(expression); + if (descriptionAssignment != null) { + template.append(", description = #{any()}"); + templateArgs.add(descriptionAssignment); + } + template.append(")"); + } + template.append("\n})"); + + // Add formatted template and imports + maybeAddImport(FQN_TAG); + maybeAddImport(FQN_TAGS); + return JavaTemplate.builder(template.toString()) + .imports(FQN_TAGS, FQN_TAG) + .javaParser(JavaParser.fromJavaVersion().dependsOn(TAGS_CLASS, TAG_CLASS)) + .build() + .apply(updateCursor(cd), cd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)), templateArgs.toArray()); + } + + private J.ClassDeclaration addTagAnnotation(J.ClassDeclaration cd, Expression tagsAssignment, @Nullable Expression descriptionAssignment) { + // Create template for @Tags annotation + StringBuilder template = new StringBuilder("@Tag(name = #{any()}"); + List templateArgs = new ArrayList<>(); + templateArgs.add(tagsAssignment); + if (descriptionAssignment != null) { + template.append(", description = #{any()}"); + templateArgs.add(descriptionAssignment); + } + template.append(")"); + + // Add formatted template and imports + maybeAddImport(FQN_TAG); + return JavaTemplate.builder(template.toString()) + .imports(FQN_TAG) + .javaParser(JavaParser.fromJavaVersion().dependsOn(TAGS_CLASS, TAG_CLASS)) + .build() + .apply(updateCursor(cd), cd.getCoordinates().addAnnotation(comparing(J.Annotation::getSimpleName)), templateArgs.toArray()); + } + } + ); + } +} diff --git a/src/main/resources/META-INF/rewrite/swagger-2.yml b/src/main/resources/META-INF/rewrite/swagger-2.yml index 5675e7d..6e0a24d 100644 --- a/src/main/resources/META-INF/rewrite/swagger-2.yml +++ b/src/main/resources/META-INF/rewrite/swagger-2.yml @@ -191,23 +191,6 @@ recipeList: annotationType: io.swagger.v3.oas.annotations.Parameter attributeName: allowMultiple ---- -type: specs.openrewrite.org/v1beta/recipe -name: org.openrewrite.openapi.swagger.MigrateApiToTag -displayName: Migrate from `@Api` to `@Tag` -description: Converts `@Api` to `@Tag` annotation and converts the directly mappable attributes and removes the others. -tags: - - swagger - - openapi -recipeList: - - org.openrewrite.java.ChangeAnnotationAttributeName: - annotationType: io.swagger.annotations.Api - oldAttributeName: "value" - newAttributeName: "name" - - org.openrewrite.java.ChangeType: - oldFullyQualifiedTypeName: io.swagger.annotations.Api - newFullyQualifiedTypeName: io.swagger.v3.oas.annotations.tags.Tag - --- type: specs.openrewrite.org/v1beta/recipe name: org.openrewrite.openapi.swagger.MigrateApiParamToParameter diff --git a/src/test/java/org/openrewrite/openapi/swagger/MigrateApiToTagTest.java b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiToTagTest.java new file mode 100644 index 0000000..3e3eddc --- /dev/null +++ b/src/test/java/org/openrewrite/openapi/swagger/MigrateApiToTagTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024 the original author or 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 org.openrewrite.openapi.swagger; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class MigrateApiToTagTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateApiToTag()) + .parser(JavaParser.fromJavaVersion().classpath("swagger-annotations-1.+")); + } + + @DocumentExample + @Test + void single() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Api; + + @Api(value = "Bar") + class Example {} + """, + """ + import io.swagger.v3.oas.annotations.tags.Tag; + + @Tag(name = "Bar") + class Example {} + """ + ) + ); + } + + @Test + void singleTagAsArray() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Api; + + @Api(tags = {"foo"}, value = "Ignore", description = "Desc") + class Example {} + """, + """ + import io.swagger.v3.oas.annotations.tags.Tag; + + @Tag(name = "foo", description = "Desc") + class Example {} + """ + ) + ); + } + + @Test + void singleTagAsLiteral() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Api; + + @Api(tags = "foo", value = "Ignore", description = "Desc") + class Example {} + """, + """ + import io.swagger.v3.oas.annotations.tags.Tag; + + @Tag(name = "foo", description = "Desc") + class Example {} + """ + ) + ); + } + + @Test + void multipleTags() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Api; + + @Api(tags = {"foo", "bar"}, value = "Ignore", description = "Desc") + class Example {} + """, + """ + import io.swagger.v3.oas.annotations.tags.Tag; + import io.swagger.v3.oas.annotations.tags.Tags; + + @Tags({ + @Tag(name = "foo", description = "Desc"), + @Tag(name = "bar", description = "Desc") + }) + class Example {} + """ + ) + ); + } +}