Skip to content

Commit a5c8e27

Browse files
committed
Implement REST service layer generation technique which unfolds all arguments
Fixes #792 Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
1 parent 0188fd0 commit a5c8e27

File tree

7 files changed

+169
-4
lines changed

7 files changed

+169
-4
lines changed

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/Settings.java

+11
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ public class Settings {
9898
public boolean generateJaxrsApplicationClient = false;
9999
public boolean generateSpringApplicationInterface = false;
100100
public boolean generateSpringApplicationClient = false;
101+
public boolean generateClientAsService = false;
102+
public boolean skipNullValuesForOptionalServiceArguments = false;
101103
public boolean scanSpringApplication;
102104
@Deprecated public RestNamespacing jaxrsNamespacing;
103105
@Deprecated public Class<? extends Annotation> jaxrsNamespacingAnnotation = null;
@@ -414,6 +416,15 @@ public void validate() {
414416
if (generateSpringApplicationClient && outputFileType != TypeScriptFileType.implementationFile) {
415417
throw new RuntimeException("'generateSpringApplicationClient' can only be used when generating implementation file ('outputFileType' parameter is 'implementationFile').");
416418
}
419+
420+
if(generateClientAsService && !(generateSpringApplicationClient || generateJaxrsApplicationClient)){
421+
throw new RuntimeException("'generateClientAsService' can only be used when application client generation is enabled via 'generateSpringApplicationClient' or 'generateJaxrsApplicationClient'.");
422+
}
423+
424+
if(skipNullValuesForOptionalServiceArguments && !generateClientAsService){
425+
throw new RuntimeException("'skipNullValuesForOptionalServiceArguments' can only be used when application client as a service generation is enabled via 'generateClientAsService'.");
426+
}
427+
417428
if (jaxrsNamespacing != null) {
418429
TypeScriptGenerator.getLogger().warning("Parameter 'jaxrsNamespacing' is deprecated. Use 'restNamespacing' parameter.");
419430
if (restNamespacing == null) {

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/compiler/ModelCompiler.java

+70-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@
2020
import cz.habarta.typescript.generator.emitter.TsAssignmentExpression;
2121
import cz.habarta.typescript.generator.emitter.TsBeanCategory;
2222
import cz.habarta.typescript.generator.emitter.TsBeanModel;
23+
import cz.habarta.typescript.generator.emitter.TsBinaryExpression;
24+
import cz.habarta.typescript.generator.emitter.TsBinaryOperator;
2325
import cz.habarta.typescript.generator.emitter.TsCallExpression;
2426
import cz.habarta.typescript.generator.emitter.TsConstructorModel;
2527
import cz.habarta.typescript.generator.emitter.TsEnumModel;
2628
import cz.habarta.typescript.generator.emitter.TsExpression;
2729
import cz.habarta.typescript.generator.emitter.TsExpressionStatement;
2830
import cz.habarta.typescript.generator.emitter.TsHelper;
2931
import cz.habarta.typescript.generator.emitter.TsIdentifierReference;
32+
import cz.habarta.typescript.generator.emitter.TsIfStatement;
3033
import cz.habarta.typescript.generator.emitter.TsMemberExpression;
3134
import cz.habarta.typescript.generator.emitter.TsMethodModel;
3235
import cz.habarta.typescript.generator.emitter.TsModel;
@@ -42,6 +45,7 @@
4245
import cz.habarta.typescript.generator.emitter.TsTaggedTemplateLiteral;
4346
import cz.habarta.typescript.generator.emitter.TsTemplateLiteral;
4447
import cz.habarta.typescript.generator.emitter.TsThisExpression;
48+
import cz.habarta.typescript.generator.emitter.TsVariableDeclarationStatement;
4549
import cz.habarta.typescript.generator.parser.BeanModel;
4650
import cz.habarta.typescript.generator.parser.EnumModel;
4751
import cz.habarta.typescript.generator.parser.MethodModel;
@@ -729,9 +733,11 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
729733
}
730734
// query params
731735
final List<RestQueryParam> queryParams = method.getQueryParams();
736+
final List<TsProperty> allSingles;
732737
final TsParameterModel queryParameter;
733738
if (queryParams != null && !queryParams.isEmpty()) {
734739
final List<TsType> types = new ArrayList<>();
740+
allSingles = new ArrayList<>(queryParams.size());
735741
if (queryParams.stream().anyMatch(param -> param instanceof RestQueryParam.Map)) {
736742
types.add(new TsType.IndexedArrayType(TsType.String, TsType.Any));
737743
} else {
@@ -746,7 +752,12 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
746752
if (restQueryParam instanceof RestQueryParam.Single) {
747753
final MethodParameterModel queryParam = ((RestQueryParam.Single) restQueryParam).getQueryParam();
748754
final TsType type = typeFromJava(symbolTable, queryParam.getType(), method.getName(), method.getOriginClass());
749-
currentSingles.add(new TsProperty(queryParam.getName(), restQueryParam.required ? type : new TsType.OptionalType(type)));
755+
TsProperty property = new TsProperty(queryParam.getName(), restQueryParam.required ? type : new TsType.OptionalType(type));
756+
currentSingles.add(property);
757+
allSingles.add(property);
758+
if(settings.generateClientAsService) {
759+
parameters.add(new TsParameterModel(property.getName(), property.getTsType()));
760+
}
750761
}
751762
if (restQueryParam instanceof RestQueryParam.Bean) {
752763
final BeanModel queryBean = ((RestQueryParam.Bean) restQueryParam).getBean();
@@ -776,9 +787,12 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
776787
boolean allQueryParamsOptional = queryParams.stream().noneMatch(queryParam -> queryParam.required);
777788
TsType.IntersectionType queryParamType = new TsType.IntersectionType(types);
778789
queryParameter = new TsParameterModel("queryParams", allQueryParamsOptional ? new TsType.OptionalType(queryParamType) : queryParamType);
779-
parameters.add(queryParameter);
790+
if(!settings.generateClientAsService){
791+
parameters.add(queryParameter);
792+
}
780793
} else {
781794
queryParameter = null;
795+
allSingles = null;
782796
}
783797
if (optionsType != null) {
784798
final TsParameterModel optionsParameter = new TsParameterModel("options", new TsType.OptionalType(optionsType));
@@ -800,6 +814,9 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
800814
final List<TsStatement> body;
801815
if (implement) {
802816
body = new ArrayList<>();
817+
if(settings.generateClientAsService && allSingles != null && !allSingles.isEmpty()){
818+
initQueryParameters(body, allSingles);
819+
}
803820
body.add(new TsReturnStatement(
804821
new TsCallExpression(
805822
new TsMemberExpression(new TsMemberExpression(new TsThisExpression(), "httpClient"), "request"),
@@ -815,11 +832,61 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
815832
} else {
816833
body = null;
817834
}
835+
List<TsParameterModel> orderedParameters = new ArrayList<>(parameters.size());
836+
List<TsParameterModel> optionalParameters = new ArrayList<>(parameters.size());
837+
for(TsParameterModel parameter : parameters){
838+
if(parameter.getTsType() instanceof TsType.OptionalType){
839+
optionalParameters.add(parameter);
840+
} else {
841+
orderedParameters.add(parameter);
842+
}
843+
}
844+
orderedParameters.addAll(optionalParameters);
818845
// method
819-
final TsMethodModel tsMethodModel = new TsMethodModel(method.getName() + nameSuffix, TsModifierFlags.None, null, parameters, wrappedReturnType, body, comments);
846+
final TsMethodModel tsMethodModel = new TsMethodModel(method.getName() + nameSuffix, TsModifierFlags.None, null, orderedParameters, wrappedReturnType, body, comments);
820847
return tsMethodModel;
821848
}
822849

850+
private void initQueryParameters(List<TsStatement> body, List<TsProperty> singleQueryParams){
851+
body.add(new TsVariableDeclarationStatement(
852+
false,
853+
"queryParams",
854+
TsType.Any,
855+
new TsObjectLiteral()
856+
));
857+
for(TsProperty property : singleQueryParams){
858+
859+
TsExpressionStatement assignmentExpressionStatement = new TsExpressionStatement(
860+
new TsAssignmentExpression(
861+
new TsMemberExpression(
862+
new TsIdentifierReference("queryParams"),
863+
property.getName()),
864+
new TsIdentifierReference(property.getName())
865+
)
866+
);
867+
868+
if(property.getTsType() instanceof TsType.OptionalType){
869+
870+
body.add(
871+
new TsIfStatement(
872+
new TsBinaryExpression(
873+
new TsIdentifierReference(property.getName()),
874+
TsBinaryOperator.NEQ,
875+
settings.skipNullValuesForOptionalServiceArguments ?
876+
TsIdentifierReference.Null : TsIdentifierReference.Undefined
877+
),
878+
Collections.singletonList(assignmentExpressionStatement)
879+
)
880+
);
881+
882+
} else {
883+
884+
body.add(assignmentExpressionStatement);
885+
886+
}
887+
}
888+
}
889+
823890
private TsParameterModel processParameter(SymbolTable symbolTable, MethodModel method, MethodParameterModel parameter) {
824891
final String parameterName = parameter.getName();
825892
final TsType parameterType = typeFromJava(symbolTable, parameter.getType(), method.getName(), method.getOriginClass());

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsBinaryOperator.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
public enum TsBinaryOperator implements Emittable {
88

9-
BarBar("||");
9+
BarBar("||"),
10+
NEQ("!=");
1011

1112
private final String formatted;
1213

typescript-generator-core/src/main/java/cz/habarta/typescript/generator/emitter/TsIdentifierReference.java

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
public class TsIdentifierReference extends TsExpression {
88

99
public static final TsIdentifierReference Undefined = new TsIdentifierReference("undefined");
10+
public static final TsIdentifierReference Null = new TsIdentifierReference("null");
1011

1112
private final String identifier;
1213

typescript-generator-gradle-plugin/src/main/java/cz/habarta/typescript/generator/gradle/GenerateTask.java

+4
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ public class GenerateTask extends DefaultTask {
9494
public boolean generateJaxrsApplicationClient;
9595
public boolean generateSpringApplicationInterface;
9696
public boolean generateSpringApplicationClient;
97+
public boolean generateClientAsService;
98+
public boolean skipNullValuesForOptionalServiceArguments;
9799
public boolean scanSpringApplication;
98100
@Deprecated public RestNamespacing jaxrsNamespacing;
99101
@Deprecated public String jaxrsNamespacingAnnotation;
@@ -183,6 +185,8 @@ private Settings createSettings(URLClassLoader classLoader) {
183185
settings.generateJaxrsApplicationClient = generateJaxrsApplicationClient;
184186
settings.generateSpringApplicationInterface = generateSpringApplicationInterface;
185187
settings.generateSpringApplicationClient = generateSpringApplicationClient;
188+
settings.generateClientAsService = generateClientAsService;
189+
settings.skipNullValuesForOptionalServiceArguments = skipNullValuesForOptionalServiceArguments;
186190
settings.scanSpringApplication = scanSpringApplication;
187191
settings.jaxrsNamespacing = jaxrsNamespacing;
188192
settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation);

typescript-generator-maven-plugin/src/main/java/cz/habarta/typescript/generator/maven/GenerateMojo.java

+21
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,25 @@ public class GenerateMojo extends AbstractMojo {
558558
@Parameter
559559
private boolean scanSpringApplication;
560560

561+
/**
562+
* If <code>true</code> it will generate client application methods as service methods, meaning that they will
563+
* accept all arguments as unfolded arguments. Otherwise, REST query parameters will be wrapped into an object <code>queryParams</code>.
564+
* This parameter can be used only when <code>generateSpringApplicationClient</code> or <code>generateJaxrsApplicationClient</code> are set to <code>true</code>.
565+
* Notice, currently only simple (non-bean) parameters will be detected. Currently, beans won't work for this type of client generation.
566+
* If you need for beans to work as well, please, set this option to <code>false</code>. This flow is intended to be fixed in the future releases.
567+
*/
568+
@Parameter
569+
private boolean generateClientAsService;
570+
571+
/**
572+
* If <code>true</code> it will not pass optional parameters to the <code>HttpClient</code> if those parameters
573+
* are set to <code>null</code>. Otherwise, only <code>undefined</code> parameters will be skipped.
574+
* Notice, mandatory parameters which are set to <code>null</code> will still be passed.
575+
* This parameter can be used only when <code>generateClientAsService</code> is set to <code>true</code>.
576+
*/
577+
@Parameter
578+
private boolean skipNullValuesForOptionalServiceArguments;
579+
561580
/**
562581
* Deprecated, use {@link #restNamespacing}.
563582
*/
@@ -942,6 +961,8 @@ private Settings createSettings(URLClassLoader classLoader) {
942961
settings.generateSpringApplicationInterface = generateSpringApplicationInterface;
943962
settings.generateSpringApplicationClient = generateSpringApplicationClient;
944963
settings.scanSpringApplication = scanSpringApplication;
964+
settings.generateClientAsService = generateClientAsService;
965+
settings.skipNullValuesForOptionalServiceArguments = skipNullValuesForOptionalServiceArguments;
945966
settings.jaxrsNamespacing = jaxrsNamespacing;
946967
settings.setJaxrsNamespacingAnnotation(classLoader, jaxrsNamespacingAnnotation);
947968
settings.restNamespacing = restNamespacing;

typescript-generator-spring/src/test/java/cz/habarta/typescript/generator/spring/SpringTest.java

+60
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,51 @@ public void testQueryParameters() {
118118
Assertions.assertTrue(output.contains("echo(queryParams: { message: string; count?: number; optionalRequestParam?: number; }): RestResponse<string>"));
119119
}
120120

121+
@Test
122+
public void testUnfoldedQueryParameters() {
123+
final Settings settings = TestUtils.settings();
124+
settings.outputFileType = TypeScriptFileType.implementationFile;
125+
settings.generateSpringApplicationClient = true;
126+
settings.generateClientAsService = true;
127+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller2.class));
128+
Assertions.assertTrue(output.contains("echo(message: string, count?: number, optionalRequestParam?: number): RestResponse<string>"));
129+
Assertions.assertTrue(output.contains("let queryParams: any = {};"));
130+
Assertions.assertTrue(output.contains("queryParams.message = message;"));
131+
Assertions.assertTrue(output.contains("if (count != undefined) {"));
132+
Assertions.assertTrue(output.contains("queryParams.count = count;"));
133+
Assertions.assertTrue(output.contains("if (optionalRequestParam != undefined)"));
134+
Assertions.assertTrue(output.contains("queryParams.optionalRequestParam = optionalRequestParam;"));
135+
Assertions.assertTrue(output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`echo`, queryParams: queryParams });"));
136+
}
137+
138+
@Test
139+
public void testUnfoldedQueryParametersWithSkipOptionalParams() {
140+
final Settings settings = TestUtils.settings();
141+
settings.outputFileType = TypeScriptFileType.implementationFile;
142+
settings.generateSpringApplicationClient = true;
143+
settings.generateClientAsService = true;
144+
settings.skipNullValuesForOptionalServiceArguments = true;
145+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(Controller2.class));
146+
Assertions.assertTrue(output.contains("echo(message: string, count?: number, optionalRequestParam?: number): RestResponse<string>"));
147+
Assertions.assertTrue(output.contains("let queryParams: any = {};"));
148+
Assertions.assertTrue(output.contains("queryParams.message = message;"));
149+
Assertions.assertTrue(output.contains("if (count != null) {"));
150+
Assertions.assertTrue(output.contains("queryParams.count = count;"));
151+
Assertions.assertTrue(output.contains("if (optionalRequestParam != null)"));
152+
Assertions.assertTrue(output.contains("queryParams.optionalRequestParam = optionalRequestParam;"));
153+
Assertions.assertTrue(output.contains("return this.httpClient.request({ method: \"GET\", url: uriEncoding`echo`, queryParams: queryParams });"));
154+
}
155+
156+
@Test
157+
public void testUnfoldedOptionalQueryParametersGeneratedAfterRequired() {
158+
final Settings settings = TestUtils.settings();
159+
settings.outputFileType = TypeScriptFileType.implementationFile;
160+
settings.generateSpringApplicationClient = true;
161+
settings.generateClientAsService = true;
162+
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ControllerWithMultipleOptionalUnorderedParameters.class));
163+
Assertions.assertTrue(output.contains("unorderedOptionalParamsMethod(requiredParam1: string, requiredParam4: number, optionalParam0?: number, optionalParam2?: string, optionalParam3?: number, optionalParam5?: number): RestResponse<string>"));
164+
}
165+
121166
@Test
122167
public void testAllOptionalQueryParameters() {
123168
final Settings settings = TestUtils.settings();
@@ -203,6 +248,21 @@ public String echo(
203248
}
204249
}
205250

251+
@RestController
252+
public static class ControllerWithMultipleOptionalUnorderedParameters {
253+
@RequestMapping("/unorderedOptionalParamsMethod")
254+
public String unorderedOptionalParamsMethod(
255+
@RequestParam(required = false) Integer optionalParam0,
256+
@RequestParam("requiredParam1") String requiredParam1,
257+
@RequestParam(required = false) String optionalParam2,
258+
@RequestParam(name = "optionalParam3", defaultValue = "1") Integer optionalParam3,
259+
@RequestParam Integer requiredParam4,
260+
@RequestParam(required = false) Integer optionalParam5
261+
) {
262+
return requiredParam1;
263+
}
264+
}
265+
206266
@Test
207267
public void testQueryParametersWithModel() {
208268
final Settings settings = TestUtils.settings();

0 commit comments

Comments
 (0)