Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement REST service layer generation technique which unfolds all arguments #793

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
@Deprecated public RestNamespacing jaxrsNamespacing;
@Deprecated public Class<? extends Annotation> jaxrsNamespacingAnnotation = null;
Expand Down Expand Up @@ -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(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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
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;
import cz.habarta.typescript.generator.emitter.TsExpression;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -729,9 +733,11 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
}
// query params
final List<RestQueryParam> queryParams = method.getQueryParams();
final List<TsProperty> allSingles;
final TsParameterModel queryParameter;
if (queryParams != null && !queryParams.isEmpty()) {
final List<TsType> 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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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));
Expand All @@ -800,6 +814,9 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
final List<TsStatement> 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"),
Expand All @@ -815,11 +832,61 @@ private TsMethodModel processRestMethod(TsModel tsModel, SymbolTable symbolTable
} else {
body = null;
}
List<TsParameterModel> orderedParameters = new ArrayList<>(parameters.size());
List<TsParameterModel> optionalParameters = new ArrayList<>(parameters.size());
for(TsParameterModel parameter : parameters){
if(parameter.getTsType() instanceof TsType.OptionalType){
optionalParameters.add(parameter);
} else {
orderedParameters.add(parameter);
}
}
orderedParameters.addAll(optionalParameters);
// method
final TsMethodModel tsMethodModel = new TsMethodModel(method.getName() + nameSuffix, TsModifierFlags.None, null, parameters, wrappedReturnType, body, comments);
final TsMethodModel tsMethodModel = new TsMethodModel(method.getName() + nameSuffix, TsModifierFlags.None, null, orderedParameters, wrappedReturnType, body, comments);
return tsMethodModel;
}

private void initQueryParameters(List<TsStatement> body, List<TsProperty> 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.skipNullValuesForOptionalServiceArguments ?
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

public enum TsBinaryOperator implements Emittable {

BarBar("||");
BarBar("||"),
NEQ("!=");

private final String formatted;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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;
Expand Down Expand Up @@ -183,6 +185,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,25 @@ public class GenerateMojo extends AbstractMojo {
@Parameter
private boolean scanSpringApplication;

/**
* If <code>true</code> 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 <code>queryParams</code>.
* This parameter can be used only when <code>generateSpringApplicationClient</code> or <code>generateJaxrsApplicationClient</code> are set to <code>true</code>.
* 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 <code>false</code>. This flow is intended to be fixed in the future releases.
*/
@Parameter
private boolean generateClientAsService;

/**
* If <code>true</code> it will not pass optional parameters to the <code>HttpClient</code> if those parameters
* are set to <code>null</code>. Otherwise, only <code>undefined</code> parameters will be skipped.
* Notice, mandatory parameters which are set to <code>null</code> will still be passed.
* This parameter can be used only when <code>generateClientAsService</code> is set to <code>true</code>.
*/
@Parameter
private boolean skipNullValuesForOptionalServiceArguments;

/**
* Deprecated, use {@link #restNamespacing}.
*/
Expand Down Expand Up @@ -942,6 +961,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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,51 @@ public void testQueryParameters() {
Assertions.assertTrue(output.contains("echo(queryParams: { message: string; count?: number; optionalRequestParam?: number; }): RestResponse<string>"));
}

@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<string>"));
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<string>"));
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 testUnfoldedOptionalQueryParametersGeneratedAfterRequired() {
final Settings settings = TestUtils.settings();
settings.outputFileType = TypeScriptFileType.implementationFile;
settings.generateSpringApplicationClient = true;
settings.generateClientAsService = true;
final String output = new TypeScriptGenerator(settings).generateTypeScript(Input.from(ControllerWithMultipleOptionalUnorderedParameters.class));
Assertions.assertTrue(output.contains("unorderedOptionalParamsMethod(requiredParam1: string, requiredParam4: number, optionalParam0?: number, optionalParam2?: string, optionalParam3?: number, optionalParam5?: number): RestResponse<string>"));
}

@Test
public void testAllOptionalQueryParameters() {
final Settings settings = TestUtils.settings();
Expand Down Expand Up @@ -203,6 +248,21 @@ public String echo(
}
}

@RestController
public static class ControllerWithMultipleOptionalUnorderedParameters {
@RequestMapping("/unorderedOptionalParamsMethod")
public String unorderedOptionalParamsMethod(
@RequestParam(required = false) Integer optionalParam0,
@RequestParam("requiredParam1") String requiredParam1,
@RequestParam(required = false) String optionalParam2,
@RequestParam(name = "optionalParam3", defaultValue = "1") Integer optionalParam3,
@RequestParam Integer requiredParam4,
@RequestParam(required = false) Integer optionalParam5
) {
return requiredParam1;
}
}

@Test
public void testQueryParametersWithModel() {
final Settings settings = TestUtils.settings();
Expand Down