[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:
Jeff Ching
2017-09-27 18:32:17 -07:00
committed by wing328
parent 3ac2b803f9
commit 4b9ee1f194
80 changed files with 2532 additions and 439 deletions

View File

@@ -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;
}
}

View File

@@ -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}}).

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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,

View File

@@ -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}}

View File

@@ -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

View File

@@ -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;
}};
}
}

View File

@@ -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();
}