mirror of
https://github.com/OpenAPITools/openapi-generator.git
synced 2025-12-21 05:47:09 +00:00
[Elixir] Improve Elixir client (#6550)
* Fix dependencies and generate model classes * Better elixir client generation. Responses are parsed and serialized by Poison into the model structs. Use shared helper functions to generate the request. Extract client connection configuration from api calls. Elixir client can sanitize the operationId Correctly output the model variables. Fix typos Fix path replacement when there are multiple replacements Cannot separate globally shared parameters from operations Error handling for the tesla response update templates Can generate clients that compile Can make requests - parse optional params, build query Add oauth to connection. Fix connection directory Add basic auth helper for creating a connection Fix map types. Fix guard clauses for creaing connections Add licenceInfo template. Parse config for moduleName via standard invokerPackage option Can provide and inject a license header into all source files fix location of connection.ex Move shared code into reusable modules Elixir filenames should be underscored Fix visibility of helper functions Parse the packageName from config options Handle date and datetime fields with DateTime.from_iso8601 Fix indentation Update documentation, add typespecs Generate a standard elixir .gitignore typespec is calculated recursively in java Use the JSON middleware and using Poison.Decoder.decode on already parsed structs move decoded struct into java Fix handling of non-json responses Switch basic auth to use the provided Tesla.Middleware.BasicAuth Update README template to include the appDescription Update sample elixir client remove junk client models that don't belong with petstore Only implement Poison.Decoder protocol if needed Update samples with skipped Poison.Deocder impl * Handle multipart file uploads Handle building form params in the body Files are handled as strings for input * Requests with no defined return type will return the Tesla.Env response * Run the bin/elixir-petstore.sh
This commit is contained in:
@@ -6,6 +6,10 @@ import io.swagger.codegen.*;
|
||||
import io.swagger.models.properties.ArrayProperty;
|
||||
import io.swagger.models.properties.MapProperty;
|
||||
import io.swagger.models.properties.Property;
|
||||
import io.swagger.models.Info;
|
||||
import io.swagger.models.Model;
|
||||
import io.swagger.models.Swagger;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
@@ -14,14 +18,17 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig {
|
||||
// source folder where to write the files
|
||||
protected String sourceFolder = "lib";
|
||||
protected String apiVersion = "1.0.0";
|
||||
protected String moduleName;
|
||||
protected static final String defaultModuleName = "Swagger.Client";
|
||||
|
||||
// This is the name of elixir project name;
|
||||
protected static final String defaultPackageName = "swagger_client";
|
||||
|
||||
String supportedElixirVersion = "1.4";
|
||||
List<String> extraApplications = Arrays.asList(":logger");
|
||||
List<String> deps = Arrays.asList(
|
||||
"{:tesla, \"~> 0.5.0\"}",
|
||||
"{:tesla, \"~> 0.8\"}",
|
||||
"{:poison, \">= 1.0.0\"}"
|
||||
);
|
||||
|
||||
@@ -32,7 +39,7 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
// set the output folder here
|
||||
outputFolder = "generated-code/elixir";
|
||||
|
||||
/**
|
||||
/*
|
||||
* Models. You can write model files using the modelTemplateFiles map.
|
||||
* if you want to create one template for file, you can do so here.
|
||||
* for multiple files for model, just put another entry in the `modelTemplateFiles` with
|
||||
@@ -62,8 +69,14 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
*/
|
||||
reservedWords = new HashSet<String>(
|
||||
Arrays.asList(
|
||||
"sample1", // replace with static values
|
||||
"sample2")
|
||||
"nil",
|
||||
"true",
|
||||
"false",
|
||||
"__MODULE__",
|
||||
"__FILE__",
|
||||
"__DIR__",
|
||||
"__ENV__",
|
||||
"__CALLER__")
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -93,6 +106,10 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
"test",
|
||||
"test_helper.exs")
|
||||
);
|
||||
supportingFiles.add(new SupportingFile("gitignore.mustache",
|
||||
"",
|
||||
".gitignore")
|
||||
);
|
||||
|
||||
/**
|
||||
* Language Specific Primitives. These types will not trigger imports by
|
||||
@@ -100,9 +117,43 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
*/
|
||||
languageSpecificPrimitives = new HashSet<String>(
|
||||
Arrays.asList(
|
||||
"Type1", // replace these with your types
|
||||
"Type2")
|
||||
"Integer",
|
||||
"Float",
|
||||
"Boolean",
|
||||
"String",
|
||||
"List",
|
||||
"Atom",
|
||||
"Map",
|
||||
"Tuple",
|
||||
"PID",
|
||||
"DateTime"
|
||||
)
|
||||
);
|
||||
|
||||
// ref: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types
|
||||
typeMapping = new HashMap<String, String>();
|
||||
typeMapping.put("integer", "Integer");
|
||||
typeMapping.put("long", "Integer");
|
||||
typeMapping.put("number", "Float");
|
||||
typeMapping.put("float", "Float");
|
||||
typeMapping.put("double", "Float");
|
||||
typeMapping.put("string", "String");
|
||||
typeMapping.put("byte", "Integer");
|
||||
typeMapping.put("boolean", "Boolean");
|
||||
typeMapping.put("Date", "DateTime");
|
||||
typeMapping.put("DateTime", "DateTime");
|
||||
typeMapping.put("file", "String");
|
||||
typeMapping.put("map", "Map");
|
||||
typeMapping.put("array", "List");
|
||||
typeMapping.put("list", "List");
|
||||
// typeMapping.put("object", "Map");
|
||||
typeMapping.put("binary", "String");
|
||||
typeMapping.put("ByteArray", "String");
|
||||
typeMapping.put("UUID", "String");
|
||||
|
||||
cliOptions.add(new CliOption(CodegenConstants.INVOKER_PACKAGE, "The main namespace to use for all classes. e.g. Yay.Pets"));
|
||||
cliOptions.add(new CliOption("licenseHeader", "The license header to prepend to the top of all source files."));
|
||||
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Elixir package name (convention: lowercase)."));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +204,41 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
writer.write(modulized(fragment.execute()));
|
||||
}
|
||||
});
|
||||
|
||||
if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) {
|
||||
setModuleName((String) additionalProperties.get(CodegenConstants.INVOKER_PACKAGE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preprocessSwagger(Swagger swagger) {
|
||||
Info info = swagger.getInfo();
|
||||
if (moduleName == null) {
|
||||
if (info.getTitle() != null) {
|
||||
// default to the appName (from title field)
|
||||
setModuleName(modulized(escapeText(info.getTitle())));
|
||||
} else {
|
||||
setModuleName(defaultModuleName);
|
||||
}
|
||||
}
|
||||
additionalProperties.put("moduleName", moduleName);
|
||||
|
||||
if (!additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
|
||||
additionalProperties.put(CodegenConstants.PACKAGE_NAME, underscored(moduleName));
|
||||
}
|
||||
|
||||
supportingFiles.add(new SupportingFile("connection.ex.mustache",
|
||||
sourceFolder(),
|
||||
"connection.ex"));
|
||||
|
||||
supportingFiles.add(new SupportingFile("request_builder.ex.mustache",
|
||||
sourceFolder(),
|
||||
"request_builder.ex"));
|
||||
|
||||
|
||||
supportingFiles.add(new SupportingFile("deserializer.ex.mustache",
|
||||
sourceFolder(),
|
||||
"deserializer.ex"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,14 +246,14 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
Map<String, Object> operations = (Map<String, Object>) super.postProcessOperations(objs).get("operations");
|
||||
List<CodegenOperation> os = (List<CodegenOperation>) operations.get("operation");
|
||||
List<ExtendedCodegenOperation> newOs = new ArrayList<ExtendedCodegenOperation>();
|
||||
Pattern pattern = Pattern.compile("(.*)\\{([^\\}]+)\\}(.*)");
|
||||
Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}([^\\{]*)");
|
||||
for (CodegenOperation o : os) {
|
||||
ArrayList<String> pathTemplateNames = new ArrayList<String>();
|
||||
Matcher matcher = pattern.matcher(o.path);
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String pathTemplateName = matcher.group(2);
|
||||
matcher.appendReplacement(buffer, "$1" + "#{" + underscore(pathTemplateName) + "}" + "$3");
|
||||
String pathTemplateName = matcher.group(1);
|
||||
matcher.appendReplacement(buffer, "#{" + underscore(pathTemplateName) + "}" + "$2");
|
||||
pathTemplateNames.add(pathTemplateName);
|
||||
}
|
||||
ExtendedCodegenOperation eco = new ExtendedCodegenOperation(o);
|
||||
@@ -177,12 +263,29 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
eco.setReplacedPathName(buffer.toString());
|
||||
}
|
||||
eco.setPathTemplateNames(pathTemplateNames);
|
||||
|
||||
// detect multipart form types
|
||||
if (eco.hasConsumes == Boolean.TRUE) {
|
||||
Map<String, String> firstType = eco.consumes.get(0);
|
||||
if (firstType != null) {
|
||||
if ("multipart/form-data".equals(firstType.get("mediaType"))) {
|
||||
eco.isMultipart = Boolean.TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newOs.add(eco);
|
||||
}
|
||||
operations.put("operation", newOs);
|
||||
return objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodegenModel fromModel(String name, Model model, Map<String, Model> allDefinitions) {
|
||||
CodegenModel cm = super.fromModel(name, model, allDefinitions);
|
||||
return new ExtendedCodegenModel(cm);
|
||||
}
|
||||
|
||||
// We should use String.join if we can use Java8
|
||||
String join(CharSequence charSequence, Iterable<String> iterable) {
|
||||
StringBuilder buf = new StringBuilder();
|
||||
@@ -222,12 +325,20 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return "_" + name; // add an underscore to the name
|
||||
}
|
||||
|
||||
private String sourceFolder() {
|
||||
ArrayList<String> underscoredWords = new ArrayList<String>();
|
||||
for (String word : moduleName.split("\\.")) {
|
||||
underscoredWords.add(underscore(word));
|
||||
}
|
||||
return "lib/" + join("/", underscoredWords);
|
||||
}
|
||||
|
||||
/**
|
||||
* Location to write model files. You can use the modelPackage() as defined when the class is
|
||||
* instantiated
|
||||
*/
|
||||
public String modelFileFolder() {
|
||||
return outputFolder + "/" + sourceFolder + "/" + underscored((String) additionalProperties.get("appName")) + "/" + "model";
|
||||
return outputFolder + "/" + sourceFolder() + "/" + "model";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,7 +347,7 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
*/
|
||||
@Override
|
||||
public String apiFileFolder() {
|
||||
return outputFolder + "/" + sourceFolder + "/" + underscored((String) additionalProperties.get("appName")) + "/" + "api";
|
||||
return outputFolder + "/" + sourceFolder() + "/" + "api";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -249,12 +360,22 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
|
||||
@Override
|
||||
public String toApiFilename(String name) {
|
||||
return snakeCase(name);
|
||||
return underscore(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toModelFilename(String name) {
|
||||
return snakeCase(name);
|
||||
return underscore(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toOperationId(String operationId) {
|
||||
// throw exception if method name is empty (should not occur as an auto-generated method name will be used)
|
||||
if (StringUtils.isEmpty(operationId)) {
|
||||
throw new RuntimeException("Empty method name (operationId) not allowed");
|
||||
}
|
||||
|
||||
return camelize(sanitizeName(operationId));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -374,6 +495,188 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
public void setReplacedPathName(String replacedPathName) {
|
||||
this.replacedPathName = replacedPathName;
|
||||
}
|
||||
|
||||
public String typespec() {
|
||||
StringBuilder sb = new StringBuilder("@spec ");
|
||||
sb.append(underscore(operationId));
|
||||
sb.append("(Tesla.Env.client, ");
|
||||
|
||||
for (CodegenParameter param : allParams) {
|
||||
if (param.required) {
|
||||
buildTypespec(param, sb);
|
||||
sb.append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("keyword()) :: {:ok, ");
|
||||
if (returnBaseType == null) {
|
||||
sb.append("nil");
|
||||
} else if (returnSimpleType) {
|
||||
if (!returnTypeIsPrimitive) {
|
||||
sb.append(moduleName);
|
||||
sb.append(".Model.");
|
||||
}
|
||||
sb.append(returnBaseType);
|
||||
sb.append(".t");
|
||||
} else if (returnContainer == null) {
|
||||
sb.append(returnBaseType);
|
||||
sb.append(".t");
|
||||
} else {
|
||||
if (returnContainer.equals("array")) {
|
||||
sb.append("list(");
|
||||
if (!returnTypeIsPrimitive) {
|
||||
sb.append(moduleName);
|
||||
sb.append(".Model.");
|
||||
}
|
||||
sb.append(returnBaseType);
|
||||
sb.append(".t)");
|
||||
} else if (returnContainer.equals("map")) {
|
||||
sb.append("map()");
|
||||
}
|
||||
}
|
||||
sb.append("} | {:error, Tesla.Env.t}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void buildTypespec(CodegenParameter param, StringBuilder sb) {
|
||||
if (param.dataType == null) {
|
||||
sb.append("nil");
|
||||
} else if (param.isListContainer) {
|
||||
// list(<subtype>)
|
||||
sb.append("list(");
|
||||
if (param.isBodyParam) {
|
||||
buildTypespec(param.items.items, sb);
|
||||
} else {
|
||||
buildTypespec(param.items, sb);
|
||||
}
|
||||
sb.append(")");
|
||||
} else if (param.isMapContainer) {
|
||||
// %{optional(String.t) => <subtype>}
|
||||
sb.append("%{optional(String.t) => ");
|
||||
buildTypespec(param.items, sb);
|
||||
sb.append("}");
|
||||
} else if (param.isPrimitiveType) {
|
||||
// <type>.t
|
||||
sb.append(param.dataType);
|
||||
sb.append(".t");
|
||||
} else if (param.isFile) {
|
||||
sb.append("String.t");
|
||||
} else {
|
||||
// <module>.Model.<type>.t
|
||||
sb.append(moduleName);
|
||||
sb.append(".Model.");
|
||||
sb.append(param.dataType);
|
||||
sb.append(".t");
|
||||
}
|
||||
}
|
||||
private void buildTypespec(CodegenProperty property, StringBuilder sb) {
|
||||
if (property.isListContainer) {
|
||||
sb.append("list(");
|
||||
buildTypespec(property.items, sb);
|
||||
sb.append(")");
|
||||
} else if (property.isMapContainer) {
|
||||
sb.append("%{optional(String.t) => ");
|
||||
buildTypespec(property.items, sb);
|
||||
sb.append("}");
|
||||
} else if (property.isPrimitiveType) {
|
||||
sb.append(property.baseType);
|
||||
sb.append(".t");
|
||||
} else {
|
||||
sb.append(moduleName);
|
||||
sb.append(".Model.");
|
||||
sb.append(property.baseType);
|
||||
sb.append(".t");
|
||||
}
|
||||
}
|
||||
|
||||
public String decodedStruct() {
|
||||
// Let Poison decode the entire response into a generic blob
|
||||
if (isMapContainer) {
|
||||
return "";
|
||||
}
|
||||
// Primitive return type, don't even try to decode
|
||||
if (returnBaseType == null || (returnSimpleType && returnTypeIsPrimitive)) {
|
||||
return "false";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (isListContainer) {
|
||||
sb.append("[");
|
||||
}
|
||||
sb.append("%");
|
||||
sb.append(moduleName);
|
||||
sb.append(".Model.");
|
||||
sb.append(returnBaseType);
|
||||
sb.append("{}");
|
||||
if (isListContainer) {
|
||||
sb.append("]");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class ExtendedCodegenModel extends CodegenModel {
|
||||
public boolean hasImports;
|
||||
public ExtendedCodegenModel(CodegenModel cm) {
|
||||
super();
|
||||
|
||||
// Copy all fields of CodegenModel
|
||||
this.parent = cm.parent;
|
||||
this.parentSchema = cm.parentSchema;
|
||||
this.parentModel = cm.parentModel;
|
||||
this.interfaceModels = cm.interfaceModels;
|
||||
this.children = cm.children;
|
||||
this.name = cm.name;
|
||||
this.classname = cm.classname;
|
||||
this.title = cm.title;
|
||||
this.description = cm.description;
|
||||
this.classVarName = cm.classVarName;
|
||||
this.modelJson = cm.modelJson;
|
||||
this.dataType = cm.dataType;
|
||||
this.xmlPrefix = cm.xmlPrefix;
|
||||
this.xmlNamespace = cm.xmlNamespace;
|
||||
this.xmlName = cm.xmlName;
|
||||
this.classFilename = cm.classFilename;
|
||||
this.unescapedDescription = cm.unescapedDescription;
|
||||
this.discriminator = cm.discriminator;
|
||||
this.defaultValue = cm.defaultValue;
|
||||
this.arrayModelType = cm.arrayModelType;
|
||||
this.isAlias = cm.isAlias;
|
||||
this.vars = cm.vars;
|
||||
this.requiredVars = cm.requiredVars;
|
||||
this.optionalVars = cm.optionalVars;
|
||||
this.readOnlyVars = cm.readOnlyVars;
|
||||
this.readWriteVars = cm.readWriteVars;
|
||||
this.allVars = cm.allVars;
|
||||
this.parentVars = cm.parentVars;
|
||||
this.allowableValues = cm.allowableValues;
|
||||
this.mandatory = cm.mandatory;
|
||||
this.allMandatory = cm.allMandatory;
|
||||
this.imports = cm.imports;
|
||||
this.hasVars = cm.hasVars;
|
||||
this.emptyVars = cm.emptyVars;
|
||||
this.hasMoreModels = cm.hasMoreModels;
|
||||
this.hasEnums = cm.hasEnums;
|
||||
this.isEnum = cm.isEnum;
|
||||
this.hasRequired = cm.hasRequired;
|
||||
this.hasOptional = cm.hasOptional;
|
||||
this.isArrayModel = cm.isArrayModel;
|
||||
this.hasChildren = cm.hasChildren;
|
||||
this.hasOnlyReadOnly = cm.hasOnlyReadOnly;
|
||||
this.externalDocs = cm.externalDocs;
|
||||
this.vendorExtensions = cm.vendorExtensions;
|
||||
this.additionalPropertiesType = cm.additionalPropertiesType;
|
||||
|
||||
this.hasImports = !this.imports.isEmpty();
|
||||
}
|
||||
|
||||
public boolean hasComplexVars() {
|
||||
for (CodegenProperty p : vars) {
|
||||
if (!p.isPrimitiveType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -386,4 +689,8 @@ public class ElixirClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
// no need to escape as Elixir does not support multi-line comments
|
||||
return input;
|
||||
}
|
||||
|
||||
public void setModuleName(String moduleName) {
|
||||
this.moduleName = moduleName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# {{#modulized}}{{appName}}{{/modulized}}
|
||||
# {{moduleName}}
|
||||
|
||||
**TODO: Add description**
|
||||
{{appDescription}}
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `{{#underscored}}{{appName}}{{/underscored}}` to your list of dependencies in `mix.exs`:
|
||||
by adding `{{#underscored}}{{packageName}}{{/underscored}}` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:{{#underscored}}{{appName}}{{/underscored}}, "~> 0.1.0"}]
|
||||
[{:{{#underscored}}{{packageName}}{{/underscored}}, "~> 0.1.0"}]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at [https://hexdocs.pm/{{#underscored}}{{appName}}{{/underscored}}](https://hexdocs.pm/{{#underscored}}{{appName}}{{/underscored}}).
|
||||
be found at [https://hexdocs.pm/{{#underscored}}{{packageName}}{{/underscored}}](https://hexdocs.pm/{{#underscored}}{{packageName}}{{/underscored}}).
|
||||
|
||||
@@ -1,35 +1,59 @@
|
||||
defmodule {{#modulized}}{{appName}}{{/modulized}}.Api.{{classname}} do
|
||||
{{>licenseInfo}}
|
||||
defmodule {{moduleName}}.Api.{{classname}} do
|
||||
@moduledoc """
|
||||
Documentation for {{#modulized}}{{appName}}{{/modulized}}.Api.{{classname}}.
|
||||
API calls for all endpoints tagged `{{baseName}}`.
|
||||
"""
|
||||
|
||||
use Tesla
|
||||
alias {{moduleName}}.Connection
|
||||
import {{moduleName}}.RequestBuilder
|
||||
|
||||
plug Tesla.Middleware.BaseUrl, "{{{basePath}}}"
|
||||
plug Tesla.Middleware.JSON
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
{{#operation}}
|
||||
|
||||
@doc """
|
||||
{{#summary}}
|
||||
{{summary}}
|
||||
{{^notes.isEmpty}}
|
||||
|
||||
{{/summary}}
|
||||
{{#notes}}
|
||||
{{notes}}
|
||||
{{/notes.isEmpty}}
|
||||
"""
|
||||
def {{#underscored}}{{operationId}}{{/underscored}}({{#allParams}}{{^-first}}, {{/-first}}{{#underscored}}{{paramName}}{{/underscored}}{{/allParams}}) do
|
||||
method = [method: :{{#underscored}}{{httpMethod}}{{/underscored}}]
|
||||
url = [url: "{{replacedPathName}}"]
|
||||
query_params = [{{^queryParams.isEmpty}}query: [{{#queryParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/queryParams}}]{{/queryParams.isEmpty}}]
|
||||
header_params = [{{^headerParams.isEmpty}}header: [{{#headerParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/headerParams}}]{{/headerParams.isEmpty}}]
|
||||
body_params = [{{^bodyParams.isEmpty}}body: {{#bodyParams}}{{#underscored}}{{paramName}}{{/underscored}}{{/bodyParams}}{{/bodyParams.isEmpty}}]
|
||||
form_params = [{{^formParams.isEmpty}}body: Enum.map_join([{{#formParams}}{{^-first}}, {{/-first}}{:"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}}}{{/formParams}}], "&", &("#{elem(&1, 0)}=#{elem(&1, 1)}")){{/formParams.isEmpty}}]
|
||||
params = query_params ++ header_params ++ body_params ++ form_params
|
||||
opts = []
|
||||
options = method ++ url ++ params ++ opts
|
||||
{{/notes}}
|
||||
|
||||
request(options)
|
||||
## Parameters
|
||||
|
||||
- connection ({{moduleName}}.Connection): Connection to server
|
||||
{{#allParams}}{{#required}} - {{#underscored}}{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
|
||||
{{/required}}{{/allParams}} - opts (KeywordList): [optional] Optional parameters
|
||||
{{#allParams}}{{^required}} - {{#underscored}}:{{paramName}}{{/underscored}} ({{dataType}}): {{description}}
|
||||
{{/required}}{{/allParams}}
|
||||
## Returns
|
||||
|
||||
{:ok, {{#isListContainer}}[%{{returnBaseType}}{}, ...]{{/isListContainer}}{{#isMapContainer}}%{}{{/isMapContainer}}{{^returnType}}%{}{{/returnType}}{{#returnSimpleType}}%{{#returnType}}{{#isMapContainer}}{{/isMapContainer}}{{moduleName}}.Model.{{{returnType}}}{{/returnType}}{}{{/returnSimpleType}}} on success
|
||||
{:error, info} on failure
|
||||
"""
|
||||
{{typespec}}
|
||||
def {{#underscored}}{{operationId}}{{/underscored}}(connection, {{#allParams}}{{#required}}{{#underscored}}{{paramName}}{{/underscored}}, {{/required}}{{/allParams}}{{^hasOptionalParams}}_{{/hasOptionalParams}}opts \\ []) do
|
||||
{{#hasOptionalParams}}
|
||||
optional_params = %{
|
||||
{{#allParams}}{{^required}}{{^isPathParam}}:"{{baseName}}" => {{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}:form{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}{{/isPathParam}}{{#hasMore}},
|
||||
{{/hasMore}}{{/required}}{{/allParams}}
|
||||
}
|
||||
{{/hasOptionalParams}}
|
||||
%{}
|
||||
|> method(:{{#underscored}}{{httpMethod}}{{/underscored}})
|
||||
|> url("{{replacedPathName}}")
|
||||
{{#allParams}}
|
||||
{{#required}}
|
||||
{{^isPathParam}} |> add_param({{#isBodyParam}}:body{{/isBodyParam}}{{#isFormParam}}{{#isMultipart}}{{#isFile}}:file{{/isFile}}{{^isFile}}:form{{/isFile}}{{/isMultipart}}{{^isMultipart}}:form{{/isMultipart}}{{/isFormParam}}{{#isQueryParam}}:query{{/isQueryParam}}{{#isHeaderParam}}:headers{{/isHeaderParam}}, :"{{baseName}}", {{#underscored}}{{paramName}}{{/underscored}})
|
||||
{{/isPathParam}}
|
||||
{{/required}}
|
||||
{{/allParams}}
|
||||
{{#hasOptionalParams}}
|
||||
|> add_optional_params(optional_params, opts)
|
||||
{{/hasOptionalParams}}
|
||||
|> Enum.into([])
|
||||
|> (&Connection.request(connection, &1)).()
|
||||
|> decode({{decodedStruct}})
|
||||
end
|
||||
{{/operation}}
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
end
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
{{>licenseInfo}}
|
||||
defmodule {{moduleName}}.Connection do
|
||||
@moduledoc """
|
||||
Handle Tesla connections for {{moduleName}}.
|
||||
"""
|
||||
|
||||
use Tesla
|
||||
|
||||
# Add any middleware here (authentication)
|
||||
plug Tesla.Middleware.BaseUrl, "{{{basePath}}}"
|
||||
plug Tesla.Middleware.Headers, %{"User-Agent" => "Elixir"}
|
||||
plug Tesla.Middleware.EncodeJson
|
||||
|
||||
{{#hasAuthMethods}}
|
||||
{{#authMethods}}
|
||||
{{#isOAuth}}
|
||||
@scopes [
|
||||
{{#scopes}}
|
||||
"{{scope}}"{{#hasMore}},{{/hasMore}} {{#description}}# {{description}}{{/description}}
|
||||
{{/scopes}}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Configure a client connection using a provided OAuth2 token as a Bearer token
|
||||
|
||||
## Parameters
|
||||
|
||||
- token (String): Bearer token
|
||||
|
||||
## Returns
|
||||
|
||||
Tesla.Env.client
|
||||
"""
|
||||
@spec new(String.t) :: Tesla.Env.client
|
||||
def new(token) when is_binary(token) do
|
||||
Tesla.build_client([
|
||||
{Tesla.Middleware.Headers, %{"Authorization" => "Bearer #{token}"}}
|
||||
])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Configure a client connection using a function which yields a Bearer token.
|
||||
|
||||
## Parameters
|
||||
|
||||
- token_fetcher (function arity of 1): Callback which provides an OAuth2 token
|
||||
given a list of scopes
|
||||
|
||||
## Returns
|
||||
|
||||
Tesla.Env.client
|
||||
"""
|
||||
@spec new(((list(String.t)) -> String.t)) :: Tesla.Env.client
|
||||
def new(token_fetcher) when is_function(token_fetcher) do
|
||||
token_fetcher.(@scopes)
|
||||
|> new
|
||||
end
|
||||
{{/isOAuth}}
|
||||
{{#isBasic}}
|
||||
@doc """
|
||||
Configure an client connection using Basic authentication.
|
||||
|
||||
## Parameters
|
||||
|
||||
- username (String): Username used for authentication
|
||||
- password (String): Password used for authentication
|
||||
|
||||
# Returns
|
||||
|
||||
Tesla.Env.client
|
||||
"""
|
||||
@spec new(String.t, String.t) :: Tesla.Env.client
|
||||
def new(username, password) do
|
||||
Tesla.build_client([
|
||||
{Tesla.Middleware.BasicAuth, %{username: username, password: password}}
|
||||
])
|
||||
end
|
||||
{{/isBasic}}
|
||||
{{/authMethods}}
|
||||
{{/hasAuthMethods}}
|
||||
@doc """
|
||||
Configure an authless client connection
|
||||
|
||||
# Returns
|
||||
|
||||
Tesla.Env.client
|
||||
"""
|
||||
@spec new() :: Tesla.Env.client
|
||||
def new do
|
||||
Tesla.build_client([])
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
{{>licenseInfo}}
|
||||
defmodule {{moduleName}}.Deserializer do
|
||||
@moduledoc """
|
||||
Helper functions for deserializing responses into models
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Update the provided model with a deserialization of a nested value
|
||||
"""
|
||||
@spec deserialize(struct(), :atom, :atom, struct(), keyword()) :: struct()
|
||||
def deserialize(model, field, :list, mod, options) do
|
||||
model
|
||||
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: [struct(mod)]]))))
|
||||
end
|
||||
def deserialize(model, field, :struct, mod, options) do
|
||||
model
|
||||
|> Map.update!(field, &(Poison.Decode.decode(&1, Keyword.merge(options, [as: struct(mod)]))))
|
||||
end
|
||||
def deserialize(model, field, :map, mod, options) do
|
||||
model
|
||||
|> Map.update!(field, &(Map.new(&1, fn {key, val} -> {key, Poison.Decode.decode(val, Keyword.merge(options, [as: struct(mod)]))} end)))
|
||||
end
|
||||
def deserialize(model, field, :date, _, _options) do
|
||||
case DateTime.from_iso8601(Map.get(model, field)) do
|
||||
{:ok, datetime} ->
|
||||
Map.put(model, field, datetime)
|
||||
_ ->
|
||||
model
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps
|
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||
/doc
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
@@ -0,0 +1,6 @@
|
||||
{{#licenseHeader}}{{licenseHeader}}
|
||||
|
||||
{{/licenseHeader}}
|
||||
# NOTE: This class is auto generated by the swagger code generator program.
|
||||
# https://github.com/swagger-api/swagger-codegen.git
|
||||
# Do not edit the class manually.
|
||||
@@ -1,8 +1,8 @@
|
||||
defmodule {{#modulized}}{{appName}}{{/modulized}}.Mixfile do
|
||||
defmodule {{moduleName}}.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :{{#underscored}}{{appName}}{{/underscored}},
|
||||
[app: :{{#underscored}}{{packageName}}{{/underscored}},
|
||||
version: "0.1.0",
|
||||
elixir: "~> {{supportedElixirVersion}}",
|
||||
build_embedded: Mix.env == :prod,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{{>licenseInfo}}
|
||||
{{#models}}{{#model}}defmodule {{moduleName}}.Model.{{classname}} do
|
||||
@moduledoc """
|
||||
{{description}}
|
||||
"""
|
||||
|
||||
@derive [Poison.Encoder]
|
||||
defstruct [
|
||||
{{#vars}}:"{{baseName}}"{{#hasMore}},
|
||||
{{/hasMore}}{{/vars}}
|
||||
]
|
||||
end
|
||||
|
||||
defimpl Poison.Decoder, for: {{moduleName}}.Model.{{classname}} do
|
||||
{{#hasComplexVars}}
|
||||
import {{moduleName}}.Deserializer
|
||||
def decode(value, options) do
|
||||
value
|
||||
{{#vars}}
|
||||
{{^isPrimitiveType}}
|
||||
{{#datatype}}|> deserialize(:"{{baseName}}", {{#isListContainer}}:list, {{moduleName}}.Model.{{items.datatype}}{{/isListContainer}}{{#isMapContainer}}:map, {{moduleName}}.Model.{{items.datatype}}{{/isMapContainer}}{{#isDate}}:date, nil{{/isDate}}{{#isDateTime}}:date, nil{{/isDateTime}}{{^isDate}}{{^isDateTime}}{{^isMapContainer}}{{^isListContainer}}:struct, {{moduleName}}.Model.{{datatype}}{{/isListContainer}}{{/isMapContainer}}{{/isDateTime}}{{/isDate}}, options)
|
||||
{{/datatype}}
|
||||
{{/isPrimitiveType}}
|
||||
{{/vars}}
|
||||
{{/hasComplexVars}}
|
||||
{{^hasComplexVars}}
|
||||
def decode(value, _options) do
|
||||
value
|
||||
{{/hasComplexVars}}
|
||||
end
|
||||
end
|
||||
{{/model}}{{/models}}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
{{>licenseInfo}}
|
||||
defmodule {{moduleName}}.RequestBuilder do
|
||||
@moduledoc """
|
||||
Helper functions for building Tesla requests
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Specify the request method when building a request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- m (String) - Request method
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec method(map(), String.t) :: map()
|
||||
def method(request, m) do
|
||||
Map.put_new(request, :method, m)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Specify the request method when building a request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- u (String) - Request URL
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec url(map(), String.t) :: map()
|
||||
def url(request, u) do
|
||||
Map.put_new(request, :url, u)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add optional parameters to the request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- definitions (Map) - Map of parameter name to parameter location.
|
||||
- options (KeywordList) - The provided optional parameters
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec add_optional_params(map(), %{optional(:atom) => :atom}, keyword()) :: map()
|
||||
def add_optional_params(request, _, []), do: request
|
||||
def add_optional_params(request, definitions, [{key, value} | tail]) do
|
||||
case definitions do
|
||||
%{^key => location} ->
|
||||
request
|
||||
|> add_param(location, key, value)
|
||||
|> add_optional_params(definitions, tail)
|
||||
_ ->
|
||||
add_optional_params(request, definitions, tail)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Add optional parameters to the request
|
||||
|
||||
## Parameters
|
||||
|
||||
- request (Map) - Collected request options
|
||||
- location (atom) - Where to put the parameter
|
||||
- key (atom) - The name of the parameter
|
||||
- value (any) - The value of the parameter
|
||||
|
||||
## Returns
|
||||
|
||||
Map
|
||||
"""
|
||||
@spec add_param(map(), :atom, :atom, any()) :: map()
|
||||
def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
|
||||
def add_param(request, :body, key, value) do
|
||||
request
|
||||
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|
||||
|> Map.update!(:body, &(Tesla.Multipart.add_field(&1, key, Poison.encode!(value), headers: [{:"Content-Type", "application/json"}])))
|
||||
end
|
||||
def add_param(request, :file, name, path) do
|
||||
request
|
||||
|> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
|
||||
|> Map.update!(:body, &(Tesla.Multipart.add_file(&1, path, name: name)))
|
||||
end
|
||||
def add_param(request, :form, name, value) do
|
||||
request
|
||||
|> Map.update(:body, %{name => value}, &(Map.put(&1, name, value)))
|
||||
end
|
||||
def add_param(request, location, key, value) do
|
||||
Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handle the response for a Tesla request
|
||||
|
||||
## Parameters
|
||||
|
||||
- env (Tesla.Env) - The response object
|
||||
- struct - The shape of the struct to deserialize into
|
||||
|
||||
## Returns
|
||||
|
||||
{:ok, struct} on success
|
||||
{:error, info} on failure
|
||||
"""
|
||||
@spec decode(Tesla.Env.t) :: {:ok, struct()} | {:error, Tesla.Env.t}
|
||||
def decode(%Tesla.Env{status: 200, body: body}), do: Poison.decode(body)
|
||||
def decode(response) do
|
||||
{:error, response}
|
||||
end
|
||||
@spec decode(Tesla.Env.t, struct()) :: {:ok, struct()} | {:error, Tesla.Env.t}
|
||||
def decode(%Tesla.Env{status: 200} = env, false), do: {:ok, env}
|
||||
def decode(%Tesla.Env{status: 200, body: body}, struct) do
|
||||
Poison.decode(body, as: struct)
|
||||
end
|
||||
def decode(response, _struct) do
|
||||
{:error, response}
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@ import io.swagger.codegen.AbstractOptionsTest;
|
||||
import io.swagger.codegen.CodegenConfig;
|
||||
import io.swagger.codegen.languages.ElixirClientCodegen;
|
||||
import io.swagger.codegen.options.ElixirClientOptionsProvider;
|
||||
import io.swagger.codegen.options.PhpClientOptionsProvider;
|
||||
import mockit.Expectations;
|
||||
import mockit.Tested;
|
||||
|
||||
@@ -26,6 +27,8 @@ public class ElixirClientOptionsTest extends AbstractOptionsTest {
|
||||
protected void setExpectations() {
|
||||
new Expectations(clientCodegen) {{
|
||||
// TODO
|
||||
clientCodegen.setModuleName(ElixirClientOptionsProvider.INVOKER_PACKAGE_VALUE);
|
||||
times = 1;
|
||||
}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.swagger.codegen.CodegenConstants;
|
||||
import java.util.Map;
|
||||
|
||||
public class ElixirClientOptionsProvider implements OptionsProvider {
|
||||
public static final String INVOKER_PACKAGE_VALUE = "Yay.Pets";
|
||||
|
||||
@Override
|
||||
public String getLanguage() {
|
||||
@@ -19,6 +20,9 @@ public class ElixirClientOptionsProvider implements OptionsProvider {
|
||||
.put(CodegenConstants.SORT_PARAMS_BY_REQUIRED_FLAG, "false")
|
||||
.put(CodegenConstants.ENSURE_UNIQUE_PARAMS, "false")
|
||||
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, "false")
|
||||
.put(CodegenConstants.INVOKER_PACKAGE, "Yay.Pets")
|
||||
.put("licenseHeader", "# Copyright 2017 Me\n#\n# Licensed under the Apache License")
|
||||
.put(CodegenConstants.PACKAGE_NAME, "yay_pets")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user