From 9399f945f607280505d5c70eda3f981a7628efe0 Mon Sep 17 00:00:00 2001 From: Oleksandr Porunov Date: Sun, 6 Feb 2022 21:49:02 +0200 Subject: [PATCH] Implement REST service layer generation technique which unfolds all arguments Fixes #792 Signed-off-by: Oleksandr Porunov --- .../typescript/generator/Settings.java | 11 ++++ .../generator/compiler/ModelCompiler.java | 61 ++++++++++++++++++- .../generator/emitter/TsBinaryOperator.java | 3 +- .../emitter/TsIdentifierReference.java | 1 + .../generator/gradle/GenerateTask.java | 4 ++ .../generator/maven/GenerateMojo.java | 21 +++++++ .../generator/spring/SpringTest.java | 35 +++++++++++ 7 files changed, 133 insertions(+), 3 deletions(-) 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..d75842f3f 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 @@ -98,6 +98,8 @@ public class Settings { public boolean generateJaxrsApplicationClient = false; public boolean generateSpringApplicationInterface = false; public boolean generateSpringApplicationClient = false; + public boolean generateClientAsService = false; + public boolean skipNullValuesInOptionalQueryParametersAsService = false; public boolean scanSpringApplication; @Deprecated public RestNamespacing jaxrsNamespacing; @Deprecated public Class jaxrsNamespacingAnnotation = null; @@ -414,6 +416,15 @@ public void validate() { if (generateSpringApplicationClient && outputFileType != TypeScriptFileType.implementationFile) { throw new RuntimeException("'generateSpringApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile')."); } + + if(generateClientAsService && !(generateSpringApplicationClient || generateJaxrsApplicationClient)){ + throw new RuntimeException("'generateClientAsService' can only be used when application client generation is enabled via 'generateSpringApplicationClient' or 'generateJaxrsApplicationClient'."); + } + + if(skipNullValuesInOptionalQueryParametersAsService && !generateClientAsService){ + throw new RuntimeException("'skipNullValuesInOptionalQueryParametersAsService' can only be used when application client as a service generation is enabled via 'generateClientAsService'."); + } + if (jaxrsNamespacing != null) { TypeScriptGenerator.getLogger().warning("Parameter 'jaxrsNamespacing' is deprecated. Use 'restNamespacing' parameter."); if (restNamespacing == null) { diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java index 5c91e7482..ba838530c 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java @@ -20,6 +20,8 @@ import cz.habarta.typescript.generator.emitter.TsAssignmentExpression; import cz.habarta.typescript.generator.emitter.TsBeanCategory; import cz.habarta.typescript.generator.emitter.TsBeanModel; +import cz.habarta.typescript.generator.emitter.TsBinaryExpression; +import cz.habarta.typescript.generator.emitter.TsBinaryOperator; import cz.habarta.typescript.generator.emitter.TsCallExpression; import cz.habarta.typescript.generator.emitter.TsConstructorModel; import cz.habarta.typescript.generator.emitter.TsEnumModel; @@ -27,6 +29,7 @@ import cz.habarta.typescript.generator.emitter.TsExpressionStatement; import cz.habarta.typescript.generator.emitter.TsHelper; import cz.habarta.typescript.generator.emitter.TsIdentifierReference; +import cz.habarta.typescript.generator.emitter.TsIfStatement; import cz.habarta.typescript.generator.emitter.TsMemberExpression; import cz.habarta.typescript.generator.emitter.TsMethodModel; import cz.habarta.typescript.generator.emitter.TsModel; @@ -42,6 +45,7 @@ import cz.habarta.typescript.generator.emitter.TsTaggedTemplateLiteral; import cz.habarta.typescript.generator.emitter.TsTemplateLiteral; import cz.habarta.typescript.generator.emitter.TsThisExpression; +import cz.habarta.typescript.generator.emitter.TsVariableDeclarationStatement; import cz.habarta.typescript.generator.parser.BeanModel; import cz.habarta.typescript.generator.parser.EnumModel; import cz.habarta.typescript.generator.parser.MethodModel; @@ -729,9 +733,11 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable } // query params final List queryParams = method.getQueryParams(); + final List allSingles; final TsParameterModel queryParameter; if (queryParams != null && !queryParams.isEmpty()) { final List types = new ArrayList<>(); + allSingles = new ArrayList<>(queryParams.size()); if (queryParams.stream().anyMatch(param -> param instanceof RestQueryParam.Map)) { types.add(new TsType.IndexedArrayType(TsType.String, TsType.Any)); } else { @@ -746,7 +752,12 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable if (restQueryParam instanceof RestQueryParam.Single) { final MethodParameterModel queryParam = ((RestQueryParam.Single) restQueryParam).getQueryParam(); final TsType type = typeFromJava(symbolTable, queryParam.getType(), method.getName(), method.getOriginClass()); - currentSingles.add(new TsProperty(queryParam.getName(), restQueryParam.required ? type : new TsType.OptionalType(type))); + TsProperty property = new TsProperty(queryParam.getName(), restQueryParam.required ? type : new TsType.OptionalType(type)); + currentSingles.add(property); + allSingles.add(property); + if(settings.generateClientAsService) { + parameters.add(new TsParameterModel(property.getName(), property.getTsType())); + } } if (restQueryParam instanceof RestQueryParam.Bean) { final BeanModel queryBean = ((RestQueryParam.Bean) restQueryParam).getBean(); @@ -776,9 +787,12 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable boolean allQueryParamsOptional = queryParams.stream().noneMatch(queryParam -> queryParam.required); TsType.IntersectionType queryParamType = new TsType.IntersectionType(types); queryParameter = new TsParameterModel("queryParams", allQueryParamsOptional ? new TsType.OptionalType(queryParamType) : queryParamType); - parameters.add(queryParameter); + if(!settings.generateClientAsService){ + parameters.add(queryParameter); + } } else { queryParameter = null; + allSingles = null; } if (optionsType != null) { final TsParameterModel optionsParameter = new TsParameterModel("options", new TsType.OptionalType(optionsType)); @@ -800,6 +814,9 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable final List body; if (implement) { body = new ArrayList<>(); + if(settings.generateClientAsService && allSingles != null && !allSingles.isEmpty()){ + initQueryParameters(body, allSingles); + } body.add(new TsReturnStatement( new TsCallExpression( new TsMemberExpression(new TsMemberExpression(new TsThisExpression(), "httpClient"), "request"), @@ -820,6 +837,46 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable return tsMethodModel; } + private void initQueryParameters(List body, List singleQueryParams){ + body.add(new TsVariableDeclarationStatement( + false, + "queryParams", + TsType.Any, + new TsObjectLiteral() + )); + for(TsProperty property : singleQueryParams){ + + TsExpressionStatement assignmentExpressionStatement = new TsExpressionStatement( + new TsAssignmentExpression( + new TsMemberExpression( + new TsIdentifierReference("queryParams"), + property.getName()), + new TsIdentifierReference(property.getName()) + ) + ); + + if(property.getTsType() instanceof TsType.OptionalType){ + + body.add( + new TsIfStatement( + new TsBinaryExpression( + new TsIdentifierReference(property.getName()), + TsBinaryOperator.NEQ, + settings.skipNullValuesInOptionalQueryParametersAsService ? + TsIdentifierReference.Null : TsIdentifierReference.Undefined + ), + Collections.singletonList(assignmentExpressionStatement) + ) + ); + + } else { + + body.add(assignmentExpressionStatement); + + } + } + } + private TsParameterModel processParameter(SymbolTable symbolTable, MethodModel method, MethodParameterModel parameter) { final String parameterName = parameter.getName(); final TsType parameterType = typeFromJava(symbolTable, parameter.getType(), method.getName(), method.getOriginClass()); diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsBinaryOperator.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsBinaryOperator.java index 05699c040..f260785cd 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsBinaryOperator.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsBinaryOperator.java @@ -6,7 +6,8 @@ public enum TsBinaryOperator implements Emittable { - BarBar("||"); + BarBar("||"), + NEQ("!="); private final String formatted; diff --git a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsIdentifierReference.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsIdentifierReference.java index 227154f8b..32885d7a6 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsIdentifierReference.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsIdentifierReference.java @@ -7,6 +7,7 @@ public class TsIdentifierReference extends TsExpression { public static final TsIdentifierReference Undefined = new TsIdentifierReference("undefined"); + public static final TsIdentifierReference Null = new TsIdentifierReference("null"); private final String identifier; 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..6d182881e 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 @@ -94,6 +94,8 @@ public class GenerateTask extends DefaultTask { public boolean generateJaxrsApplicationClient; public boolean generateSpringApplicationInterface; public boolean generateSpringApplicationClient; + public boolean generateClientAsService; + public boolean skipNullValuesInOptionalQueryParametersAsService; public boolean scanSpringApplication; @Deprecated public RestNamespacing jaxrsNamespacing; @Deprecated public String jaxrsNamespacingAnnotation; @@ -183,6 +185,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; settings.generateSpringApplicationInterface = generateSpringApplicationInterface; settings.generateSpringApplicationClient = generateSpringApplicationClient; + settings.generateClientAsService = generateClientAsService; + settings.skipNullValuesInOptionalQueryParametersAsService = skipNullValuesInOptionalQueryParametersAsService; settings.scanSpringApplication = scanSpringApplication; settings.jaxrsNamespacing = jaxrsNamespacing; settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation); 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..f4443204b 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,25 @@ public class GenerateMojo extends AbstractMojo { @Parameter private boolean scanSpringApplication; + /** + * If true it will generate client application methods as service methods, meaning that they will + * accept all arguments as unfolded arguments. Otherwise, REST query parameters will be wrapped into an object queryParams. + * This parameter can be used only when generateSpringApplicationClient or generateJaxrsApplicationClient are set to true. + * Notice, currently only simple (non-bean) parameters will be detected. Currently, beans won't work for this type of client generation. + * If you need for beans to work as well, please, set this option to false. This flow is intended to be fixed in the future releases. + */ + @Parameter + private boolean generateClientAsService; + + /** + * If true it will not pass optional parameters to the HttpClient if those parameters + * are set to null. Otherwise, only undefined parameters will be skipped. + * Notice, mandatory parameters which are set to null will still be passed. + * This parameter can be used only when generateClientAsService is set to true. + */ + @Parameter + private boolean skipNullValuesInOptionalQueryParametersAsService; + /** * Deprecated, use {@link #restNamespacing}. */ @@ -942,6 +961,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.generateSpringApplicationInterface = generateSpringApplicationInterface; settings.generateSpringApplicationClient = generateSpringApplicationClient; settings.scanSpringApplication = scanSpringApplication; + settings.generateClientAsService = generateClientAsService; + settings.skipNullValuesInOptionalQueryParametersAsService = skipNullValuesInOptionalQueryParametersAsService; settings.jaxrsNamespacing = jaxrsNamespacing; settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation); settings.restNamespacing = restNamespacing; 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..1a8f935f3 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 @@ -118,6 +118,41 @@ public void testQueryParameters() { Assertions.assertTrue(output.contains("echo(queryParams: { message: string; count?: number; optionalRequestParam?: number; }): RestResponse")); } + @Test + public void testUnfoldedQueryParameters() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.generateClientAsService = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller2.class)); + Assertions.assertTrue(output.contains("echo(message: string, count?: number, optionalRequestParam?: number): RestResponse")); + Assertions.assertTrue(output.contains("let queryParams: any = {};")); + Assertions.assertTrue(output.contains("queryParams.message = message;")); + Assertions.assertTrue(output.contains("if (count != undefined) {")); + Assertions.assertTrue(output.contains("queryParams.count = count;")); + Assertions.assertTrue(output.contains("if (optionalRequestParam != undefined)")); + Assertions.assertTrue(output.contains("queryParams.optionalRequestParam = optionalRequestParam;")); + Assertions.assertTrue(output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`echo`, queryParams: queryParams });")); + } + + @Test + public void testUnfoldedQueryParametersWithSkipOptionalParams() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.generateClientAsService = true; + settings.skipNullValuesInOptionalQueryParametersAsService = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller2.class)); + Assertions.assertTrue(output.contains("echo(message: string, count?: number, optionalRequestParam?: number): RestResponse")); + Assertions.assertTrue(output.contains("let queryParams: any = {};")); + Assertions.assertTrue(output.contains("queryParams.message = message;")); + Assertions.assertTrue(output.contains("if (count != null) {")); + Assertions.assertTrue(output.contains("queryParams.count = count;")); + Assertions.assertTrue(output.contains("if (optionalRequestParam != null)")); + Assertions.assertTrue(output.contains("queryParams.optionalRequestParam = optionalRequestParam;")); + Assertions.assertTrue(output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`echo`, queryParams: queryParams });")); + } + @Test public void testAllOptionalQueryParameters() { final Settings settings = TestUtils.settings();