Skip to content

[feat][elixir] use ecto schemas and changesets in generated models #21208

New issue

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

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

Already on GitHub? # to your account

Draft
wants to merge 9 commits into
base: master
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class ElixirClientCodegen extends DefaultCodegen {
String supportedElixirVersion = "1.18";
List<String> extraApplications = Arrays.asList(":logger");
List<String> deps = Arrays.asList(
"{:ecto, \"~> 3.12\"}",
"{:tesla, \"~> 1.14\"}",
"{:ex_doc, \"~> 0.37.3\", only: :dev, runtime: false}",
"{:dialyxir, \"~> 1.4\", only: [:dev, :test], runtime: false}");
Expand Down Expand Up @@ -203,6 +204,7 @@ public ElixirClientCodegen() {
typeMapping.put("integer", "integer()");
typeMapping.put("boolean", "boolean()");
typeMapping.put("array", "list()");
typeMapping.put("set", "list()");
typeMapping.put("object", "map()");
typeMapping.put("map", "map()");
typeMapping.put("null", "nil");
Expand Down Expand Up @@ -302,6 +304,28 @@ public void execute(Template.Fragment fragment, Writer writer) throws IOExceptio
writer.write(text.toUpperCase(Locale.ROOT));
}
});
additionalProperties.put("quoteIfString", new Mustache.Lambda() {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
String text = fragment.execute();
if (text != null) {
try {
// Try to parse as a number
Double.parseDouble(text);
// If parsing succeeds, it's a number, so write it as is
writer.write(text);
} catch (NumberFormatException e) {
// Check if it's a boolean
if (text.equals("true") || text.equals("false")) {
writer.write(text);
} else {
// It's not a number or boolean, so it's a string - quote it
writer.write("\"" + text + "\"");
}
}
}
}
});

if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) {
setModuleName((String) additionalProperties.get(CodegenConstants.INVOKER_PACKAGE));
Expand Down Expand Up @@ -337,9 +361,9 @@ public void preprocessOpenAPI(OpenAPI openAPI) {
sourceFolder(),
"request_builder.ex"));

supportingFiles.add(new SupportingFile("deserializer.ex.mustache",
supportingFiles.add(new SupportingFile("ecto_utils.ex.mustache",
sourceFolder(),
"deserializer.ex"));
"ecto_utils.ex"));
}

@Override
Expand Down Expand Up @@ -384,7 +408,65 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
@Override
public CodegenModel fromModel(String name, Schema model) {
CodegenModel cm = super.fromModel(name, model);
return new ExtendedCodegenModel(cm);
ExtendedCodegenModel ecm = new ExtendedCodegenModel(cm);

// Convert all CodegenProperty objects to ExtendedCodegenProperty
List<CodegenProperty> vars = new ArrayList<>(ecm.vars);
ecm.vars.clear();
for (CodegenProperty var : vars) {
ecm.vars.add(new ExtendedCodegenProperty(var));
}

List<CodegenProperty> allVars = new ArrayList<>(ecm.allVars);
ecm.allVars.clear();
for (CodegenProperty var : allVars) {
ecm.allVars.add(new ExtendedCodegenProperty(var));
}

List<CodegenProperty> requiredVars = new ArrayList<>(ecm.requiredVars);
ecm.requiredVars.clear();
for (CodegenProperty var : requiredVars) {
ecm.requiredVars.add(new ExtendedCodegenProperty(var));
}

List<CodegenProperty> optionalVars = new ArrayList<>(ecm.optionalVars);
ecm.optionalVars.clear();
for (CodegenProperty var : optionalVars) {
ecm.optionalVars.add(new ExtendedCodegenProperty(var));
}

List<CodegenProperty> ectoFields = new ArrayList<>(ecm.ectoFields);
ecm.ectoFields.clear();
for (CodegenProperty field : ectoFields) {
ecm.ectoFields.add(new ExtendedCodegenProperty(field));
}

List<CodegenProperty> ectoEmbeds = new ArrayList<>(ecm.ectoEmbeds);
ecm.ectoEmbeds.clear();
for (CodegenProperty embed : ectoEmbeds) {
ecm.ectoEmbeds.add(new ExtendedCodegenProperty(embed));
}

List<CodegenProperty> requiredEctoFields = new ArrayList<>(ecm.requiredEctoFields);
ecm.requiredEctoFields.clear();
for (CodegenProperty field : requiredEctoFields) {
ecm.requiredEctoFields.add(new ExtendedCodegenProperty(field));
}

List<CodegenProperty> ectoEnums = new ArrayList<>(ecm.ectoEnums);
ecm.ectoEnums.clear();
for (CodegenProperty field : ectoEnums) {
ecm.ectoEnums.add(new ExtendedCodegenProperty(field));
}


List<CodegenProperty> ectoMaps = new ArrayList<>(ecm.ectoMaps);
ecm.ectoMaps.clear();
for (CodegenProperty field : ectoMaps) {
ecm.ectoMaps.add(new ExtendedCodegenProperty(field));
}

return ecm;
}

@Override
Expand Down Expand Up @@ -865,6 +947,11 @@ private boolean getRequiresHttpcWorkaround() {

class ExtendedCodegenModel extends CodegenModel {
public boolean hasImports;
public List<CodegenProperty> ectoFields = new ArrayList<>();
public List<CodegenProperty> ectoEmbeds = new ArrayList<>();
public List<CodegenProperty> ectoEnums = new ArrayList<>();
public List<CodegenProperty> ectoMaps = new ArrayList<>();
public List<CodegenProperty> requiredEctoFields = new ArrayList<>();

public ExtendedCodegenModel(CodegenModel cm) {
super();
Expand Down Expand Up @@ -918,15 +1005,196 @@ public ExtendedCodegenModel(CodegenModel cm) {
this.additionalPropertiesType = cm.additionalPropertiesType;

this.hasImports = !this.imports.isEmpty();

for (CodegenProperty var : this.vars) {
if (var.isPrimitiveType || var.isMap || var.isEnum || var.isEnumRef) {
this.ectoFields.add(var);
if (var.required) {
this.requiredEctoFields.add(var);
}
if (var.isEnum || var.isEnumRef) {
this.ectoEnums.add(var);
}
if (var.isMap && !var.isFreeFormObject && var.additionalProperties != null && var.additionalProperties.isModel) {
this.ectoMaps.add(var);
}
} else {
this.ectoEmbeds.add(var);
}
}
}
}

class ExtendedCodegenProperty extends CodegenProperty {
public String enumBaseType;
public String mapValueType;
public boolean isMapWithSchema;

public boolean hasComplexVars() {
for (CodegenProperty p : vars) {
if (!p.isPrimitiveType) {
return true;
public ExtendedCodegenProperty(CodegenProperty cp) {
super();

// Copy all fields of CodegenProperty
this.openApiType = cp.openApiType;
this.baseName = cp.baseName;
this.complexType = cp.complexType;
this.getter = cp.getter;
this.setter = cp.setter;
this.description = cp.description;
this.dataType = cp.dataType;
this.datatypeWithEnum = cp.datatypeWithEnum;
this.dataFormat = cp.dataFormat;
this.name = cp.name;
this.min = cp.min;
this.max = cp.max;
this.defaultValue = cp.defaultValue;
this.defaultValueWithParam = cp.defaultValueWithParam;
this.baseType = cp.baseType;
this.containerType = cp.containerType;
this.title = cp.title;
this.unescapedDescription = cp.unescapedDescription;
this.maxLength = cp.maxLength;
this.minLength = cp.minLength;
this.pattern = cp.pattern;
this.example = cp.example;
this.jsonSchema = cp.jsonSchema;
this.minimum = cp.minimum;
this.maximum = cp.maximum;
this.exclusiveMinimum = cp.exclusiveMinimum;
this.exclusiveMaximum = cp.exclusiveMaximum;
this.required = cp.required;
this.deprecated = cp.deprecated;
this.isPrimitiveType = cp.isPrimitiveType;
this.isModel = cp.isModel;
this.isContainer = cp.isContainer;
this.isString = cp.isString;
this.isNumeric = cp.isNumeric;
this.isInteger = cp.isInteger;
this.isLong = cp.isLong;
this.isNumber = cp.isNumber;
this.isFloat = cp.isFloat;
this.isDouble = cp.isDouble;
this.isByteArray = cp.isByteArray;
this.isBinary = cp.isBinary;
this.isFile = cp.isFile;
this.isBoolean = cp.isBoolean;
this.isDate = cp.isDate;
this.isDateTime = cp.isDateTime;
this.isUuid = cp.isUuid;
this.isEmail = cp.isEmail;
this.isModel = cp.isModel;
this.isNull = cp.isNull;
this.isArray = cp.isArray;
this.isMap = cp.isMap;
this.isEnum = cp.isEnum;
this.isEnumRef = cp.isEnumRef;
this.isReadOnly = cp.isReadOnly;
this.isWriteOnly = cp.isWriteOnly;
this.isNullable = cp.isNullable;
this._enum = cp._enum;
this.allowableValues = cp.allowableValues;
this.items = cp.items;
this.additionalProperties = cp.additionalProperties;
this.vars = cp.vars;
this.requiredVars = cp.requiredVars;
this.vendorExtensions = cp.vendorExtensions;

// For enum references, determine the base type from the enum values
if (cp.isEnumRef && cp.allowableValues != null && cp.allowableValues.get("values") != null) {
List<Object> values = (List<Object>) cp.allowableValues.get("values");
if (!values.isEmpty()) {
Object firstValue = values.get(0);
if (firstValue instanceof String) {
this.enumBaseType = "String.t";
} else if (firstValue instanceof Integer || firstValue instanceof Long) {
this.enumBaseType = "integer()";
} else if (firstValue instanceof Float || firstValue instanceof Double) {
this.enumBaseType = "float()";
} else if (firstValue instanceof Boolean) {
this.enumBaseType = "boolean()";
} else {
// Default to string for unknown types
this.enumBaseType = "String.t";
}
} else {
// No values, default to string
this.enumBaseType = "String.t";
}
}
// For map properties, extract the model name from additionalProperties
if (cp.isMap && cp.additionalProperties != null) {
// Check if the map has a schema (model) or is a free-form map
if (cp.additionalProperties.isModel) {
// Extract just the model name without the full type declaration
this.mapValueType = cp.additionalProperties.complexType;
this.isMapWithSchema = true;
} else {
this.isMapWithSchema = false;
}
}
return false;
}

public String ectoType() {
String ectoType = ectoType(this);
if (":any".equals(ectoType)) {
return ectoType + ", virtual: true";
}
return ectoType;
}

private String ectoType(CodegenProperty property) {
if (property == null) {
return ":any";
}

if (property.isEnumRef) {
List<Object> values = (List<Object>) property.allowableValues.get("values");
if (!values.isEmpty()) {
Object firstValue = values.get(0);
if (firstValue instanceof String) {
return ":string";
} else if (firstValue instanceof Integer || firstValue instanceof Long) {
return ":integer";
} else if (firstValue instanceof Float || firstValue instanceof Double) {
return ":float";
} else if (firstValue instanceof Boolean) {
return ":boolean";
} else {
// Default to string for unknown types
return ":string";
}
} else {
// No values, default to string
return ":string";
}
}

String baseType = property.baseType;
switch (baseType) {
case "integer()":
return ":integer";
case "float()":
return ":float";
case "number()":
return ":float";
case "boolean()":
return ":boolean";
case "String.t":
return ":string";
case "Date.t":
return ":date";
case "DateTime.t":
return ":utc_datetime";
case "binary()":
return ":binary";
case "list()":
return "{:array, " + ectoType(property.items) + "}";
case "map()":
return "{:map, " + ectoType(property.items) + "}";
case "nil":
return ":any";
default:
return ":any";
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ defmodule {{moduleName}}.Connection do
[
{Tesla.Middleware.BaseUrl, base_url},
{Tesla.Middleware.Headers, [{"user-agent", user_agent}]},
{Tesla.Middleware.EncodeJson, engine: json_engine}
{Tesla.Middleware.JSON, engine: json_engine}
| middleware
]
end
Expand Down
Loading
Loading