Skip to content

feat(clients): flatten body parameters #4849

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
19 changes: 8 additions & 11 deletions clients/algoliasearch-client-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,37 +47,34 @@ import "github.com/algolia/algoliasearch-client-go/v4/algolia/search"
client, err := search.NewClient("YOUR_APP_ID", "YOUR_API_KEY")

// Add a new record to your Algolia index
response, err := client.SaveObject(client.NewApiSaveObjectRequest(
"<YOUR_INDEX_NAME>", map[string]any{"objectID": "id", "test": "val"},
))
saveResponse, err := client.SaveObject("<YOUR_INDEX_NAME>", map[string]any{"objectID": "id", "test": "val"})
if err != nil {
// handle the eventual error
panic(err)
}

// use the model directly
print(response)
print(saveResponse)

// Poll the task status to know when it has been indexed
taskResponse, err := searchClient.WaitForTask("<YOUR_INDEX_NAME>", response.TaskID, nil, nil, nil)
_, err = client.WaitForTask("<YOUR_INDEX_NAME>", saveResponse.TaskID)
if err != nil {
panic(err)
}

// Fetch search results, with typo tolerance
response, err := client.Search(client.NewApiSearchRequest(

search.NewEmptySearchMethodParams().SetRequests(
[]search.SearchQuery{*search.SearchForHitsAsSearchQuery(
search.NewEmptySearchForHits().SetIndexName("<YOUR_INDEX_NAME>").SetQuery("<YOUR_QUERY>").SetHitsPerPage(50))}),
))
response, err := client.Search([]search.SearchQuery{
*search.SearchForHitsAsSearchQuery(search.NewSearchForHits("<YOUR_INDEX_NAME>").SetQuery("<YOUR_QUERY>").SetHitsPerPage(50)),
}, nil)
if err != nil {
// handle the eventual error
panic(err)
}

// use the model directly
print(response)

return 0
```

For full documentation, visit the **[Algolia Go API Client](https://www.algolia.com/doc/libraries/go/)**.
Expand Down
108 changes: 106 additions & 2 deletions generators/src/main/java/com/algolia/codegen/AlgoliaGoGenerator.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.algolia.codegen;

import com.algolia.codegen.cts.lambda.ScreamingSnakeCaseLambda;
import com.algolia.codegen.exceptions.*;
import com.algolia.codegen.lambda.ScreamingSnakeCaseLambda;
import com.algolia.codegen.utils.*;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.samskivert.mustache.Mustache;
Expand All @@ -11,6 +13,7 @@
import io.swagger.v3.oas.models.servers.Server;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.GoClientCodegen;
import org.openapitools.codegen.model.ModelMap;
Expand All @@ -19,6 +22,9 @@

public class AlgoliaGoGenerator extends GoClientCodegen {

// This is used for the CTS generation
private static final AlgoliaGoGenerator INSTANCE = new AlgoliaGoGenerator();

@Override
public String getName() {
return "algolia-go";
Expand All @@ -29,7 +35,6 @@ public void processOpts() {
String client = (String) additionalProperties.get("client");

additionalProperties.put("packageName", client.equals("query-suggestions") ? "suggestions" : Helpers.camelize(client));
additionalProperties.put("enumClassPrefix", true);
additionalProperties.put("is" + Helpers.capitalize(Helpers.camelize(client)) + "Client", true);

String outputFolder = "algolia" + File.separator + client;
Expand Down Expand Up @@ -78,6 +83,10 @@ public void processOpenAPI(OpenAPI openAPI) {
super.processOpenAPI(openAPI);
Helpers.generateServers(super.fromServers(openAPI.getServers()), additionalProperties);
Timeouts.enrichBundle(openAPI, additionalProperties);
additionalProperties.put(
"appDescription",
Arrays.stream(openAPI.getInfo().getDescription().split("\n")).map(line -> "// " + line).collect(Collectors.joining("\n")).trim()
);
}

@Override
Expand Down Expand Up @@ -140,9 +149,104 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> models) {
OperationsMap operations = super.postProcessOperationsWithModels(objs, models);

// Flatten body params to remove the wrapping object
for (CodegenOperation ope : operations.getOperations().getOperation()) {
// clean up the description
String[] lines = ope.unescapedNotes.split("\n");
ope.notes = (lines[0] + "\n" + Arrays.stream(lines).skip(1).map(line -> "// " + line).collect(Collectors.joining("\n"))).trim();

// enrich the params
for (CodegenParameter param : ope.optionalParams) {
param.nameInPascalCase = Helpers.capitalize(param.baseName);
}

CodegenParameter bodyParam = ope.bodyParam;
if (bodyParam != null) {
flattenBody(ope);
}

// If the optional param struct only has 1 param, we can remove the wrapper
if (ope.optionalParams.size() == 1) {
CodegenParameter param = ope.optionalParams.get(0);

// move it to required, it's easier to handle im mustache
ope.hasOptionalParams = false;
ope.optionalParams.clear();

ope.hasRequiredParams = true;
ope.requiredParams.add(param);
}
}

ModelPruner.removeOrphans(this, operations, models);
Helpers.removeHelpers(operations);
GenericPropagator.propagateGenericsToOperations(operations, models);
return operations;
}

private void flattenBody(CodegenOperation ope) {
CodegenParameter bodyParam = ope.bodyParam;
bodyParam.nameInPascalCase = Helpers.capitalize(bodyParam.baseName);
if (!bodyParam.isModel) {
return;
}

if (!canFlattenBody(ope)) {
System.out.println(
"Operation " + ope.operationId + " has body param " + bodyParam.paramName + " in colision with a parameter, skipping flattening"
);
return;
}

bodyParam.vendorExtensions.put("x-flat-body", bodyParam.getVars().size() > 0);

if (bodyParam.getVars().size() > 0) {
ope.allParams.removeIf(param -> param.isBodyParam);
ope.requiredParams.removeIf(param -> param.isBodyParam);
ope.optionalParams.removeIf(param -> param.isBodyParam);
}

for (CodegenProperty prop : bodyParam.getVars()) {
prop.nameInLowerCase = toParamName(prop.baseName);

CodegenParameter param = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.convertValue(prop, CodegenParameter.class);
param.nameInPascalCase = Helpers.capitalize(prop.baseName);
param.paramName = toParamName(prop.baseName);

if (prop.required) {
ope.requiredParams.add(param);
ope.hasRequiredParams = true;
} else {
ope.optionalParams.add(param);
ope.hasOptionalParams = true;
}
ope.allParams.add(param);
}
}

public static boolean canFlattenBody(CodegenOperation ope) {
if (ope.bodyParam == null || !ope.bodyParam.isModel) {
return false;
}

if (ope.allParams.size() == 1) {
return true;
}

for (CodegenProperty prop : ope.bodyParam.getVars()) {
for (CodegenParameter param : ope.allParams) {
if (param.paramName.equals(prop.baseName)) {
return false;
}
}
}
return true;
}

public static String toEnum(String value) {
return INSTANCE.toEnumVarName(value, "String");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,20 +237,13 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
continue;
}

boolean hasBodyParams = !ope.bodyParams.isEmpty();
boolean hasBodyParams = ope.bodyParam != null;
boolean hasHeaderParams = !ope.headerParams.isEmpty();
boolean hasQueryParams = !ope.queryParams.isEmpty();
boolean hasPathParams = !ope.pathParams.isEmpty();

// If there is nothing but body params, we just check if it's a single param
if (
hasBodyParams &&
!hasHeaderParams &&
!hasQueryParams &&
!hasPathParams &&
ope.bodyParams.size() == 1 &&
!ope.bodyParams.get(0).isArray
) {
if (hasBodyParams && !hasHeaderParams && !hasQueryParams && !hasPathParams && !ope.bodyParam.isArray) {
// At this point the single parameter is already an object, to avoid double wrapping we skip
// it
ope.vendorExtensions.put("x-is-single-body-param", true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ protected Builder<String, Lambda> addMustacheLambdas() {
lambdas.put("escapeSlash", new EscapeSlashLambda());
lambdas.put("escapeJSON", new EscapeJSONLambda());
lambdas.put("replaceBacktick", new ReplaceBacktickLambda());
lambdas.put("screamingSnakeCase", new ScreamingSnakeCaseLambda());

return lambdas;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class DynamicSnippetLambda implements Mustache.Lambda {
private final Map<String, CodegenOperation> operations;

private final Map<String, Snippet> snippets;
private final String language;

public DynamicSnippetLambda(
DefaultCodegen generator,
Expand All @@ -41,6 +42,7 @@ public DynamicSnippetLambda(
String client
) {
this.operations = operations;
this.language = language;
this.paramsType = new ParametersWithDataType(models, language, client, true);

JsonNode snippetsFile = Helpers.readJsonFile("tests/CTS/guides/" + client + ".json");
Expand Down Expand Up @@ -74,7 +76,7 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio

// set the method attributes
Map<String, Object> context = (Map<String, Object>) fragment.context();
snippet.addMethodCall(context, paramsType, operation);
snippet.addMethodCall(language, context, paramsType, operation);

writer.write(adaptor.compileTemplate(executor, context, "tests/method.mustache"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.algolia.codegen.lambda;
package com.algolia.codegen.cts.lambda;

import com.algolia.codegen.utils.Helpers;
import com.samskivert.mustache.Mustache;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.algolia.codegen.cts.manager;

import com.algolia.codegen.AlgoliaGoGenerator;
import com.algolia.codegen.exceptions.GeneratorException;
import com.algolia.codegen.utils.*;
import com.samskivert.mustache.Mustache.Lambda;
import java.util.*;
import org.openapitools.codegen.SupportingFile;

Expand Down Expand Up @@ -40,4 +42,9 @@ public void addSnippetsSupportingFiles(List<SupportingFile> supportingFiles, Str
supportingFiles.add(new SupportingFile("snippets/.golangci.mustache", output + "/go/.golangci.yml"));
supportingFiles.add(new SupportingFile("snippets/go.mod.mustache", output + "/go/go.mod"));
}

@Override
public void addMustacheLambdas(Map<String, Lambda> lambdas) {
lambdas.put("toEnum", (fragment, writer) -> writer.write(AlgoliaGoGenerator.toEnum(fragment.execute())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ public void addDataToBundle(Map<String, Object> bundle) throws GeneratorExceptio

@Override
public void addMustacheLambdas(Map<String, Lambda> lambdas) {
lambdas.put("javaEnum", (fragment, writer) -> writer.write(AlgoliaJavaGenerator.toEnum(fragment.execute())));
lambdas.put("toEnum", (fragment, writer) -> writer.write(AlgoliaJavaGenerator.toEnum(fragment.execute())));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.algolia.codegen.cts.tests;

import com.algolia.codegen.AlgoliaGoGenerator;
import com.algolia.codegen.AlgoliaSwiftGenerator;
import com.algolia.codegen.exceptions.*;
import com.algolia.codegen.utils.*;
Expand Down Expand Up @@ -60,7 +61,7 @@ public void enhanceParameters(Map<String, Object> parameters, Map<String, Object
IJsonSchemaValidationProperties spec = null;
String paramName = null;
// special case if there is only bodyParam which is not an array
if (operation != null && operation.allParams.size() == 1 && operation.bodyParams.size() == 1 && !operation.bodyParam.isArray) {
if (operation != null && operation.allParams.size() == 1 && operation.bodyParam != null && !operation.bodyParam.isArray) {
spec = operation.bodyParam;
paramName = operation.bodyParam.paramName;
}
Expand All @@ -83,11 +84,28 @@ public void enhanceParameters(Map<String, Object> parameters, Map<String, Object
throw new CTSException("Parameter " + param.getKey() + " not found in the root parameter");
}
}
Map<String, Object> paramWithType = traverseParams(param.getKey(), param.getValue(), specParam, "", 0, false);
parametersWithDataType.add(paramWithType);
parametersWithDataTypeMap.put((String) paramWithType.get("key"), paramWithType);
if (
language.equals("go") &&
specParam != null &&
specParam.isBodyParam &&
operation != null &&
operation.bodyParam != null &&
operation.bodyParam.isModel &&
operation.bodyParam.getVars().size() > 0 &&
AlgoliaGoGenerator.canFlattenBody(operation)
) {
// flatten the body params by skipping one level
flattenBodyParams((Map<String, Object>) param.getValue(), operation, parametersWithDataType);
} else {
Map<String, Object> paramWithType = traverseParams(param.getKey(), param.getValue(), specParam, "", 0, false);
parametersWithDataType.add(paramWithType);
parametersWithDataTypeMap.put((String) paramWithType.get("key"), paramWithType);
}
}
}
} else if (language.equals("go") && parameters != null && operation.bodyParam.getVars().size() > 0) {
// also flatten when the body is the only parameter
flattenBodyParams(parameters, operation, parametersWithDataType);
} else {
Map<String, Object> paramWithType = traverseParams(paramName, parameters, spec, "", 0, false);
parametersWithDataType.add(paramWithType);
Expand All @@ -105,6 +123,22 @@ private String toJSONWithVar(Map<String, Object> parameters) throws JsonProcessi
return Json.mapper().writeValueAsString(parameters).replaceAll("\"\\$var: (.*?)\"", "$1");
}

private void flattenBodyParams(
Map<String, Object> parameters,
CodegenOperation operation,
List<Map<String, Object>> parametersWithDataType
) throws CTSException {
for (String nestedParam : parameters.keySet()) {
for (CodegenProperty prop : operation.bodyParam.getVars()) {
if (prop.baseName.equals(nestedParam)) {
Map<String, Object> paramWithType = traverseParams(prop.baseName, parameters.get(nestedParam), prop, "", 0, false);
parametersWithDataType.add(paramWithType);
break;
}
}
}
}

private Map<String, Object> traverseParams(
String paramName,
Object param,
Expand Down Expand Up @@ -414,15 +448,7 @@ private void handleModel(
}

if (language.equals("swift")) {
// Store ordered params from the spec
var orderedParams = spec.getVars().stream().map(v -> v.baseName).toList();

// Create a map to store the indices of each string in orderedParams
Map<String, Integer> indexMap = IntStream.range(0, orderedParams.size())
.boxed()
.collect(Collectors.toMap(orderedParams::get, i -> i));

values.sort(Comparator.comparing(value -> indexMap.getOrDefault((String) value.get("key"), Integer.MAX_VALUE)));
sortParameters(spec, values);
}

var hasAdditionalProperties = values
Expand Down Expand Up @@ -750,4 +776,14 @@ private boolean couldMatchEnum(Object value, CodegenProperty model) {

return ((List) values).contains(value);
}

private void sortParameters(IJsonSchemaValidationProperties spec, List<Map<String, Object>> parameters) {
// Store ordered params from the spec
var orderedParams = spec.getVars().stream().map(v -> v.baseName).toList();

// Create a map to store the indices of each string in orderedParams
Map<String, Integer> indexMap = IntStream.range(0, orderedParams.size()).boxed().collect(Collectors.toMap(orderedParams::get, i -> i));

parameters.sort(Comparator.comparing(param -> indexMap.getOrDefault((String) param.get("key"), Integer.MAX_VALUE)));
}
}
Loading