Skip to content

Commit

Permalink
Handle multiple tags in @Api (#21)
Browse files Browse the repository at this point in the history
* 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 <timtebeek@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tim te Beek <tim@moderne.io>
  • Loading branch information
4 people authored Nov 24, 2024
1 parent 6fdfb9d commit 74ccc1a
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 17 deletions.
198 changes: 198 additions & 0 deletions src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<?, ExecutionContext> getVisitor() {
return Preconditions.check(
new UsesType<>(FQN_API, false),
new JavaIsoVisitor<ExecutionContext>() {
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<String, Expression> 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<String, Expression> extractAnnotationArgumentAssignments(J.Annotation apiAnnotation) {
if (apiAnnotation.getArguments() == null ||
apiAnnotation.getArguments().isEmpty() ||
apiAnnotation.getArguments().get(0) instanceof J.Empty) {
return emptyMap();
}
Map<String, Expression> 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<String, Expression> 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<Expression> 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<Expression> tagsAssignments, @Nullable Expression descriptionAssignment) {
// Create template for @Tags annotation
StringBuilder template = new StringBuilder("@Tags({");
List<Expression> 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<Expression> 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());
}
}
);
}
}
17 changes: 0 additions & 17 deletions src/main/resources/META-INF/rewrite/swagger-2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
122 changes: 122 additions & 0 deletions src/test/java/org/openrewrite/openapi/swagger/MigrateApiToTagTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 {}
"""
)
);
}
}

0 comments on commit 74ccc1a

Please sign in to comment.