diff --git a/build.gradle.kts b/build.gradle.kts index 0d0c30d..c6e8b0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-engine:latest.release") testRuntimeOnly("io.swagger:swagger-annotations:1.6.13") + testRuntimeOnly("jakarta.ws.rs:jakarta.ws.rs-api:3.1.0") testRuntimeOnly("org.gradle:gradle-tooling-api:latest.release") } diff --git a/src/main/java/org/openrewrite/openapi/swagger/AnnotationUtils.java b/src/main/java/org/openrewrite/openapi/swagger/AnnotationUtils.java new file mode 100644 index 0000000..104bc31 --- /dev/null +++ b/src/main/java/org/openrewrite/openapi/swagger/AnnotationUtils.java @@ -0,0 +1,45 @@ +/* + * 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 lombok.experimental.UtilityClass; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +@UtilityClass +class AnnotationUtils { + public static Map extractAnnotationArgumentAssignments(J.Annotation annotation) { + if (annotation.getArguments() == null || + annotation.getArguments().isEmpty() || + annotation.getArguments().get(0) instanceof J.Empty) { + return emptyMap(); + } + Map map = new HashMap<>(); + for (Expression expression : annotation.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; + } +} diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java index 214d240..4194bac 100644 --- a/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateApiToTag.java @@ -27,11 +27,9 @@ 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; @@ -42,31 +40,33 @@ public class MigrateApiToTag extends Recipe { 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" + - "}"; + 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" + - "}"; + 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() { @@ -89,7 +89,7 @@ public TreeVisitor getVisitor() { 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); + Map annotationArgumentAssignments = AnnotationUtils.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); @@ -102,23 +102,6 @@ public TreeVisitor 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); diff --git a/src/main/java/org/openrewrite/openapi/swagger/MigrateSwaggerDefinitionToOpenAPIDefinition.java b/src/main/java/org/openrewrite/openapi/swagger/MigrateSwaggerDefinitionToOpenAPIDefinition.java new file mode 100644 index 0000000..6b35f18 --- /dev/null +++ b/src/main/java/org/openrewrite/openapi/swagger/MigrateSwaggerDefinitionToOpenAPIDefinition.java @@ -0,0 +1,116 @@ +/* + * 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.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.List; +import java.util.Map; + +public class MigrateSwaggerDefinitionToOpenAPIDefinition extends Recipe { + + private static final String FQN_SWAGGER_DEFINITION = "io.swagger.annotations.SwaggerDefinition"; + private static final String FQN_OPENAPI_DEFINITION = "io.swagger.v3.oas.annotations.OpenAPIDefinition"; + private static final String FQN_SERVER = "io.swagger.v3.oas.annotations.servers.Server"; + + @Override + public String getDisplayName() { + return "Migrate from `@SwaggerDefinition` to `@OpenAPIDefinition`"; + } + + @Override + public String getDescription() { + return "Migrate from `@SwaggerDefinition` to `@OpenAPIDefinition`."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check( + new UsesType<>(FQN_SWAGGER_DEFINITION, false), + new JavaIsoVisitor() { + private final AnnotationMatcher annotationMatcher = new AnnotationMatcher(FQN_SWAGGER_DEFINITION); + + @Override + public J.@Nullable Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) { + J.Annotation ann = super.visitAnnotation(annotation, ctx); + + if (annotationMatcher.matches(ann)) { + Map args = AnnotationUtils.extractAnnotationArgumentAssignments(ann); + + StringBuilder tpl = new StringBuilder("@OpenAPIDefinition(\n"); + List tplArgs = new ArrayList<>(); + List parts = new ArrayList<>(); + + Expression basePath = args.get("basePath"); + Expression host = args.get("host"); + Expression schemes = args.get("schemes"); + String servers = ""; + if (basePath != null && host != null && schemes != null) { + tpl.append("servers = {\n"); + if (schemes instanceof J.FieldAccess) { + servers += "@Server(url = \"" + ((J.FieldAccess) schemes).getSimpleName().toLowerCase() + "://" + host + basePath + "\")"; + } else if (schemes instanceof J.NewArray) { + for (Expression scheme : ((J.NewArray) schemes).getInitializer()) { + if (!servers.isEmpty()) { + servers += ",\n"; + } + String schemeName = ((J.FieldAccess) scheme).getSimpleName().toLowerCase(); + servers += "@Server(url = \"" + schemeName + "://" + host + basePath + "\")"; + } + } + servers += "\n}"; + parts.add(servers); + } + + args.remove("basePath"); + args.remove("host"); + args.remove("schemes"); + args.remove("produces"); + args.remove("consumes"); + for (Map.Entry arg : args.entrySet()) { + parts.add(arg.getKey() + " = #{any()}"); + tplArgs.add(arg.getValue()); + } + tpl.append(String.join(",\n", parts)); + tpl.append("\n)"); + + ann = JavaTemplate.builder(tpl.toString()) + .imports(FQN_OPENAPI_DEFINITION, FQN_SERVER) + .javaParser(JavaParser.fromJavaVersion().classpath("swagger-annotations")) + .build() + .apply(updateCursor(ann), ann.getCoordinates().replace(), tplArgs.toArray()); + maybeRemoveImport(FQN_SWAGGER_DEFINITION); + maybeAddImport(FQN_OPENAPI_DEFINITION, false); + maybeAddImport(FQN_SERVER, false); + ann = maybeAutoFormat(annotation, ann, ctx); + } + + doAfterVisit(new RemoveUnusedImports().getVisitor()); + return ann; + } + } + ); + } +} diff --git a/src/main/resources/META-INF/rewrite/swagger-2.yml b/src/main/resources/META-INF/rewrite/swagger-2.yml index c56a588..0865a56 100644 --- a/src/main/resources/META-INF/rewrite/swagger-2.yml +++ b/src/main/resources/META-INF/rewrite/swagger-2.yml @@ -43,6 +43,9 @@ recipeList: - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: io.swagger.annotations.Tag newFullyQualifiedTypeName: io.swagger.v3.oas.annotations.tags.Tag + - org.openrewrite.java.ChangeType: + oldFullyQualifiedTypeName: io.swagger.annotations.Info + newFullyQualifiedTypeName: io.swagger.v3.oas.annotations.info.Info - org.openrewrite.java.ChangeType: oldFullyQualifiedTypeName: springfox.documentation.annotations.ApiIgnore newFullyQualifiedTypeName: io.swagger.v3.oas.annotations.Hidden @@ -53,6 +56,7 @@ recipeList: - org.openrewrite.openapi.swagger.MigrateApiParamToParameter - org.openrewrite.openapi.swagger.MigrateApiModelPropertyToSchema - org.openrewrite.openapi.swagger.MigrateApiModelToSchema + - org.openrewrite.openapi.swagger.MigrateSwaggerDefinitionToOpenAPIDefinition # todo add swagger-core to common-dependencies diff --git a/src/test/java/org/openrewrite/openapi/swagger/SwaggerToOpenAPITest.java b/src/test/java/org/openrewrite/openapi/swagger/SwaggerToOpenAPITest.java index b48d1f1..a0f8ea4 100644 --- a/src/test/java/org/openrewrite/openapi/swagger/SwaggerToOpenAPITest.java +++ b/src/test/java/org/openrewrite/openapi/swagger/SwaggerToOpenAPITest.java @@ -30,7 +30,7 @@ class SwaggerToOpenAPITest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { spec.recipeFromResources("org.openrewrite.openapi.swagger.SwaggerToOpenAPI") - .parser(JavaParser.fromJavaVersion().classpath("swagger-annotations-1.+", "swagger-annotations-2.+")); + .parser(JavaParser.fromJavaVersion().classpath("swagger-annotations-1.+", "swagger-annotations-2.+", "rs-api")); } @Test @@ -47,8 +47,6 @@ void shouldChangeSwaggerArtifacts() { //language=java java( """ - package example.org; - import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -59,8 +57,6 @@ class Example { } """, """ - package example.org; - import io.swagger.v3.oas.annotations.media.Schema; @Schema(name="ApiModelExampleValue", description="ApiModelExampleDescription") @@ -158,4 +154,82 @@ public void create(Example foo) { ) ); } + + @Test + void migrateSwaggerDefinitionsToOpenAPIDefinitionSingleSchema() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Info; + import io.swagger.annotations.SwaggerDefinition; + import jakarta.ws.rs.core.MediaType; + + @SwaggerDefinition( + basePath = "/api", + host="example.com", + info = @Info(title = "Example", version = "V1.0"), + consumes = { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }, + produces = { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }, + schemes = SwaggerDefinition.Scheme.HTTPS + ) + class Example { + } + """, + """ + import io.swagger.v3.oas.annotations.OpenAPIDefinition; + import io.swagger.v3.oas.annotations.info.Info; + import io.swagger.v3.oas.annotations.servers.Server; + + @OpenAPIDefinition( + servers = { + @Server(url = "https://example.com/api") + }, + info = @Info(title = "Example", version = "V1.0") + ) + class Example { + } + """ + ) + ); + } + + @Test + void migrateSwaggerDefinitionsToOpenAPIDefinitionMultipleSchema() { + rewriteRun( + //language=java + java( + """ + import io.swagger.annotations.Info; + import io.swagger.annotations.SwaggerDefinition; + import jakarta.ws.rs.core.MediaType; + + @SwaggerDefinition( + basePath = "/api", + host="example.com", + info = @Info(title = "Example", version = "V1.0"), + consumes = { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }, + produces = { MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }, + schemes = { SwaggerDefinition.Scheme.HTTP, SwaggerDefinition.Scheme.HTTPS }) + class Example { + } + """, + """ + import io.swagger.v3.oas.annotations.OpenAPIDefinition; + import io.swagger.v3.oas.annotations.info.Info; + import io.swagger.v3.oas.annotations.servers.Server; + + @OpenAPIDefinition( + servers = { + @Server(url = "http://example.com/api"), + @Server(url = "https://example.com/api") + }, + info = @Info(title = "Example", version = "V1.0") + ) + class Example { + } + """ + ) + ); + } }