Skip to content

Commit d3fb62c

Browse files
authored
feat(java): generic support for hits (#829)
1 parent da6474f commit d3fb62c

File tree

20 files changed

+294
-165
lines changed

20 files changed

+294
-165
lines changed

clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/ApiClient.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.algolia.utils.retry.StatefulHost;
66
import com.fasterxml.jackson.core.JsonProcessingException;
77
import com.fasterxml.jackson.core.type.TypeReference;
8+
import com.fasterxml.jackson.databind.JavaType;
89
import com.fasterxml.jackson.databind.ObjectMapper;
910
import java.io.IOException;
1011
import java.io.UnsupportedEncodingException;
@@ -26,7 +27,7 @@ public abstract class ApiClient {
2627
private String contentType;
2728

2829
private Requester requester;
29-
private ObjectMapper json;
30+
protected ObjectMapper json;
3031

3132
public ApiClient(String appId, String apiKey, String clientName, String version, ClientOptions options) {
3233
if (appId == null || appId.length() == 0) {
@@ -262,7 +263,7 @@ public RequestBody serialize(Object obj) throws AlgoliaRuntimeException {
262263
* @param returnType Return type
263264
* @see #execute(Call, TypeReference)
264265
*/
265-
public <T> CompletableFuture<T> executeAsync(Call call, final TypeReference returnType) {
266+
public <T> CompletableFuture<T> executeAsync(Call call, final JavaType returnType) {
266267
final CompletableFuture<T> future = new CompletableFuture<>();
267268
call.enqueue(
268269
new Callback() {
@@ -287,6 +288,14 @@ public void onResponse(Call call, Response response) throws IOException {
287288
return future;
288289
}
289290

291+
public <T> CompletableFuture<T> executeAsync(Call call, final Class<?> returnType, final Class<?> innerType) {
292+
return executeAsync(call, json.getTypeFactory().constructParametricType(returnType, innerType));
293+
}
294+
295+
public <T> CompletableFuture<T> executeAsync(Call call, final TypeReference returnType) {
296+
return executeAsync(call, json.getTypeFactory().constructType(returnType));
297+
}
298+
290299
/**
291300
* Build HTTP call with the given options.
292301
*

clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import com.algolia.utils.retry.RetryStrategy;
55
import com.algolia.utils.retry.StatefulHost;
66
import com.fasterxml.jackson.core.JsonProcessingException;
7-
import com.fasterxml.jackson.core.type.TypeReference;
7+
import com.fasterxml.jackson.databind.JavaType;
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import java.io.IOException;
1010
import java.util.List;
@@ -47,7 +47,7 @@ public Call newCall(Request request) {
4747
}
4848

4949
@Override
50-
public <T> T handleResponse(Response response, TypeReference returnType) throws AlgoliaRuntimeException {
50+
public <T> T handleResponse(Response response, JavaType returnType) throws AlgoliaRuntimeException {
5151
if (response.isSuccessful()) {
5252
if (returnType == null || response.code() == 204) {
5353
// returning null if the returnType is not defined, or the status code is 204 (No Content)
@@ -74,12 +74,12 @@ public <T> T handleResponse(Response response, TypeReference returnType) throws
7474
}
7575
}
7676

77-
private <T> T deserialize(Response response, TypeReference returnType) throws AlgoliaRuntimeException {
77+
private <T> T deserialize(Response response, JavaType returnType) throws AlgoliaRuntimeException {
7878
if (response == null || returnType == null) {
7979
return null;
8080
}
8181

82-
if ("byte[]".equals(returnType.getType().getTypeName())) {
82+
if ("[byte".equals(returnType.getRawClass().getName())) {
8383
// Handle binary response (byte array).
8484
try {
8585
return (T) response.body().bytes();

clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/Requester.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import com.algolia.exceptions.AlgoliaRuntimeException;
44
import com.algolia.utils.retry.StatefulHost;
5-
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.JavaType;
66
import java.util.List;
77
import okhttp3.Call;
88
import okhttp3.Request;
@@ -11,7 +11,7 @@
1111
public interface Requester {
1212
public Call newCall(Request request);
1313

14-
public <T> T handleResponse(Response response, TypeReference returnType) throws AlgoliaRuntimeException;
14+
public <T> T handleResponse(Response response, JavaType returnType) throws AlgoliaRuntimeException;
1515

1616
/**
1717
* Enable/disable debugging for this API client.

generators/src/main/java/com/algolia/codegen/AlgoliaJavaGenerator.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import java.util.*;
88
import org.openapitools.codegen.*;
99
import org.openapitools.codegen.languages.JavaClientCodegen;
10+
import org.openapitools.codegen.model.ModelMap;
1011
import org.openapitools.codegen.model.ModelsMap;
12+
import org.openapitools.codegen.model.OperationsMap;
1113

1214
@SuppressWarnings("unchecked")
1315
public class AlgoliaJavaGenerator extends JavaClientCodegen {
@@ -68,6 +70,7 @@ public void processOpts() {
6870

6971
@Override
7072
protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema schema) {
73+
// this is needed to preserve additionalProperties: true
7174
super.addParentContainer(codegenModel, codegenModel.name, schema);
7275
}
7376

@@ -81,7 +84,9 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
8184
Map<String, ModelsMap> models = super.postProcessAllModels(objs);
8285

8386
for (ModelsMap modelContainer : models.values()) {
87+
// modelContainers always have 1 and only 1 model in our specs
8488
CodegenModel model = modelContainer.getModels().get(0).getModel();
89+
8590
if (!model.oneOf.isEmpty()) {
8691
List<HashMap<String, String>> oneOfList = new ArrayList();
8792

@@ -101,12 +106,21 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
101106
}
102107
}
103108

109+
GenericPropagator.propagateGenericsToModels(models);
110+
104111
return models;
105112
}
106113

114+
@Override
115+
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
116+
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);
117+
GenericPropagator.propagateGenericsToOperations(operations, models);
118+
return operations;
119+
}
120+
107121
@Override
108122
public String toEnumVarName(String value, String datatype) {
109-
if ("String".equals(datatype)) {
123+
if ("String".equals(datatype) && !value.matches("[A-Z0-9_]+")) {
110124
// convert camelCase77String to CAMEL_CASE_77_STRING
111125
return value.replaceAll("-", "_").replaceAll("(.+?)([A-Z]|[0-9])", "$1_$2").toUpperCase(Locale.ROOT);
112126
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.algolia.codegen;
2+
3+
import com.algolia.codegen.exceptions.*;
4+
import java.util.*;
5+
import org.openapitools.codegen.*;
6+
import org.openapitools.codegen.model.*;
7+
8+
public class GenericPropagator {
9+
10+
private static Set<String> primitiveModels = new HashSet<>(Arrays.asList("object", "array", "string", "boolean", "integer"));
11+
12+
// Only static use of this class
13+
private GenericPropagator() {}
14+
15+
private static void setVendorExtension(IJsonSchemaValidationProperties property, String key, Object value) {
16+
if (property instanceof CodegenModel) {
17+
((CodegenModel) property).vendorExtensions.put(key, value);
18+
} else if (property instanceof CodegenProperty) {
19+
((CodegenProperty) property).vendorExtensions.put(key, value);
20+
}
21+
}
22+
23+
/**
24+
* Add the property x-propagated-generic to a model or property, meaning it should be replaced
25+
* with T directly
26+
*/
27+
private static void setPropagatedGeneric(IJsonSchemaValidationProperties property) {
28+
setVendorExtension(property, "x-propagated-generic", true);
29+
}
30+
31+
/**
32+
* Add the property x-has-child-generic to a model or property, meaning one of its members is
33+
* generic and it should propagate the T
34+
*/
35+
private static void setHasChildGeneric(IJsonSchemaValidationProperties property) {
36+
setVendorExtension(property, "x-has-child-generic", true);
37+
}
38+
39+
/**
40+
* @return true if the vendor extensions of the property contains either x-propagated-generic or
41+
* x-has-child-generic
42+
*/
43+
private static boolean hasGeneric(IJsonSchemaValidationProperties property) {
44+
if (property instanceof CodegenModel) {
45+
return (
46+
(boolean) ((CodegenModel) property).vendorExtensions.getOrDefault("x-propagated-generic", false) ||
47+
(boolean) ((CodegenModel) property).vendorExtensions.getOrDefault("x-has-child-generic", false)
48+
);
49+
} else if (property instanceof CodegenProperty) {
50+
return (
51+
(boolean) ((CodegenProperty) property).vendorExtensions.getOrDefault("x-propagated-generic", false) ||
52+
(boolean) ((CodegenProperty) property).vendorExtensions.getOrDefault("x-has-child-generic", false)
53+
);
54+
}
55+
return false;
56+
}
57+
58+
private static CodegenModel propertyToModel(Map<String, CodegenModel> models, CodegenProperty prop) {
59+
// openapi generator returns some weird error when looking for primitive type,
60+
// so we filter them by hand
61+
if (prop == null || primitiveModels.contains(prop.openApiType) || !models.containsKey(prop.openApiType)) {
62+
return null;
63+
}
64+
return models.get(prop.openApiType);
65+
}
66+
67+
private static boolean markPropagatedGeneric(IJsonSchemaValidationProperties model) {
68+
CodegenProperty items = model.getItems();
69+
// if items itself isn't generic, we recurse on its items and properties until we reach the
70+
// end or find a generic property
71+
if (items != null && ((boolean) items.vendorExtensions.getOrDefault("x-is-generic", false) || markPropagatedGeneric(items))) {
72+
setPropagatedGeneric(model);
73+
return true;
74+
}
75+
for (CodegenProperty var : model.getVars()) {
76+
// same thing for the var, if it's not a generic, we recurse on it until we find one
77+
if ((boolean) var.vendorExtensions.getOrDefault("x-is-generic", false) || markPropagatedGeneric(var)) {
78+
setPropagatedGeneric(model);
79+
return true;
80+
}
81+
}
82+
return false;
83+
}
84+
85+
private static boolean propagateGenericRecursive(Map<String, CodegenModel> models, IJsonSchemaValidationProperties property) {
86+
CodegenProperty items = property.getItems();
87+
// if items itself isn't generic, we recurse on its items and properties (and it's
88+
// equivalent model if we find one) until we reach the end or find a generic property.
89+
// We need to check the model too because the tree isn't complete sometime, depending on the ref
90+
// in the spec, so we get the model with the same name and recurse.
91+
if (items != null && ((hasGeneric(items) || propagateGenericRecursive(models, items) || hasGeneric(propertyToModel(models, items))))) {
92+
setHasChildGeneric(property);
93+
return true;
94+
}
95+
for (CodegenProperty var : property.getVars()) {
96+
// same thing for the var
97+
if (hasGeneric(var) || propagateGenericRecursive(models, var) || hasGeneric(propertyToModel(models, var))) {
98+
setHasChildGeneric(property);
99+
return true;
100+
}
101+
}
102+
return false;
103+
}
104+
105+
private static Map<String, CodegenModel> convertToMap(Map<String, ModelsMap> models) {
106+
Map<String, CodegenModel> modelsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
107+
for (ModelsMap modelMap : models.values()) {
108+
// modelContainers always have 1 and only 1 model in our specs
109+
CodegenModel model = modelMap.getModels().get(0).getModel();
110+
modelsMap.put(model.name, model);
111+
}
112+
return modelsMap;
113+
}
114+
115+
private static Map<String, CodegenModel> convertToMap(List<ModelMap> models) {
116+
Map<String, CodegenModel> modelsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
117+
for (ModelMap modelMap : models) {
118+
CodegenModel model = modelMap.getModel();
119+
modelsMap.put(model.name, model);
120+
}
121+
return modelsMap;
122+
}
123+
124+
/**
125+
* Models and their members will be marked with either x-propagated-generic or x-has-child-generic
126+
*/
127+
public static void propagateGenericsToModels(Map<String, ModelsMap> modelsMap) {
128+
// We propagate generics in two phases:
129+
// 1. We mark the direct parent of the generic model to replace it with T
130+
// 2. We tell each parent with generic properties to pass that generic type all the way down
131+
132+
Map<String, CodegenModel> models = convertToMap(modelsMap);
133+
134+
for (CodegenModel model : models.values()) {
135+
markPropagatedGeneric(model);
136+
}
137+
138+
for (CodegenModel model : models.values()) {
139+
propagateGenericRecursive(models, model);
140+
}
141+
}
142+
143+
/** Mark operations with a generic return type with x-is-generic */
144+
public static void propagateGenericsToOperations(OperationsMap operations, List<ModelMap> allModels) {
145+
Map<String, CodegenModel> models = convertToMap(allModels);
146+
for (CodegenOperation ope : operations.getOperations().getOperation()) {
147+
CodegenModel returnType = models.get(ope.returnType);
148+
if (returnType != null && hasGeneric(returnType)) {
149+
ope.vendorExtensions.put("x-is-generic", true);
150+
// we use {{#optionalParams.0}} to check for optionalParams, so we loose the
151+
// vendorExtensions at the operation level
152+
if (ope.optionalParams.size() > 0) {
153+
ope.optionalParams.get(0).vendorExtensions.put("x-is-generic", true);
154+
}
155+
}
156+
}
157+
}
158+
}

generators/src/main/java/com/algolia/codegen/cts/AlgoliaCTSGenerator.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.algolia.codegen.cts;
22

3+
import com.algolia.codegen.GenericPropagator;
34
import com.algolia.codegen.Utils;
45
import com.algolia.codegen.cts.manager.CTSManager;
56
import com.algolia.codegen.cts.manager.CTSManagerFactory;
@@ -13,6 +14,7 @@
1314
import org.openapitools.codegen.*;
1415
import org.openapitools.codegen.model.ModelMap;
1516
import org.openapitools.codegen.model.ModelsMap;
17+
import org.openapitools.codegen.model.OperationsMap;
1618

1719
@SuppressWarnings("unchecked")
1820
public class AlgoliaCTSGenerator extends DefaultCodegen {
@@ -66,6 +68,7 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
6668
models.put(entry.getKey(), innerModel.get(0).getModel());
6769
}
6870
}
71+
GenericPropagator.propagateGenericsToModels(mod);
6972
return mod;
7073
}
7174

@@ -160,6 +163,13 @@ private TreeMap<String, CodegenOperation> buildOperations(Map<String, Object> ob
160163
return new TreeMap<String, CodegenOperation>(result);
161164
}
162165

166+
@Override
167+
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
168+
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);
169+
GenericPropagator.propagateGenericsToOperations(operations, models);
170+
return operations;
171+
}
172+
163173
@Override
164174
public String escapeUnsafeCharacters(String input) {
165175
return input;

generators/src/main/java/com/algolia/codegen/cts/tests/TestsClient.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ public void addSupportingFiles(List<SupportingFile> supportingFiles, String outp
3333
if (!available()) {
3434
return;
3535
}
36-
String clientName = language.equals("php") ? Utils.createClientName(client, language) : client;
37-
supportingFiles.add(new SupportingFile("client/suite.mustache", outputFolder + "/client", clientName + extension));
36+
supportingFiles.add(
37+
new SupportingFile("client/suite.mustache", outputFolder + "/client", Utils.createClientName(client, language) + extension)
38+
);
3839
}
3940

4041
public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation> operations, Map<String, Object> bundle) throws Exception {

generators/src/main/java/com/algolia/codegen/cts/tests/TestsRequest.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,13 @@ public void addSupportingFiles(List<SupportingFile> supportingFiles, String outp
3535
if (!available()) {
3636
return;
3737
}
38-
String clientName = language.equals("php") ? Utils.createClientName(client, language) : client;
39-
supportingFiles.add(new SupportingFile("requests/requests.mustache", outputFolder + "/methods/requests", clientName + extension));
38+
supportingFiles.add(
39+
new SupportingFile(
40+
"requests/requests.mustache",
41+
outputFolder + "/methods/requests",
42+
Utils.createClientName(client, language) + extension
43+
)
44+
);
4045
}
4146

4247
@Override
@@ -62,6 +67,7 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
6267
test.put("testIndex", i);
6368

6469
CodegenOperation ope = entry.getValue();
70+
test.put("isGeneric", (boolean) ope.vendorExtensions.getOrDefault("x-is-generic", false));
6571

6672
// We check on the spec if body parameters should be present in the CTS
6773
// If so, we change the `null` default to an empty object, so we know if

0 commit comments

Comments
 (0)