Skip to content

feat(java): generic support for hits #829

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

Merged
merged 17 commits into from
Jul 21, 2022
Merged
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 @@ -5,6 +5,7 @@
import com.algolia.utils.retry.StatefulHost;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
Expand All @@ -26,7 +27,7 @@ public abstract class ApiClient {
private String contentType;

private Requester requester;
private ObjectMapper json;
protected ObjectMapper json;

public ApiClient(String appId, String apiKey, String clientName, String version, ClientOptions options) {
if (appId == null || appId.length() == 0) {
Expand Down Expand Up @@ -262,7 +263,7 @@ public RequestBody serialize(Object obj) throws AlgoliaRuntimeException {
* @param returnType Return type
* @see #execute(Call, TypeReference)
*/
public <T> CompletableFuture<T> executeAsync(Call call, final TypeReference returnType) {
public <T> CompletableFuture<T> executeAsync(Call call, final JavaType returnType) {
final CompletableFuture<T> future = new CompletableFuture<>();
call.enqueue(
new Callback() {
Expand All @@ -287,6 +288,14 @@ public void onResponse(Call call, Response response) throws IOException {
return future;
}

public <T> CompletableFuture<T> executeAsync(Call call, final Class<?> returnType, final Class<?> innerType) {
return executeAsync(call, json.getTypeFactory().constructParametricType(returnType, innerType));
}

public <T> CompletableFuture<T> executeAsync(Call call, final TypeReference returnType) {
return executeAsync(call, json.getTypeFactory().constructType(returnType));
}

/**
* Build HTTP call with the given options.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.algolia.utils.retry.RetryStrategy;
import com.algolia.utils.retry.StatefulHost;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
Expand Down Expand Up @@ -47,7 +47,7 @@ public Call newCall(Request request) {
}

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

private <T> T deserialize(Response response, TypeReference returnType) throws AlgoliaRuntimeException {
private <T> T deserialize(Response response, JavaType returnType) throws AlgoliaRuntimeException {
if (response == null || returnType == null) {
return null;
}

if ("byte[]".equals(returnType.getType().getTypeName())) {
if ("[byte".equals(returnType.getRawClass().getName())) {
// Handle binary response (byte array).
try {
return (T) response.body().bytes();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.algolia.exceptions.AlgoliaRuntimeException;
import com.algolia.utils.retry.StatefulHost;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import java.util.List;
import okhttp3.Call;
import okhttp3.Request;
Expand All @@ -11,7 +11,7 @@
public interface Requester {
public Call newCall(Request request);

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

/**
* Enable/disable debugging for this API client.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import java.util.*;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.JavaClientCodegen;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;

@SuppressWarnings("unchecked")
public class AlgoliaJavaGenerator extends JavaClientCodegen {
Expand Down Expand Up @@ -68,6 +70,7 @@ public void processOpts() {

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

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

for (ModelsMap modelContainer : models.values()) {
// modelContainers always have 1 and only 1 model in our specs
CodegenModel model = modelContainer.getModels().get(0).getModel();

if (!model.oneOf.isEmpty()) {
List<HashMap<String, String>> oneOfList = new ArrayList();

Expand All @@ -101,12 +106,21 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
}
}

GenericPropagator.propagateGenericsToModels(models);

return models;
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);
GenericPropagator.propagateGenericsToOperations(operations, models);
return operations;
}

@Override
public String toEnumVarName(String value, String datatype) {
if ("String".equals(datatype)) {
if ("String".equals(datatype) && !value.matches("[A-Z0-9_]+")) {
// convert camelCase77String to CAMEL_CASE_77_STRING
return value.replaceAll("-", "_").replaceAll("(.+?)([A-Z]|[0-9])", "$1_$2").toUpperCase(Locale.ROOT);
}
Expand Down
158 changes: 158 additions & 0 deletions generators/src/main/java/com/algolia/codegen/GenericPropagator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package com.algolia.codegen;

import com.algolia.codegen.exceptions.*;
import java.util.*;
import org.openapitools.codegen.*;
import org.openapitools.codegen.model.*;

public class GenericPropagator {

private static Set<String> primitiveModels = new HashSet<>(Arrays.asList("object", "array", "string", "boolean", "integer"));

// Only static use of this class
private GenericPropagator() {}

private static void setVendorExtension(IJsonSchemaValidationProperties property, String key, Object value) {
if (property instanceof CodegenModel) {
((CodegenModel) property).vendorExtensions.put(key, value);
} else if (property instanceof CodegenProperty) {
((CodegenProperty) property).vendorExtensions.put(key, value);
}
}

/**
* Add the property x-propagated-generic to a model or property, meaning it should be replaced
* with T directly
*/
private static void setPropagatedGeneric(IJsonSchemaValidationProperties property) {
setVendorExtension(property, "x-propagated-generic", true);
}

/**
* Add the property x-has-child-generic to a model or property, meaning one of its members is
* generic and it should propagate the T
*/
private static void setHasChildGeneric(IJsonSchemaValidationProperties property) {
setVendorExtension(property, "x-has-child-generic", true);
}
Comment on lines +23 to +37
Copy link
Member

@shortcuts shortcuts Jul 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't even need them but I can understand you want to keep it to make it clearer


/**
* @return true if the vendor extensions of the property contains either x-propagated-generic or
* x-has-child-generic
*/
private static boolean hasGeneric(IJsonSchemaValidationProperties property) {
if (property instanceof CodegenModel) {
return (
(boolean) ((CodegenModel) property).vendorExtensions.getOrDefault("x-propagated-generic", false) ||
(boolean) ((CodegenModel) property).vendorExtensions.getOrDefault("x-has-child-generic", false)
);
} else if (property instanceof CodegenProperty) {
return (
(boolean) ((CodegenProperty) property).vendorExtensions.getOrDefault("x-propagated-generic", false) ||
(boolean) ((CodegenProperty) property).vendorExtensions.getOrDefault("x-has-child-generic", false)
);
}
return false;
}

private static CodegenModel propertyToModel(Map<String, CodegenModel> models, CodegenProperty prop) {
// openapi generator returns some weird error when looking for primitive type,
// so we filter them by hand
if (prop == null || primitiveModels.contains(prop.openApiType) || !models.containsKey(prop.openApiType)) {
return null;
}
return models.get(prop.openApiType);
}

private static boolean markPropagatedGeneric(IJsonSchemaValidationProperties model) {
CodegenProperty items = model.getItems();
// if items itself isn't generic, we recurse on its items and properties until we reach the
// end or find a generic property
if (items != null && ((boolean) items.vendorExtensions.getOrDefault("x-is-generic", false) || markPropagatedGeneric(items))) {
setPropagatedGeneric(model);
return true;
}
for (CodegenProperty var : model.getVars()) {
// same thing for the var, if it's not a generic, we recurse on it until we find one
if ((boolean) var.vendorExtensions.getOrDefault("x-is-generic", false) || markPropagatedGeneric(var)) {
setPropagatedGeneric(model);
return true;
}
}
return false;
}

private static boolean propagateGenericRecursive(Map<String, CodegenModel> models, IJsonSchemaValidationProperties property) {
CodegenProperty items = property.getItems();
// if items itself isn't generic, we recurse on its items and properties (and it's
// equivalent model if we find one) until we reach the end or find a generic property.
// We need to check the model too because the tree isn't complete sometime, depending on the ref
// in the spec, so we get the model with the same name and recurse.
if (items != null && ((hasGeneric(items) || propagateGenericRecursive(models, items) || hasGeneric(propertyToModel(models, items))))) {
setHasChildGeneric(property);
return true;
}
for (CodegenProperty var : property.getVars()) {
// same thing for the var
if (hasGeneric(var) || propagateGenericRecursive(models, var) || hasGeneric(propertyToModel(models, var))) {
setHasChildGeneric(property);
return true;
}
}
return false;
}

private static Map<String, CodegenModel> convertToMap(Map<String, ModelsMap> models) {
Map<String, CodegenModel> modelsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (ModelsMap modelMap : models.values()) {
// modelContainers always have 1 and only 1 model in our specs
CodegenModel model = modelMap.getModels().get(0).getModel();
modelsMap.put(model.name, model);
}
return modelsMap;
}

private static Map<String, CodegenModel> convertToMap(List<ModelMap> models) {
Map<String, CodegenModel> modelsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (ModelMap modelMap : models) {
CodegenModel model = modelMap.getModel();
modelsMap.put(model.name, model);
}
return modelsMap;
}

/**
* Models and their members will be marked with either x-propagated-generic or x-has-child-generic
*/
public static void propagateGenericsToModels(Map<String, ModelsMap> modelsMap) {
// We propagate generics in two phases:
// 1. We mark the direct parent of the generic model to replace it with T
// 2. We tell each parent with generic properties to pass that generic type all the way down

Map<String, CodegenModel> models = convertToMap(modelsMap);

for (CodegenModel model : models.values()) {
markPropagatedGeneric(model);
}

for (CodegenModel model : models.values()) {
propagateGenericRecursive(models, model);
}
}

/** Mark operations with a generic return type with x-is-generic */
public static void propagateGenericsToOperations(OperationsMap operations, List<ModelMap> allModels) {
Map<String, CodegenModel> models = convertToMap(allModels);
for (CodegenOperation ope : operations.getOperations().getOperation()) {
CodegenModel returnType = models.get(ope.returnType);
if (returnType != null && hasGeneric(returnType)) {
ope.vendorExtensions.put("x-is-generic", true);
// we use {{#optionalParams.0}} to check for optionalParams, so we loose the
// vendorExtensions at the operation level
if (ope.optionalParams.size() > 0) {
ope.optionalParams.get(0).vendorExtensions.put("x-is-generic", true);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.algolia.codegen.cts;

import com.algolia.codegen.GenericPropagator;
import com.algolia.codegen.Utils;
import com.algolia.codegen.cts.manager.CTSManager;
import com.algolia.codegen.cts.manager.CTSManagerFactory;
Expand All @@ -13,6 +14,7 @@
import org.openapitools.codegen.*;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;

@SuppressWarnings("unchecked")
public class AlgoliaCTSGenerator extends DefaultCodegen {
Expand Down Expand Up @@ -66,6 +68,7 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
models.put(entry.getKey(), innerModel.get(0).getModel());
}
}
GenericPropagator.propagateGenericsToModels(mod);
return mod;
}

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

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);
GenericPropagator.propagateGenericsToOperations(operations, models);
return operations;
}

@Override
public String escapeUnsafeCharacters(String input) {
return input;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public void addSupportingFiles(List<SupportingFile> supportingFiles, String outp
if (!available()) {
return;
}
String clientName = language.equals("php") ? Utils.createClientName(client, language) : client;
supportingFiles.add(new SupportingFile("client/suite.mustache", outputFolder + "/client", clientName + extension));
supportingFiles.add(
new SupportingFile("client/suite.mustache", outputFolder + "/client", Utils.createClientName(client, language) + extension)
);
}

public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation> operations, Map<String, Object> bundle) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ public void addSupportingFiles(List<SupportingFile> supportingFiles, String outp
if (!available()) {
return;
}
String clientName = language.equals("php") ? Utils.createClientName(client, language) : client;
supportingFiles.add(new SupportingFile("requests/requests.mustache", outputFolder + "/methods/requests", clientName + extension));
supportingFiles.add(
new SupportingFile(
"requests/requests.mustache",
outputFolder + "/methods/requests",
Utils.createClientName(client, language) + extension
)
);
}

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

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

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