diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java new file mode 100644 index 00000000000..f08bbf604b1 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ElmClientCodegen.java @@ -0,0 +1,524 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.languages.features.BeanValidationFeatures; +import org.openapitools.codegen.languages.features.JbossFeature; +import org.openapitools.codegen.languages.features.SwaggerFeatures; +import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.parameters.*; +import io.swagger.v3.oas.models.responses.*; + +import java.io.File; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public class ElmClientCodegen extends DefaultCodegen implements CodegenConfig { + private static final String X_ENCODER = "x-encoder"; + private static final String X_DECODER = "x-decoder"; + private static final String X_DISCRIMINATOR_TYPE = "x-discriminator-value"; + private static final String X_UNION_TYPE = "x-union-type"; + + private Set customPrimitives = new HashSet(); + + protected String packageName = "swagger"; + protected String packageVersion = "1.0.0"; + + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "elm"; + } + + public String getHelp() { + return "Generates a Elm client library (beta)."; + } + + public ElmClientCodegen() { + super(); + outputFolder = "generated-code/elm"; + modelTemplateFiles.put("model.mustache", ".elm"); + apiTemplateFiles.put("api.mustache", ".elm"); + templateDir = "elm"; + + supportsInheritance = true; + + reservedWords = new HashSet<>( + Arrays.asList( + "if", "then", "else", + "case", "of", + "let", "in", + "type", + "module", "where", + "import", "exposing", + "as", + "port") + ); + + defaultIncludes = new HashSet<>( + Arrays.asList( + "List") + ); + + languageSpecificPrimitives = new HashSet<>( + Arrays.asList( + "Bool", + "Dict", + "Float", + "Int", + "String") + ); + + customPrimitives = new HashSet<>( + Arrays.asList( + "Byte", + "DateOnly", + "DateTime") + ); + + instantiationTypes.clear(); + instantiationTypes.put("array", "List"); + + typeMapping.clear(); + typeMapping.put("integer", "Int"); + typeMapping.put("long", "Int"); + typeMapping.put("number", "Float"); + typeMapping.put("float", "Float"); + typeMapping.put("double", "Float"); + typeMapping.put("boolean", "Bool"); + typeMapping.put("string", "String"); + typeMapping.put("array", "List"); + typeMapping.put("date", "DateOnly"); + typeMapping.put("DateTime", "DateTime"); + typeMapping.put("password", "String"); + typeMapping.put("file", "String"); + typeMapping.put("ByteArray", "Byte"); + typeMapping.put("binary", "String"); + + importMapping.clear(); + + cliOptions.clear(); + + supportingFiles.add(new SupportingFile("Byte.mustache", "src", "Byte.elm")); + supportingFiles.add(new SupportingFile("DateOnly.mustache", "src", "DateOnly.elm")); + supportingFiles.add(new SupportingFile("DateTime.mustache", "src", "DateTime.elm")); + supportingFiles.add(new SupportingFile("Main.mustache", "src", "Main.elm")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("elm-package.mustache", "", "elm-package.json")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + @Override + public String escapeQuotationMark(String input) { + return input.replace("\"", ""); + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return "Default"; + } + return initialCaps(name); + } + + @Override + public String toModelName(String name) { + return camelize(name); + } + + @Override + public String toModelFilename(String name) { + return toModelName(name); + } + + @Override + public String toEnumName(CodegenProperty property) { + return toModelName(property.name); + } + + @Override + public String toVarName(String name) { + final String varName = camelize(name, true); + return isReservedWord(varName) ? escapeReservedWord(name) : varName; + } + + @Override + public String toEnumVarName(String value, String datatype) { + final String camelized = camelize(value.replace(" ", "_").replace("(", "_").replace(")", "")); // TODO FIXME escape properly + if (!Character.isUpperCase(camelized.charAt(0))) { + return "N" + camelized; + } + return camelized; + } + + @Override + public String toInstantiationType(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + String inner = getSchemaType(ap.getItems()); + return instantiationTypes.get("array") + " " + inner; + } else { + return null; + } + } + + @Override + public String escapeReservedWord(String name) { + return name + "_"; + } + + @Override + public String apiFileFolder() { + return outputFolder + "/src/Request/" + apiPackage().replace('.', File.separatorChar); + } + + @Override + public String modelFileFolder() { + return outputFolder + "/src/Data/" + modelPackage().replace('.', File.separatorChar); + } + + @Override + public CodegenModel fromModel(String name, Schema schema, Map allDefinitions) { + CodegenModel m = super.fromModel(name, schema, allDefinitions); + + if (schema instanceof ArraySchema) { + ArraySchema am = (ArraySchema) schema; + CodegenProperty codegenProperty = fromProperty(name, (Schema) am.getItems()); + m.vendorExtensions.putAll(codegenProperty.vendorExtensions); + } + + return m; + } + + @SuppressWarnings({"static-method", "unchecked"}) + public Map postProcessAllModels(Map objs) { + // Index all CodegenModels by model name. + Map allModels = new HashMap<>(); + for (Map.Entry entry : objs.entrySet()) { + String modelName = toModelName(entry.getKey()); + Map inner = (Map) entry.getValue(); + List> models = (List>) inner.get("models"); + for (Map mo : models) { + CodegenModel cm = (CodegenModel) mo.get("model"); + allModels.put(modelName, cm); + } + } + // Let parent know about all its children + for (CodegenModel cm : allModels.values()) { + CodegenModel parent = allModels.get(cm.parent); + + if (parent != null) { + if (parent.children == null) { + parent.children = new ArrayList<>(); + parent.hasChildren = true; + } + parent.children.add(cm); + Collections.sort(parent.children, new Comparator() { + @Override + public int compare(CodegenModel cm1, CodegenModel cm2) { + return Collator.getInstance().compare(cm1.classname, cm2.classname); + } + }); + } + } + for (Map.Entry entry : objs.entrySet()) { + Map inner = (Map) entry.getValue(); + List> models = (List>) inner.get("models"); + for (Map mo : models) { + CodegenModel cm = (CodegenModel) mo.get("model"); + if (cm.isEnum) { + this.addEncoderAndDecoder(cm.vendorExtensions, cm.classname, false); + cm.vendorExtensions.put(X_UNION_TYPE, cm.classname); + } else if (cm.isAlias) { + this.addEncoderAndDecoder(cm.vendorExtensions, cm.dataType, true); + } + + List elmImports = new ArrayList<>(); + for (CodegenProperty property : cm.allVars) { + if (property.complexType != null) { + elmImports.add(createPropertyImport(property)); + } + } + if (cm.isArrayModel) { + if (cm.arrayModelType != null) { + // add type imports + final ElmImport elmImport = new ElmImport(); + final String modulePrefix = customPrimitives.contains(cm.arrayModelType) ? "" : "Data."; + elmImport.moduleName = modulePrefix + cm.arrayModelType; + elmImport.exposures = new TreeSet<>(); + elmImport.exposures.add(cm.arrayModelType); + elmImport.exposures.add(camelize(cm.arrayModelType, true) + "Decoder"); + elmImport.exposures.add(camelize(cm.arrayModelType, true) + "Encoder"); + elmImport.hasExposures = true; + elmImports.add(elmImport); + } + } + if (cm.discriminator != null) { + for (CodegenModel child : cm.children) { + // add child imports + final ElmImport elmImport = new ElmImport(); + final String modulePrefix = customPrimitives.contains(child.classname) ? "" : "Data."; + elmImport.moduleName = modulePrefix + child.classname; + elmImport.exposures = new TreeSet<>(); + elmImport.exposures.add(child.classname); + elmImport.exposures.add(child.classVarName + "Decoder"); + elmImport.exposures.add(child.classVarName + "Encoder"); + elmImport.hasExposures = true; + elmImports.add(elmImport); + + // set discriminator value to all children (recursively) + this.setDiscriminatorValue(child, cm.getDiscriminatorName(), this.getDiscriminatorValue(child)); + + // add all non-discriminator vars + int index = 0; + for (CodegenProperty property : cm.vars) { + if (!cm.discriminator.equals(property.baseName)) { + child.vars.add(index++, property); + } + } + } + } + inner.put("elmImports", elmImports); + } + } + return objs; + } + + private void setDiscriminatorValue(CodegenModel model, String baseName, String value) { + for (CodegenProperty prop : model.vars) { + if (prop.baseName.equals(baseName)) { + prop.discriminatorValue = value; + } + } + for (CodegenProperty prop : model.allVars) { + if (prop.baseName.equals(baseName)) { + prop.discriminatorValue = value; + } + } + if (model.children != null) { + final boolean newDiscriminator = model.discriminator != null; + for (CodegenModel child : model.children) { + this.setDiscriminatorValue(child, baseName, newDiscriminator ? value : this.getDiscriminatorValue(child)); + } + } + } + + private String getDiscriminatorValue(CodegenModel model) { + return model.vendorExtensions.containsKey(X_DISCRIMINATOR_TYPE) ? + (String) model.vendorExtensions.get(X_DISCRIMINATOR_TYPE) : model.classname; + } + + private ElmImport createPropertyImport(final CodegenProperty property) { + final ElmImport elmImport = new ElmImport(); + final String modulePrefix = customPrimitives.contains(property.complexType) ? "" : "Data."; + elmImport.moduleName = modulePrefix + property.complexType; + elmImport.exposures = new TreeSet<>(); + elmImport.exposures.add(property.complexType); + if (property.vendorExtensions.containsKey(X_DECODER)) { + elmImport.exposures.add((String) property.vendorExtensions.get(X_DECODER)); + } + if (property.vendorExtensions.containsKey(X_ENCODER)) { + elmImport.exposures.add((String) property.vendorExtensions.get(X_ENCODER)); + } + elmImport.hasExposures = true; + return elmImport; + } + + @Override + public Map postProcessModels(Map objs) { + return postProcessModelsEnum(objs); + } + + @Override + @SuppressWarnings({"static-method", "unchecked"}) + public Map postProcessOperations(Map operations) { + Map objs = (Map) operations.get("operations"); + List ops = (List) objs.get("operation"); + + Map> dependencies = new HashMap<>(); + + for (CodegenOperation op : ops) { + String path = op.path; + for (CodegenParameter param : op.pathParams) { + final String var = param.isString ? param.paramName : "toString " + param.paramName; + path = path.replace("{" + param.paramName + "}", "\" ++ " + var + " ++ \""); + } + op.path = ("\"" + path + "\"").replaceAll(" \\+\\+ \"\"", ""); + + if (op.bodyParam != null) { + final String encoder = (String) op.bodyParam.vendorExtensions.get(X_ENCODER); + if (encoder != null) { + if (!dependencies.containsKey(op.bodyParam.dataType)) { + dependencies.put(op.bodyParam.dataType, new TreeSet()); + } + dependencies.get(op.bodyParam.dataType).add(encoder); + } + } + for (CodegenResponse resp : op.responses) { + final String decoder = (String) resp.vendorExtensions.get(X_DECODER); + if (decoder != null) { + if (!dependencies.containsKey(resp.dataType)) { + dependencies.put(resp.dataType, new TreeSet()); + } + dependencies.get(resp.dataType).add(decoder); + } + } + } + + List elmImports = new ArrayList<>(); + for (Map.Entry> entry : dependencies.entrySet()) { + final ElmImport elmImport = new ElmImport(); + final String key = entry.getKey(); + elmImport.moduleName = "Data." + key; + elmImport.exposures = entry.getValue(); + elmImport.exposures.add(key); + elmImport.hasExposures = true; + elmImports.add(elmImport); + } + operations.put("elmImports", elmImports); + + return operations; + } + + @Override + public String toDefaultValue(Schema p) { + if (p instanceof StringSchema) { + StringSchema sp = (StringSchema) p; + if (sp.getDefault() != null) { + return toOptionalValue("\"" + sp.getDefault().toString() + "\""); + } + return toOptionalValue(null); + } else if (p instanceof BooleanSchema) { + BooleanSchema bp = (BooleanSchema) p; + if (bp.getDefault() != null) { + return toOptionalValue(Boolean.valueOf(bp.getDefault().toString()) ? "True" : "False"); + } + return toOptionalValue(null); + } else if (p instanceof DateSchema) { + return toOptionalValue(null); + } else if (p instanceof DateTimeSchema) { + return toOptionalValue(null); + } else if (p instanceof NumberSchema) { + NumberSchema dp = (NumberSchema) p; + if (dp.getDefault() != null) { + return toOptionalValue(dp.getDefault().toString()); + } + return toOptionalValue(null); + } else if (p instanceof IntegerSchema) { + IntegerSchema ip = (IntegerSchema) p; + if (ip.getDefault() != null) { + return toOptionalValue(ip.getDefault().toString()); + } + return toOptionalValue(null); + } else { + return toOptionalValue(null); + } + } + + private String toOptionalValue(String value) { + if (value == null) { + return "Nothing"; + } + return "(Just " + value + ")"; + } + + @Override + public String getSchemaType(Schema p) { + String swaggerType = super.getSchemaType(p); + String type; + if (typeMapping.containsKey(swaggerType)) { + type = typeMapping.get(swaggerType); + if (languageSpecificPrimitives.contains(type)) { + return type; + } + } else + type = swaggerType; + return toModelName(type); + } + + @Override + public String getTypeDeclaration(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + return getTypeDeclaration(inner); + } else if (p instanceof MapSchema) { + MapSchema mp = (MapSchema) p; + Schema inner = (Schema) mp.getAdditionalProperties(); + return getTypeDeclaration(inner); + } + return super.getTypeDeclaration(p); + } + + @Override + public CodegenProperty fromProperty(String name, Schema p) { + final CodegenProperty property = super.fromProperty(name, p); + + final String dataType = property.isEnum ? property.baseName : property.datatype; + addEncoderAndDecoder(property.vendorExtensions, dataType, property.isPrimitiveType && !property.isEnum); + if (property.isEnum) { + property.vendorExtensions.put(X_UNION_TYPE, property.datatypeWithEnum); + } + + return property; + } + + @Override + public CodegenResponse fromResponse(String responseCode, ApiResponse resp) { + final CodegenResponse response = super.fromResponse(responseCode, resp); + if (response.dataType != null) { + addEncoderAndDecoder(response.vendorExtensions, response.dataType, response.primitiveType); + } + return response; + } + + @Override + public CodegenParameter fromParameter(Parameter param, Set imports) { + final CodegenParameter parameter = super.fromParameter(param, imports); + addEncoderAndDecoder(parameter.vendorExtensions, parameter.dataType, parameter.isPrimitiveType); + return parameter; + } + + private void addEncoderAndDecoder(Map vendorExtensions, String dataType, Boolean isPrimitiveType) { + final String baseName = camelize(dataType, true); + String encoderName; + String decoderName; + if (isPrimitiveType) { + encoderName = "Encode." + baseName; + decoderName = "Decode." + baseName; + } else { + encoderName = baseName + "Encoder"; + decoderName = baseName + "Decoder"; + } + if (!vendorExtensions.containsKey(X_ENCODER)) { + vendorExtensions.put(X_ENCODER, encoderName); + } + if (!vendorExtensions.containsKey(X_DECODER)) { + vendorExtensions.put(X_DECODER, decoderName); + } + } + + private static class ElmImport { + public String moduleName; + public String as; + public Set exposures; + public Boolean hasExposures; + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FlashClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/FlashClientCodegen.java old mode 100755 new mode 100644 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java old mode 100755 new mode 100644 diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 08082ce5d1f..cb1d27bc17d 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -19,6 +19,7 @@ org.openapitools.codegen.languages.CSharpNancyFXServerCodegen org.openapitools.codegen.languages.DartClientCodegen org.openapitools.codegen.languages.EiffelClientCodegen org.openapitools.codegen.languages.ElixirClientCodegen +org.openapitools.codegen.languages.ElmClientCodegen org.openapitools.codegen.languages.ErlangClientCodegen org.openapitools.codegen.languages.ErlangServerCodegen org.openapitools.codegen.languages.FlashClientCodegen