From a59f163cfbd1388ff21b25274acf262a3f1c136a 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 | 77 +++++++++++++++++-- .../generator/emitter/TsBinaryOperator.java | 3 +- .../emitter/TsIdentifierReference.java | 1 + .../typescript/generator/util/Utils.java | 31 ++++++++ .../typescript/generator/UtilsTest.java | 6 ++ .../generator/gradle/GenerateTask.java | 4 + .../generator/maven/GenerateMojo.java | 21 +++++ .../generator/spring/SpringTest.java | 61 +++++++++++++++ 9 files changed, 207 insertions(+), 8 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 1ed63e406..2aad59647 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 skipNullValuesForOptionalServiceArguments = false; public boolean scanSpringApplication; public List> springCustomQueryParameterAnnotations = new ArrayList<>(); public List> springCustomRequestBodyAnnotations = new ArrayList<>(); @@ -432,6 +434,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(skipNullValuesForOptionalServiceArguments && !generateClientAsService){ + throw new RuntimeException("'skipNullValuesForOptionalServiceArguments' 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 10a49bbcd..cb6ba175f 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,13 +733,15 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable parameters.add(processParameter(symbolTable, method, method.getEntityParam())); } // query params - final TsParameterModel queryParameter = convertRestParams(method.getQueryParams(), symbolTable, method, tsModel, "queryParams", "QueryParams"); - if(queryParameter != null){ + final List allQuerySingles = new ArrayList<>(); + final TsParameterModel queryParameter = convertRestParams(method.getQueryParams(), parameters, symbolTable, method, tsModel, "queryParams", "QueryParams", allQuerySingles); + if(queryParameter != null && !settings.generateClientAsService){ parameters.add(queryParameter); } - // body params - final TsParameterModel headersParameter = convertRestParams(method.getHeaders(), symbolTable, method, tsModel, "headers", "Headers"); - if(headersParameter != null){ + // header params + final List allHeaderSingles = new ArrayList<>(); + final TsParameterModel headersParameter = convertRestParams(method.getHeaders(), parameters, symbolTable, method, tsModel, "headers", "Headers", allHeaderSingles); + if(headersParameter != null && !settings.generateClientAsService){ parameters.add(headersParameter); } if (optionsType != null) { @@ -758,6 +764,14 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable final List body; if (implement) { body = new ArrayList<>(); + if(settings.generateClientAsService){ + if(!allQuerySingles.isEmpty()){ + initServiceParameters(body, allQuerySingles, "queryParams"); + } + if(!allHeaderSingles.isEmpty()){ + initServiceParameters(body, allHeaderSingles, "headers"); + } + } body.add(new TsReturnStatement( new TsCallExpression( new TsMemberExpression(new TsMemberExpression(new TsThisExpression(), "httpClient"), "request"), @@ -779,7 +793,49 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable return tsMethodModel; } - private TsParameterModel convertRestParams(List restParams, SymbolTable symbolTable, RestMethodModel method, TsModel tsModel, String parameterName, String beanSuffix){ + private void initServiceParameters(List body, List singleServiceParams, String argumentName){ + body.add(new TsVariableDeclarationStatement( + false, + argumentName, + TsType.Any, + new TsObjectLiteral() + )); + for(TsProperty property : singleServiceParams){ + + String validJavaScriptArgumentName = Utils.toValidJavaScriptVariableName(property.getName()); + + TsExpressionStatement assignmentExpressionStatement = new TsExpressionStatement( + new TsAssignmentExpression( + new TsMemberExpression( + new TsIdentifierReference(argumentName), + property.getName()), + new TsIdentifierReference(validJavaScriptArgumentName) + ) + ); + + if(property.getTsType() instanceof TsType.OptionalType){ + + body.add( + new TsIfStatement( + new TsBinaryExpression( + new TsIdentifierReference(validJavaScriptArgumentName), + TsBinaryOperator.NEQ, + settings.skipNullValuesForOptionalServiceArguments ? + TsIdentifierReference.Null : TsIdentifierReference.Undefined + ), + Collections.singletonList(assignmentExpressionStatement) + ) + ); + + } else { + + body.add(assignmentExpressionStatement); + + } + } + } + + private TsParameterModel convertRestParams(List restParams, List parameters, SymbolTable symbolTable, RestMethodModel method, TsModel tsModel, String parameterName, String beanSuffix, List allSingles){ if (restParams == null || restParams.isEmpty()){ return null; } @@ -798,7 +854,13 @@ private TsParameterModel convertRestParams(List restParams, SymbolTab if (restParam instanceof RestParam.Single) { final MethodParameterModel restParamMethodParameterModel = ((RestParam.Single) restParam).getRestParam(); final TsType type = typeFromJava(symbolTable, restParamMethodParameterModel.getType(), method.getName(), method.getOriginClass()); - currentSingles.add(new TsProperty(restParamMethodParameterModel.getName(), restParam.required ? type : new TsType.OptionalType(type))); + TsProperty property = new TsProperty(restParamMethodParameterModel.getName(), restParam.required ? type : new TsType.OptionalType(type)); + currentSingles.add(property); + allSingles.add(property); + if(settings.generateClientAsService) { + String validJavaScriptArgumentName = Utils.toValidJavaScriptVariableName(property.getName()); + parameters.add(new TsParameterModel(validJavaScriptArgumentName, property.getTsType())); + } } if (restParam instanceof RestParam.Bean) { final BeanModel paramBean = ((RestParam.Bean) restParam).getBean(); @@ -825,6 +887,7 @@ private TsParameterModel convertRestParams(List restParams, SymbolTab } flushSingles.run(); } + boolean allParamsOptional = restParams.stream().noneMatch(param -> param.required); TsType.IntersectionType paramType = new TsType.IntersectionType(types); return new TsParameterModel(parameterName, allParamsOptional ? new TsType.OptionalType(paramType) : paramType); 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-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java index 1041156c8..e935844ab 100644 --- a/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java +++ b/typescript-generator-core/src/main/java/cz/habarta/typescript/generator/util/Utils.java @@ -508,4 +508,35 @@ public static Supplier memoize(Supplier supplier) { return () -> value.updateAndGet(current -> current != null ? current : Objects.requireNonNull(supplier.get())); } + public static String toValidJavaScriptVariableName(String variableName){ + + StringBuilder validJavaScriptVariableName = new StringBuilder(); + + boolean convertNextLetterToCapital = false; + + for(int i=0; i= '0' && currentChar <= '9'){ + + if(validJavaScriptVariableName.length() == 0){ + // Number prefix is forbidden + continue; + } + + } else if (currentChar >= 'a' && currentChar <= 'z'){ + if(convertNextLetterToCapital){ + currentChar = Character.toUpperCase(currentChar); + } + } else if(currentChar != '_' && (currentChar < 'A' || currentChar > 'Z')){ + convertNextLetterToCapital = validJavaScriptVariableName.length() > 0; + continue; + } + validJavaScriptVariableName.append(currentChar); + convertNextLetterToCapital = false; + } + + return validJavaScriptVariableName.toString(); + } } diff --git a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/UtilsTest.java b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/UtilsTest.java index fe87b504c..6782a9430 100644 --- a/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/UtilsTest.java +++ b/typescript-generator-core/src/test/java/cz/habarta/typescript/generator/UtilsTest.java @@ -76,4 +76,10 @@ class NewClass{} Assertions.assertFalse(Utils.isPrimitiveType(NewClass.class)); } + @Test + public void testShouldConvertStringToCorrectJavaScriptVariableName(){ + Assertions.assertEquals("helloWorld", Utils.toValidJavaScriptVariableName("hello-world")); + Assertions.assertEquals("helloWorld", Utils.toValidJavaScriptVariableName("123hello world")); + Assertions.assertEquals("hello123World", Utils.toValidJavaScriptVariableName("hello123World")); + } } 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 49c80c3fc..f5b948f3d 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 @@ -96,6 +96,8 @@ public class GenerateTask extends DefaultTask { public boolean generateJaxrsApplicationClient; public boolean generateSpringApplicationInterface; public boolean generateSpringApplicationClient; + public boolean generateClientAsService; + public boolean skipNullValuesForOptionalServiceArguments; public boolean scanSpringApplication; @Deprecated public RestNamespacing jaxrsNamespacing; @Deprecated public String jaxrsNamespacingAnnotation; @@ -187,6 +189,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient; settings.generateSpringApplicationInterface = generateSpringApplicationInterface; settings.generateSpringApplicationClient = generateSpringApplicationClient; + settings.generateClientAsService = generateClientAsService; + settings.skipNullValuesForOptionalServiceArguments = skipNullValuesForOptionalServiceArguments; 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 47a9b5606..338c75b27 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 @@ -575,6 +575,25 @@ public class GenerateMojo extends AbstractMojo { @Parameter private List springCustomRequestBodyAnnotations; + /** + * 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 skipNullValuesForOptionalServiceArguments; + /** * Deprecated, use {@link #restNamespacing}. */ @@ -974,6 +993,8 @@ private Settings createSettings(URLClassLoader classLoader) { settings.generateSpringApplicationInterface = generateSpringApplicationInterface; settings.generateSpringApplicationClient = generateSpringApplicationClient; settings.scanSpringApplication = scanSpringApplication; + settings.generateClientAsService = generateClientAsService; + settings.skipNullValuesForOptionalServiceArguments = skipNullValuesForOptionalServiceArguments; 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 92e03a657..d35c0a52b 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 @@ -125,6 +125,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.skipNullValuesForOptionalServiceArguments = 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(); @@ -228,6 +263,23 @@ public void testHeadersParameterWithQueryParameter() { Assertions.assertTrue(output.contains("sendHeadersAndQueryParams(queryParams: { petId: string; }, headers: { ownerId: number; }): RestResponse")); } + @Test + public void testHeadersServiceWithNonValidHeaderNames() { + final Settings settings = TestUtils.settings(); + settings.outputFileType = TypeScriptFileType.implementationFile; + settings.generateSpringApplicationClient = true; + settings.restHeaderArgumentsParsed = true; + settings.generateClientAsService = true; + final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller12.class)); + Assertions.assertTrue(output.contains("sendHeaders(ownerId: number, petId?: string, normalHeader: number): RestResponse")); + Assertions.assertTrue(output.contains("let headers: any = {};")); + Assertions.assertTrue(output.contains("headers[\"owner-id\"] = ownerId;")); + Assertions.assertTrue(output.contains("if (petId != undefined)")); + Assertions.assertTrue(output.contains("headers[\"pet-id\"] = petId;")); + Assertions.assertTrue(output.contains("headers.normalHeader = normalHeader;")); + Assertions.assertTrue(output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`headers12`, headers: headers });")); + } + @RestController @RequestMapping("/owners/{ownerId}") public static class Controller1 { @@ -385,6 +437,15 @@ public void sendHeadersAndQueryParams( ) {} } + @RestController + public static class Controller12 { + @GetMapping("/headers12") + public void sendHeaders( + @RequestHeader(name = "owner-id") Long ownerId, + @RequestHeader(name = "pet-id", required = false) String petId, + @RequestHeader Long normalHeader) {} + } + public static class Pet { }