diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java index 6d42a3f5c..1be34200d 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java @@ -99,6 +99,8 @@ public class Settings { public boolean generateSpringApplicationInterface = false; public boolean generateSpringApplicationClient = false; public boolean scanSpringApplication; + public List> springCustomQueryParameterAnnotations = new ArrayList<>(); + public List> springCustomRequestBodyAnnotations = new ArrayList<>(); @Deprecated public RestNamespacing jaxrsNamespacing; @Deprecated public Class jaxrsNamespacingAnnotation = null; @Deprecated public String jaxrsNamespacingAnnotationElement; // default is "value" @@ -254,6 +256,14 @@ public void loadIncludePropertyAnnotations(ClassLoader classLoader, List this.includePropertyAnnotations = loadClasses(classLoader, includePropertyAnnotations, Annotation.class); } + public void loadSpringCustomQueryParameterAnnotations(ClassLoader classLoader, List springCustomQueryParameterAnnotations) { + this.springCustomQueryParameterAnnotations = loadClasses(classLoader, springCustomQueryParameterAnnotations, Annotation.class); + } + + public void loadSpringCustomRequestBodyAnnotations(ClassLoader classLoader, List springCustomRequestBodyAnnotations) { + this.springCustomRequestBodyAnnotations = loadClasses(classLoader, springCustomRequestBodyAnnotations, Annotation.class); + } + public void loadExcludePropertyAnnotations(ClassLoader classLoader, List excludePropertyAnnotations) { this.excludePropertyAnnotations = loadClasses(classLoader, excludePropertyAnnotations, Annotation.class); } @@ -291,7 +301,7 @@ public static Map convertToMap(List items, String itemNa } return result; } - + public void validate() { if (classLoader == null) { classLoader = Thread.currentThread().getContextClassLoader(); diff --git a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java index a8fb841a1..8b290ad31 100644 --- a/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java +++ b/typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java @@ -58,6 +58,8 @@ public class GenerateTask extends DefaultTask { public List excludeClassPatterns; public List includePropertyAnnotations; public List excludePropertyAnnotations; + public List springCustomQueryParameterAnnotations; + public List springCustomRequestBodyAnnotations; public JsonLibrary jsonLibrary; public Jackson2Configuration jackson2Configuration; public GsonConfiguration gsonConfiguration; @@ -201,6 +203,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.loadExtensions(classLoader, Utils.concat(extensionClasses, extensions), extensionsWithConfiguration); settings.loadIncludePropertyAnnotations(classLoader, includePropertyAnnotations); settings.loadExcludePropertyAnnotations(classLoader, excludePropertyAnnotations); + settings.loadSpringCustomQueryParameterAnnotations(classLoader, springCustomQueryParameterAnnotations); + settings.loadSpringCustomRequestBodyAnnotations(classLoader, springCustomRequestBodyAnnotations); settings.loadOptionalAnnotations(classLoader, optionalAnnotations); settings.loadRequiredAnnotations(classLoader, requiredAnnotations); settings.loadNullableAnnotations(classLoader, nullableAnnotations); diff --git a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java index 8c6ad8f49..b41971c46 100644 --- a/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java +++ b/typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java @@ -558,6 +558,22 @@ public class GenerateMojo extends AbstractMojo { @Parameter private boolean scanSpringApplication; + /** + * Additional annotations which are treated as optional request parameters during Spring controllers parsing when Spring's + * RequestParam or ModelAttribute is not used on a specific method's argument. + * This option may be useful when Spring controller method accepts custom resolved parameters. + */ + @Parameter + private List springCustomQueryParameterAnnotations; + + /** + * Additional annotations which are treated as request body during Spring controllers parsing when Spring's + * RequestBody is not used on a specific method's argument. + * This option may be useful when Spring controller method accepts custom resolved parameters. + */ + @Parameter + private List springCustomRequestBodyAnnotations; + /** * Deprecated, use {@link #restNamespacing}. */ @@ -787,7 +803,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private String npmBuildScript; - + /** * List of additional NPM dependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -796,7 +812,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private List npmDependencies; - + /** * List of additional NPM devDependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -805,7 +821,7 @@ public class GenerateMojo extends AbstractMojo { */ @Parameter private List npmDevDependencies; - + /** * List of additional NPM peerDependencies.
* Only applicable when {@link #generateNpmPackageJson} parameter is true and generating implementation file (.ts).
@@ -959,6 +975,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.loadExtensions(classLoader, extensions, extensionsWithConfiguration); settings.loadIncludePropertyAnnotations(classLoader, includePropertyAnnotations); settings.loadExcludePropertyAnnotations(classLoader, excludePropertyAnnotations); + settings.loadSpringCustomQueryParameterAnnotations(classLoader, springCustomQueryParameterAnnotations); + settings.loadSpringCustomRequestBodyAnnotations(classLoader, springCustomRequestBodyAnnotations); settings.loadOptionalAnnotations(classLoader, optionalAnnotations); settings.loadRequiredAnnotations(classLoader, requiredAnnotations); settings.loadNullableAnnotations(classLoader, nullableAnnotations); diff --git a/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java b/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java index 99b36662f..9e4fe0253 100644 --- a/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java +++ b/typescript-generator-spring/src/main/java/cz/habarta/typescript/generator/spring/SpringApplicationParser.java @@ -26,6 +26,7 @@ import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; @@ -304,6 +305,7 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp queryParams.add(new RestQueryParam.Single(new MethodParameterModel("size", Long.class), false)); queryParams.add(new RestQueryParam.Single(new MethodParameterModel("sort", String.class), false)); } else { + boolean queryParamAdded = false; final RequestParam requestParamAnnotation = AnnotationUtils.findAnnotation(parameter, RequestParam.class); if (requestParamAnnotation != null) { if (parameter.getType() == MultiValueMap.class) { @@ -315,6 +317,7 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp parameter.getName() ), parameter.getParameterizedType()), isRequired)); foundType(result, parameter.getParameterizedType(), controllerClass, method.getName()); + queryParamAdded = true; } } @@ -330,12 +333,26 @@ private void parseControllerMethod(JaxrsApplicationParser.Result result, JaxrsAp propertyDescriptor.getPropertyType() ), false)); foundType(result, propertyDescriptor.getPropertyType(), controllerClass, method.getName()); + queryParamAdded = true; } } } catch (IntrospectionException e) { TypeScriptGenerator.getLogger().warning(String.format("Cannot introspect '%s' class: " + e.getMessage(), parameter.getAnnotatedType())); } } + + if(!queryParamAdded){ + for(Class customRequestParamAnnotationClass : settings.springCustomQueryParameterAnnotations){ + Annotation customRequestParamAnnotation = AnnotationUtils.findAnnotation(parameter, customRequestParamAnnotationClass); + if(customRequestParamAnnotation != null){ + queryParams.add(new RestQueryParam.Single(new MethodParameterModel( + parameter.getName(), + parameter.getParameterizedType()), false)); + foundType(result, parameter.getParameterizedType(), controllerClass, method.getName()); + break; + } + } + } } } @@ -373,8 +390,16 @@ private MethodParameterModel getEntityParameter(Class controller, Method meth final List parameterTypes = settings.getTypeParser().getMethodParameterTypes(method); final List> parameters = Utils.zip(Arrays.asList(method.getParameters()), parameterTypes); for (Pair pair : parameters) { - final RequestBody requestBodyAnnotation = AnnotationUtils.findAnnotation(pair.getValue1(), RequestBody.class); - if (requestBodyAnnotation != null) { + boolean hasRequestBody = AnnotationUtils.findAnnotation(pair.getValue1(), RequestBody.class) != null; + if(!hasRequestBody){ + for(Class customRequestBodyAnnotationClass : settings.springCustomRequestBodyAnnotations){ + if(AnnotationUtils.findAnnotation(pair.getValue1(), customRequestBodyAnnotationClass) != null){ + hasRequestBody = true; + break; + } + } + } + if (hasRequestBody) { final Type resolvedType = GenericsResolver.resolveType(controller, pair.getValue2(), method.getDeclaringClass()); return new MethodParameterModel(pair.getValue1().getName(), resolvedType); } diff --git a/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java index a2a932bef..f8f962b28 100644 --- a/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java +++ b/typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java @@ -9,8 +9,10 @@ import cz.habarta.typescript.generator.util.Utils; import io.swagger.annotations.ApiOperation; import io.swagger.v3.oas.annotations.Operation; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; @@ -35,7 +37,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; - public class SpringTest { @Test @@ -169,6 +170,27 @@ public void testInheritance() { Assertions.assertFalse(output.contains("uriEncoding`test/b`")); } + @Test + public void testCustomQueryParameters() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.springCustomQueryParameterAnnotations = Arrays.asList(CustomRequestParam.class); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller8.class)); + Assertions.assertTrue(output.contains("echo(queryParams?: { message?: string; }): RestResponse")); + } + + @Test + public void testCustomRequestBody() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.springCustomRequestBodyAnnotations = Arrays.asList(CustomRequestBody.class); + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller9.class)); + Assertions.assertTrue(output.contains("setEntity(data: Data1): RestResponse")); + Assertions.assertTrue(output.contains("interface Data1")); + } + @RestController @RequestMapping("/owners/{ownerId}") public static class Controller1 { @@ -203,6 +225,23 @@ public String echo( } } + @RestController + public static class Controller8 { + @RequestMapping("/echo3") + public String echo( + @CustomRequestParam String message + ) { + return message; + } + } + + @RestController + public static class Controller9 { + @RequestMapping(path = "/data2", method = RequestMethod.PUT) + public void setEntity(@CustomRequestBody Data1 data) { + } + } + @Test public void testQueryParametersWithModel() { final Settings settings = TestUtils.settings(); @@ -488,4 +527,16 @@ public String shouldBeExcluded() { } } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CustomRequestParam { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + public @interface CustomRequestBody { + + } + }