Skip to content

Commit 3c4047a

Browse files
committed
Generate default methods
1 parent e66d333 commit 3c4047a

31 files changed

+336
-302
lines changed

spotify-web-api-generator-java/src/main/java/de/sonallux/spotify/generator/java/EndpointRequestBodyHelper.java

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.google.common.base.CaseFormat;
44
import de.sonallux.spotify.core.model.SpotifyWebApi;
55
import de.sonallux.spotify.core.model.SpotifyWebApiEndpoint;
6-
import de.sonallux.spotify.core.model.SpotifyWebApiObject;
76
import de.sonallux.spotify.generator.java.util.JavaUtils;
87

98
import java.util.List;
@@ -43,19 +42,12 @@ public static String getEndpointRequestBodyName(SpotifyWebApiEndpoint endpoint)
4342
*/
4443
public static void fixDuplicateEndpointParameters(SpotifyWebApiEndpoint endpoint) {
4544
String paramName;
46-
switch (endpoint.getId()) {
47-
case "endpoint-remove-albums-user":
48-
case "endpoint-save-albums-user":
49-
case "endpoint-follow-artists-users":
50-
case "endpoint-unfollow-artists-users":
51-
paramName = "ids";
52-
break;
53-
case "endpoint-replace-playlists-tracks":
54-
case "endpoint-add-tracks-to-playlist":
55-
paramName = "uris";
56-
break;
57-
default:
58-
return;
45+
if (endpoint.getParameters().stream().filter(p -> "ids".equals(p.getName())).count() == 2) {
46+
paramName = "ids";
47+
} else if (endpoint.getParameters().stream().filter(p -> "uris".equals(p.getName())).count() == 2) {
48+
paramName = "uris";
49+
} else {
50+
return;
5951
}
6052
endpoint.getParameters().removeIf(p -> p.getLocation() == QUERY && paramName.equals(p.getName()));
6153
for (var param : endpoint.getParameters()) {

spotify-web-api-generator-java/src/main/java/de/sonallux/spotify/generator/java/templates/CategoryTemplate.java

Lines changed: 104 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@
1010
import lombok.Getter;
1111
import lombok.Setter;
1212

13-
import java.util.ArrayList;
14-
import java.util.HashMap;
15-
import java.util.List;
16-
import java.util.Map;
17-
import java.util.stream.Collectors;
13+
import java.util.*;
14+
import java.util.concurrent.atomic.AtomicBoolean;
1815

19-
import static de.sonallux.spotify.core.model.SpotifyWebApiEndpoint.ParameterLocation.*;
16+
import static java.util.stream.Collectors.joining;
17+
import static java.util.stream.Collectors.toList;
2018

2119
public class CategoryTemplate extends AbstractTemplate<SpotifyWebApiCategory> {
2220

@@ -41,7 +39,7 @@ Map<String, Object> buildContext(SpotifyWebApiCategory category, Map<String, Obj
4139
context.put("name", category.getName());
4240
context.put("className", JavaUtils.getClassName(category));
4341
context.put("documentationLink", category.getLink());
44-
context.put("endpoints", category.getEndpointList().stream().flatMap(e -> buildEndpointContext(e).stream()).collect(Collectors.toList()));
42+
context.put("endpoints", category.getEndpointList().stream().flatMap(e -> buildEndpointContext(e).stream()).collect(toList()));
4543
return context;
4644
}
4745

@@ -66,106 +64,143 @@ private List<Map<String, Object>> buildEndpointContext(SpotifyWebApiEndpoint end
6664
baseContext.put("responseType", getResponseType(endpoint));
6765
baseContext.put("documentationLink", endpoint.getLink());
6866

69-
List<Map<String, Object>> contexts = new ArrayList<>();
70-
var arguments = getArguments(endpoint);
71-
for (var args : arguments) {
72-
var context = new HashMap<>(baseContext);
73-
context.put("arguments", args.stream().map(Argument::asMethodArgument).collect(Collectors.joining(", ")));
74-
context.put("javaDocParams", args.stream().map(Argument::asJavaDoc).collect(Collectors.toList()));
75-
if (endpoint.getHttpMethod().equals("DELETE") && args.stream().anyMatch(a -> a.getAnnotation().startsWith("@Body"))) {
76-
// Officially DELETE does not allow a request body, but Spotify uses it.
77-
// This adjusts the http method annotation, so retrofit does not throw an error.
78-
context.put("deleteWithBody", true);
79-
}
80-
contexts.add(context);
81-
}
82-
return contexts;
83-
}
84-
85-
private List<List<Argument>> getArguments(SpotifyWebApiEndpoint endpoint) {
8667
EndpointRequestBodyHelper.fixDuplicateEndpointParameters(endpoint);
8768

88-
List<Argument> requiredArgs = new ArrayList<>();
89-
endpoint.getParameters().stream()
90-
.filter(p -> p.getLocation() == PATH)
91-
.map(p -> new Argument("@Path(\"" + p.getName() + "\")", JavaUtils.mapToPrimitiveJavaType(p.getType()), p.getName(), p.getDescription()))
92-
.forEach(requiredArgs::add);
93-
94-
endpoint.getParameters().stream()
95-
.filter(p -> p.getLocation() == QUERY && p.isRequired())
96-
.map(p -> new Argument("@Query(\"" + p.getName() + "\")", JavaUtils.mapToPrimitiveJavaType(p.getType()), p.getName(), p.getDescription()))
97-
.forEach(requiredArgs::add);
98-
99-
boolean requestBodyArgAdded = false;
100-
if (endpoint.getParameters().stream().anyMatch(p -> p.getLocation() == BODY && p.isRequired())) {
101-
requestBodyArgAdded = true;
102-
requiredArgs.add(new Argument(
103-
"@Body", EndpointRequestBodyHelper.getEndpointRequestBodyName(endpoint), "requestBody", "the request body"));
69+
var rawArguments = argumentsFromEndpoint(endpoint);
70+
List<Parameter> requiredParameters = new ArrayList<>();
71+
List<Parameter> optionalParameters = new ArrayList<>();
72+
for (var rawArgument : rawArguments) {
73+
if (rawArgument.isRequired()) {
74+
requiredParameters.add(rawArgument);
75+
} else {
76+
optionalParameters.add(rawArgument);
77+
}
10478
}
10579

106-
List<List<Argument>> args = new ArrayList<>();
107-
args.add(new ArrayList<>(requiredArgs));
108-
109-
var optionalQueryArgs = endpoint.getParameters().stream().filter(p -> p.getLocation() == QUERY && !p.isRequired()).collect(Collectors.toList());
110-
if (optionalQueryArgs.size() == 1) {
111-
var p = optionalQueryArgs.get(0);
112-
requiredArgs.add(new Argument(
113-
"@Query(\"" + p.getName() + "\")", JavaUtils.mapToPrimitiveJavaType(p.getType()), p.getName(), p.getDescription()));
114-
args.add(requiredArgs);
115-
} else if (optionalQueryArgs.size() > 1) {
116-
requiredArgs.add(new Argument(
117-
"@QueryMap", "java.util.Map<String, Object>", "queryParameters", "A map of optional query parameters"));
118-
args.add(requiredArgs);
80+
requiredParameters.sort(Comparator.comparing(Parameter::getOrder));
81+
optionalParameters.sort(Comparator.comparing(Parameter::getOrder));
82+
83+
if (endpoint.getHttpMethod().equals("DELETE") && rawArguments.stream().anyMatch(a -> a.getAnnotation().startsWith("@Body"))) {
84+
// Officially DELETE does not allow a request body, but Spotify uses it.
85+
// This adjusts the http method annotation, so retrofit does not throw an error.
86+
baseContext.put("deleteWithBody", true);
11987
}
12088

121-
if (!requestBodyArgAdded && endpoint.getParameters().stream().anyMatch(p -> p.getLocation() == BODY)) {
122-
List<List<Argument>> endpointArguments = new ArrayList<>();
123-
for (var arguments : args) {
124-
var duplicateArguments = new ArrayList<>(arguments);
125-
duplicateArguments.add(new Argument(
126-
"@Body", EndpointRequestBodyHelper.getEndpointRequestBodyName(endpoint), "requestBody", "The request body"));
127-
endpointArguments.add(arguments);
128-
endpointArguments.add(duplicateArguments);
89+
List<Parameter> allParameters;
90+
if (optionalParameters.size() == 0) {
91+
baseContext.put("requiredParametersMethod", false);
92+
allParameters = requiredParameters;
93+
} else {
94+
allParameters = new ArrayList<>(requiredParameters);
95+
var arguments = requiredParameters.stream().map(Parameter::getFieldName).collect(toList());
96+
97+
// If multiple query parameters are optional, wrap them together in one @QueryMap parameter
98+
var optionalArgumentsWithoutQuery = optionalParameters.stream().filter(a -> !a.getAnnotation().startsWith("@Query")).collect(toList());
99+
if (optionalParameters.size() - optionalArgumentsWithoutQuery.size() > 1) {
100+
allParameters.add(new Parameter("@QueryMap", "java.util.Map<String, Object>", "queryParameters", "A map of optional query parameters", false, 4));
101+
arguments.add("java.util.Map.of()");
102+
optionalParameters = optionalArgumentsWithoutQuery;
129103
}
130-
return endpointArguments;
104+
105+
optionalParameters.forEach(optionalArg -> {
106+
allParameters.add(optionalArg);
107+
arguments.add(getDefaultArgumentValue(endpoint, optionalArg));
108+
});
109+
baseContext.put("requiredParametersMethod", true);
110+
baseContext.put("requiredParameters", requiredParameters.stream().map(Parameter::asMethodArgumentWithoutAnnotation).collect(joining(", ")));
111+
baseContext.put("requiredJavaDocParameters", requiredParameters.stream().map(Parameter::asJavaDoc).collect(toList()));
112+
baseContext.put("arguments", String.join(", ", arguments));
131113
}
132114

133-
return args;
115+
baseContext.put("parameters", allParameters.stream().map(Parameter::asMethodArgument).collect(joining(", ")));
116+
baseContext.put("javaDocParameters", allParameters.stream().map(Parameter::asJavaDoc).collect(toList()));
117+
return List.of(baseContext);
118+
}
119+
120+
private String getDefaultArgumentValue(SpotifyWebApiEndpoint endpoint, Parameter parameter) {
121+
if (parameter.getAnnotation().startsWith("@Body")) {
122+
return "new " + EndpointRequestBodyHelper.getEndpointRequestBodyName(endpoint) + "()";
123+
} else {
124+
return "null";
125+
}
134126
}
135127

136128
private String getResponseType(SpotifyWebApiEndpoint endpoint) {
137-
if (endpoint.getResponseTypes().size() == 1
138-
|| 1 == endpoint.getResponseTypes().stream()
139-
.map(SpotifyWebApiEndpoint.ResponseType::getType).distinct().count()) {
129+
if (endpoint.getResponseTypes().stream()
130+
.map(SpotifyWebApiEndpoint.ResponseType::getType)
131+
.distinct().count() == 1) {
140132
return JavaUtils.mapToPrimitiveJavaType(endpoint.getResponseTypes().get(0).getType());
141133
}
142134
var nonVoidResponseTypes = endpoint.getResponseTypes().stream()
143-
.map(SpotifyWebApiEndpoint.ResponseType::getType).filter(t -> !"Void".equals(t)).distinct().collect(Collectors.toList());
135+
.map(SpotifyWebApiEndpoint.ResponseType::getType).filter(t -> !"Void".equals(t)).distinct().collect(toList());
144136
if (nonVoidResponseTypes.size() == 1) {
145137
return JavaUtils.mapToPrimitiveJavaType(endpoint.getResponseTypes().get(0).getType());
146138
}
147139
return "";
148140
}
149141

142+
private List<Parameter> argumentsFromEndpoint(SpotifyWebApiEndpoint endpoint) {
143+
var hasBodyParameter = new AtomicBoolean(false);
144+
var hasRequiredBodyParameter = new AtomicBoolean(false);
145+
146+
var arguments = endpoint.getParameters().stream().map(parameter -> {
147+
switch (parameter.getLocation()) {
148+
case PATH: {
149+
return new Parameter("@Path(\"" + parameter.getName() + "\")", parameter, 1);
150+
}
151+
case QUERY: {
152+
return new Parameter("@Query(\"" + parameter.getName() + "\")", parameter, 2);
153+
}
154+
case BODY: {
155+
hasBodyParameter.set(true);
156+
if (parameter.isRequired()) {
157+
hasRequiredBodyParameter.set(true);
158+
}
159+
return null;
160+
}
161+
case HEADER: // Ignore header parameters because they are only Authorization and Content-Type header
162+
default: return null;
163+
}
164+
}).filter(Objects::nonNull).collect(toList());
165+
166+
if (hasBodyParameter.get()) {
167+
var requestBodyType = EndpointRequestBodyHelper.getEndpointRequestBodyName(endpoint);
168+
arguments.add(new Parameter("@Body", requestBodyType, "requestBody", "The request body", hasRequiredBodyParameter.get(), 3));
169+
}
170+
return arguments;
171+
}
172+
150173
@Getter
151174
@Setter
152-
private static class Argument {
175+
private static class Parameter {
153176
private String annotation;
154177
private String type;
155178
private String fieldName;
156179
private String description;
180+
private boolean isRequired;
181+
private int order;
157182

158-
public Argument(String annotation, String type, String fieldName, String description) {
183+
public Parameter(String annotation, String type, String fieldName, String description, boolean isRequired, int order) {
159184
this.annotation = annotation;
160185
this.type = type;
161186
this.fieldName = JavaUtils.escapeFieldName(fieldName);
162187
this.description = Markdown2Html.convertToSingleLine(description);
188+
this.isRequired = isRequired;
189+
this.order = order;
190+
}
191+
192+
private Parameter(String annotation, SpotifyWebApiEndpoint.Parameter parameter, int order) {
193+
// Do not map type to primitive type, so we can pass null to it if it is optional
194+
this(annotation, JavaUtils.mapToJavaType(parameter.getType()), parameter.getName(), parameter.getDescription(), parameter.isRequired(), order);
163195
}
164196

165197
public String asMethodArgument() {
166198
return annotation + " " + type + " " + fieldName;
167199
}
168200

201+
public String asMethodArgumentWithoutAnnotation() {
202+
return type + " " + fieldName;
203+
}
169204
public String asJavaDoc() {
170205
return "@param " + fieldName + " " + description;
171206
}

spotify-web-api-generator-java/src/main/java/de/sonallux/spotify/generator/java/templates/ObjectTemplate.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package de.sonallux.spotify.generator.java.templates;
22

3-
import com.google.common.base.CaseFormat;
43
import com.google.common.base.Strings;
54
import de.sonallux.spotify.core.model.SpotifyWebApiObject;
65
import de.sonallux.spotify.generator.java.util.JavaPackage;

spotify-web-api-generator-java/src/main/java/de/sonallux/spotify/generator/java/templates/RequestBodyTemplate.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ private Map<String, Object> buildPropertyContext(Property property) {
6565
context.put("hasDescription", true);
6666
context.put("description", Markdown2Html.convertToLines(description));
6767
}
68-
context.put("type", JavaUtils.mapToPrimitiveJavaType(property.getType()));
68+
69+
// Do not use primitive type here, so parameters can be set to null
70+
context.put("type", JavaUtils.mapToJavaType(property.getType()));
6971
context.put("required", property.isRequired());
7072

7173
return context;

spotify-web-api-generator-java/src/main/resources/templates/SpotifyWebApi.mustache

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package {{package}};
22

3+
import com.fasterxml.jackson.annotation.JsonInclude;
34
import com.fasterxml.jackson.databind.DeserializationFeature;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
@@ -45,6 +46,7 @@ public class SpotifyWebApi extends BaseSpotifyApi {
4546
private static Retrofit createDefaultRetrofit(OkHttpClient okHttpClient, HttpUrl baseUrl) {
4647
var mapper = new ObjectMapper()
4748
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
49+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
4850
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
4951
.registerModule(new JavaTimeModule());
5052
return new Retrofit.Builder()

spotify-web-api-generator-java/src/main/resources/templates/category.mustache

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import retrofit2.http.*;
99
*/
1010
public interface {{className}} {
1111
{{#endpoints}}
12+
{{#requiredParametersMethod}}
1213

1314
/**
1415
* <h3>{{name}}</h3>
@@ -25,9 +26,38 @@ public interface {{className}} {
2526
{{/notes}}
2627
*
2728
{{/hasNotes}}
28-
{{#javaDocParams}}
29+
{{#requiredJavaDocParameters}}
2930
* {{.}}
30-
{{/javaDocParams}}
31+
{{/requiredJavaDocParameters}}
32+
* @return {{responseDescriptionFirstLine}}
33+
{{#responseDescriptionOthers}}
34+
* {{.}}
35+
{{/responseDescriptionOthers}}
36+
* @see <a href="{{documentationLink}}">{{name}}</a>
37+
*/
38+
default Call<{{responseType}}> {{methodName}}({{requiredParameters}}) {
39+
return {{methodName}}({{arguments}});
40+
}
41+
{{/requiredParametersMethod}}
42+
43+
/**
44+
* <h3>{{name}}</h3>
45+
* {{description}}
46+
{{#scopes}}
47+
* <h3>Required OAuth scopes</h3>
48+
* <code>{{scopes}}</code>
49+
{{/scopes}}
50+
*
51+
{{#hasNotes}}
52+
* <h3>Notes</h3>
53+
{{#notes}}
54+
* {{.}}
55+
{{/notes}}
56+
*
57+
{{/hasNotes}}
58+
{{#javaDocParameters}}
59+
* {{.}}
60+
{{/javaDocParameters}}
3161
* @return {{responseDescriptionFirstLine}}
3262
{{#responseDescriptionOthers}}
3363
* {{.}}
@@ -40,6 +70,6 @@ public interface {{className}} {
4070
{{^deleteWithBody}}
4171
@{{httpMethod}}("{{path}}")
4272
{{/deleteWithBody}}
43-
Call<{{responseType}}> {{methodName}}({{arguments}});
73+
Call<{{responseType}}> {{methodName}}({{parameters}});
4474
{{/endpoints}}
4575
}

spotify-web-api-java/src/main/generated/de/sonallux/spotify/api/SpotifyWebApi.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.sonallux.spotify.api;
22

3+
import com.fasterxml.jackson.annotation.JsonInclude;
34
import com.fasterxml.jackson.databind.DeserializationFeature;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
@@ -67,6 +68,7 @@ public SpotifyWebApi() {
6768
private static Retrofit createDefaultRetrofit(OkHttpClient okHttpClient, HttpUrl baseUrl) {
6869
var mapper = new ObjectMapper()
6970
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
71+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
7072
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
7173
.registerModule(new JavaTimeModule());
7274
return new Retrofit.Builder()

0 commit comments

Comments
 (0)