Generator erlang-proper (#1102)

* Generator erlang-proper

Used to generate PropEr generators for property-based testing

* Fix binary/2 implementation. Add behaviour attribute.

* Remove line from copyright notice

* Avoid escpaing HTML and remove suffix from variable name

* Update samples

* Include querystring parameters

* We use export_all, don't consider warnings as errors

* List command sequence on failure

* Use hasConsumes instead

* Add nowarn_export_all, re-add warning_as_errors
This commit is contained in:
Juan Facorro 2018-10-27 10:54:35 +02:00 committed by William Cheng
parent a6b0a8b4b7
commit fc0a0d2cda
32 changed files with 1836 additions and 0 deletions

32
bin/erlang-petstore-proper.sh Executable file
View File

@ -0,0 +1,32 @@
#!/bin/sh
SCRIPT="$0"
echo "# START SCRIPT: $SCRIPT"
while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done
if [ ! -d "${APP_DIR}" ]; then
APP_DIR=`dirname "$SCRIPT"`/..
APP_DIR=`cd "${APP_DIR}"; pwd`
fi
executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"
if [ ! -f "$executable" ]
then
mvn -B clean package
fi
# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -XX:MaxPermSize=256M -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="generate -t modules/openapi-generator/src/main/resources/erlang-proper -DpackageName=petstore -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -g erlang-proper -o samples/client/petstore/erlang-proper $@"
java $JAVA_OPTS -jar $executable $ags

View File

@ -0,0 +1,542 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.languages;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ErlangProperCodegen extends DefaultCodegen implements CodegenConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(ErlangProperCodegen.class);
protected String packageName = "openapi";
protected String packageVersion = "1.0.0";
protected String sourceFolder = "src";
protected String modelFolder = "model";
public CodegenType getTag() {
return CodegenType.CLIENT;
}
public String getName() {
return "erlang-proper";
}
public String getHelp() {
return "Generates an Erlang library with PropEr generators (beta).";
}
public ErlangProperCodegen() {
super();
outputFolder = "generated-code/erlang";
modelTemplateFiles.put("model.mustache", ".erl");
apiTemplateFiles.put("api.mustache", "_api.erl");
apiTemplateFiles.put("statem.mustache", "_statem.erl");
embeddedTemplateDir = templateDir = "erlang-proper";
setReservedWordsLowerCase(
Arrays.asList(
"after", "and", "andalso", "band", "begin", "bnot", "bor", "bsl", "bsr", "bxor", "case",
"catch", "cond", "div", "end", "fun", "if", "let", "not", "of", "or", "orelse", "receive",
"rem", "try", "when", "xor"
)
);
instantiationTypes.clear();
typeMapping.clear();
typeMapping.put("enum", "binary()");
typeMapping.put("date", "date()");
typeMapping.put("datetime", "datetime()");
typeMapping.put("DateTime", "datetime()");
typeMapping.put("boolean", "boolean()");
typeMapping.put("string", "binary()");
typeMapping.put("integer", "integer()");
typeMapping.put("int", "integer()");
typeMapping.put("float", "integer()");
typeMapping.put("long", "integer()");
typeMapping.put("double", "float()");
typeMapping.put("array", "list()");
typeMapping.put("map", "map()");
typeMapping.put("number", "integer()");
typeMapping.put("bigdecimal", "float()");
typeMapping.put("List", "list()");
typeMapping.put("object", "map()");
typeMapping.put("file", "binary()");
typeMapping.put("binary", "binary()");
typeMapping.put("bytearray", "binary()");
typeMapping.put("byte", "binary()");
typeMapping.put("uuid", "binary()");
typeMapping.put("password", "binary()");
cliOptions.clear();
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Erlang application name (convention: lowercase).")
.defaultValue(this.packageName));
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Erlang application version")
.defaultValue(this.packageVersion));
}
@Override
public CodegenModel fromModel(String name, Schema model, Map<String, Schema> allDefinitions) {
CodegenModel cm = super.fromModel(name, model, allDefinitions);
if(ModelUtils.isArraySchema(model)) {
return new CodegenArrayModel(cm, (ArraySchema) model);
} else {
return cm;
}
}
@Override
public String getTypeDeclaration(String name) {
return name + ":" + name + "()";
}
@Override
public String getTypeDeclaration(Schema schema) {
String typeDeclaration = super.getSchemaType(schema);
if(ModelUtils.isArraySchema(schema)) {
ArraySchema arraySchema = (ArraySchema) schema;
String complexType = getSchemaType(arraySchema.getItems());
StringBuilder sb = new StringBuilder("list(");
sb.append(complexType);
return sb.append(")").toString();
} else if (typeMapping.containsKey(typeDeclaration)) {
return typeMapping.get(typeDeclaration);
} else {
return getTypeDeclaration(toModelName(snakeCase(typeDeclaration)));
}
}
@Override
public String getSchemaType(Schema schema) {
String schemaType = super.getSchemaType(schema);
if(ModelUtils.isArraySchema(schema)) {
ArraySchema arraySchema = (ArraySchema) schema;
String complexType = getSchemaType(arraySchema.getItems());
StringBuilder sb = new StringBuilder("list(");
sb.append(complexType);
Integer minItems = schema.getMinItems();
Integer maxItems = schema.getMaxItems();
if(minItems != null) sb.append(", ").append(minItems);
if(minItems != null && maxItems != null) sb.append(", ").append(maxItems);
return sb.append(")").toString();
} else if(ModelUtils.isIntegerSchema(schema)) {
StringBuilder sb = new StringBuilder("integer(");
BigDecimal min = schema.getMinimum();
BigDecimal max = schema.getMaximum();
if(min != null) sb.append(min);
if(min != null && max != null) sb.append(", ").append(max);
return sb.append(")").toString();
} else if(ModelUtils.isDateSchema(schema) || ModelUtils.isDateTimeSchema(schema)) {
return typeMapping.get(schemaType);
} else if(ModelUtils.isStringSchema(schema)) {
StringBuilder sb = new StringBuilder("binary(");
Integer min = schema.getMinLength();
Integer max = schema.getMaxLength();
if(min != null) sb.append(min);
if(min != null && max != null) sb.append(", ").append(max);
return sb.append(")").toString();
} else if (typeMapping.containsKey(schemaType)) {
return typeMapping.get(schemaType);
} else {
return getTypeDeclaration(toModelName(snakeCase(schemaType)));
}
}
@Override
public void processOpts() {
super.processOpts();
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
} else {
setPackageName("openapi");
}
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) {
setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION));
} else {
setPackageVersion("1.0.0");
}
additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion);
additionalProperties.put("length", new Mustache.Lambda() {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
writer.write(length(fragment.context()));
}
});
additionalProperties.put("qsEncode", new Mustache.Lambda() {
@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
writer.write(qsEncode(fragment.context()));
}
});
modelPackage = packageName;
apiPackage = packageName;
supportingFiles.add(new SupportingFile("rebar.config.mustache", "", "rebar.config"));
supportingFiles.add(new SupportingFile("app.src.mustache", "", "src" + File.separator +
this.packageName + ".app.src"));
supportingFiles.add(new SupportingFile("utils.mustache", "", "src" + File.separator +
this.packageName + "_utils.erl"));
supportingFiles.add(new SupportingFile("gen.mustache", "", "src" + File.separator + this
.packageName + "_gen.erl"));
supportingFiles.add(new SupportingFile("include.mustache", "", "src" + File.separator +
this.packageName + ".hrl"));
supportingFiles.add(new SupportingFile("statem.hrl.mustache", "", "src" + File.separator +
this.packageName + "_statem.hrl"));
supportingFiles.add(new SupportingFile("test.mustache", "", "test" + File.separator +
"prop_" + this.packageName + ".erl"));
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
}
private String qsEncode(Object o) {
String r = "";
CodegenParameter q = (CodegenParameter) o;
if (q.required) {
if (q.isListContainer) {
r += "[{<<\"" + q.baseName + "\">>, X} || X <- " + q.paramName + "]";
} else {
r += "{<<\"" + q.baseName + "\">>, " + q.paramName + "}";
}
}
return r;
}
@Override
public String escapeReservedWord(String name) {
// Can't start with an underscore, as our fields need to start with an
// UppercaseLetter so that Go treats them as public/visible.
// Options?
// - MyName
// - AName
// - TheName
// - XName
// - X_Name
// ... or maybe a suffix?
// - Name_ ... think this will work.
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return camelize(name) + '_';
}
@Override
public String apiFileFolder() {
return outputFolder + File.separator + sourceFolder + File.separator;
}
@Override
public String modelFileFolder() {
return outputFolder + File.separator
+ sourceFolder + File.separator
+ modelFolder + File.separator;
}
@Override
public String toVarName(String name) {
// replace - with _ e.g. created-at => created_at
name = sanitizeName(name.replaceAll("-", "_"));
// for reserved word or word starting with number, append _
if (isReservedWord(name))
name = escapeReservedWord(name);
return name;
}
@Override
public String toParamName(String name) {
return camelize(toVarName(name));
}
@Override
public String toArrayModelParamName(String name) {
if (name == null) {
LOGGER.warn("parameter name for array model is null. Default to 'array_model'.");
name = "array_model";
}
if (name.indexOf(":") > 0) {
name = name.substring(0, name.indexOf(":")) + "_array";
}
return toParamName(name);
}
@Override
public String toModelName(String name) {
return this.packageName + "_" + underscore(name.replaceAll("-", "_").replaceAll("\\.", "_"));
}
@Override
public String toApiName(String name) {
return this.packageName;
}
@Override
public String toModelFilename(String name) {
return this.packageName + "_" + underscore(name.replaceAll("\\.", "_"));
}
@Override
public String toApiFilename(String name) {
return toApiName(name);
}
@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + underscore(sanitizeName("call_" + operationId)).replaceAll("\\.", "_"));
operationId = "call_" + operationId;
}
return underscore(operationId.replaceAll("\\.", "_"));
}
@Override
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
Map<String, Object> operations = (Map<String, Object>) objs.get("operations");
List<CodegenOperation> os = (List<CodegenOperation>) operations.get("operation");
List<ExtendedCodegenOperation> newOs = new ArrayList<>();
Pattern pattern = Pattern.compile("\\{([^\\}]+)\\}");
for (CodegenOperation o : os) {
// force http method to lower case
o.httpMethod = o.httpMethod.toLowerCase(Locale.ROOT);
if (o.isListContainer) {
o.returnType = "[" + o.returnBaseType + "]";
}
Matcher matcher = pattern.matcher(o.path);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
String pathTemplateName = matcher.group(1);
matcher.appendReplacement(buffer, "\", " + camelize(pathTemplateName) + ", \"");
}
matcher.appendTail(buffer);
ExtendedCodegenOperation eco = new ExtendedCodegenOperation(o);
if (buffer.length() == 0) {
eco.setReplacedPathName(o.path);
} else {
eco.setReplacedPathName(buffer.toString());
}
newOs.add(eco);
}
operations.put("operation", newOs);
return objs;
}
public void setPackageName(String packageName) {
this.packageName = packageName;
}
public void setPackageVersion(String packageVersion) {
this.packageVersion = packageVersion;
}
String length(Object os) {
int l = 1;
for (CodegenParameter o : ((ExtendedCodegenOperation) os).allParams) {
if (o.required)
l++;
}
return Integer.toString(l);
}
private int lengthRequired(List<CodegenParameter> allParams) {
int l = 0;
for (CodegenParameter o : allParams) {
if (o.required || o.isBodyParam)
l++;
}
return l;
}
@Override
public String escapeQuotationMark(String input) {
// remove " to avoid code injection
return input.replace("\"", "");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("*/", "*_/").replace("/*", "/_*");
}
class CodegenArrayModel extends CodegenModel {
Integer minItems;
Integer maxItems;
public CodegenArrayModel(CodegenModel cm, ArraySchema schema) {
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.externalDocumentation = cm.externalDocumentation;
this.vendorExtensions = cm.vendorExtensions;
this.additionalPropertiesType = cm.additionalPropertiesType;
this.minItems = schema.getMinItems();
this.maxItems = schema.getMaxItems();
}
}
class ExtendedCodegenOperation extends CodegenOperation {
private String replacedPathName;
String arity;
ExtendedCodegenOperation(CodegenOperation o) {
super();
// Copy all fields of CodegenOperation
this.responseHeaders.addAll(o.responseHeaders);
this.hasAuthMethods = o.hasAuthMethods;
this.hasConsumes = o.hasConsumes;
this.hasProduces = o.hasProduces;
this.hasParams = o.hasParams;
this.hasOptionalParams = o.hasOptionalParams;
this.returnTypeIsPrimitive = o.returnTypeIsPrimitive;
this.returnSimpleType = o.returnSimpleType;
this.subresourceOperation = o.subresourceOperation;
this.isMapContainer = o.isMapContainer;
this.isListContainer = o.isListContainer;
this.isMultipart = o.isMultipart;
this.hasMore = o.hasMore;
this.isResponseBinary = o.isResponseBinary;
this.hasReference = o.hasReference;
this.isRestfulIndex = o.isRestfulIndex;
this.isRestfulShow = o.isRestfulShow;
this.isRestfulCreate = o.isRestfulCreate;
this.isRestfulUpdate = o.isRestfulUpdate;
this.isRestfulDestroy = o.isRestfulDestroy;
this.isRestful = o.isRestful;
this.path = o.path;
this.operationId = o.operationId;
this.returnType = o.returnType;
this.httpMethod = o.httpMethod;
this.returnBaseType = o.returnBaseType;
this.returnContainer = o.returnContainer;
this.summary = o.summary;
this.unescapedNotes = o.unescapedNotes;
this.notes = o.notes;
this.baseName = o.baseName;
this.defaultResponse = o.defaultResponse;
this.discriminator = o.discriminator;
this.consumes = o.consumes;
this.produces = o.produces;
this.bodyParam = o.bodyParam;
this.allParams = o.allParams;
this.arity = Integer.toString(lengthRequired(o.allParams));
this.bodyParams = o.bodyParams;
this.pathParams = o.pathParams;
this.queryParams = o.queryParams;
this.headerParams = o.headerParams;
this.formParams = o.formParams;
this.authMethods = o.authMethods;
this.tags = o.tags;
this.responses = o.responses;
this.imports = o.imports;
this.examples = o.examples;
this.externalDocs = o.externalDocs;
this.vendorExtensions = o.vendorExtensions;
this.nickname = o.nickname;
this.operationIdLowerCase = o.operationIdLowerCase;
this.operationIdCamelCase = o.operationIdCamelCase;
}
public String getReplacedPathName() {
return replacedPathName;
}
public void setReplacedPathName(String replacedPathName) {
this.replacedPathName = replacedPathName;
}
}
}

View File

@ -22,6 +22,7 @@ org.openapitools.codegen.languages.EiffelClientCodegen
org.openapitools.codegen.languages.ElixirClientCodegen
org.openapitools.codegen.languages.ElmClientCodegen
org.openapitools.codegen.languages.ErlangClientCodegen
org.openapitools.codegen.languages.ErlangProperCodegen
org.openapitools.codegen.languages.ErlangServerCodegen
org.openapitools.codegen.languages.FlashClientCodegen
org.openapitools.codegen.languages.FinchServerCodegen

View File

@ -0,0 +1,6 @@
# OpenAPI client library for Erlang with Erlang QuickCheck generators
## Overview
An Erlang client stub and Erlang QuickCheck generators, generated by
[OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec.

View File

@ -0,0 +1,30 @@
-module({{classname}}_api).
-export([ {{#operations}}{{#operation}}{{^-first}}
, {{/-first}}{{operationId}}/{{arity}}{{/operation}}{{/operations}}
]).
-define(BASE_URL, "{{{basePathWithoutHost}}}").
{{#operations}}
{{#operation}}
%% @doc {{{summary}}}
{{^notes.isEmpty}}
%% {{{notes}}}
{{/notes.isEmpty}}
-spec {{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{{dataType}}}{{/required}}{{/allParams}}) ->
{{packageName}}_utils:response().
{{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{paramName}}{{/required}}{{/allParams}}) ->
Method = {{httpMethod}},
Host = application:get_env({{packageName}}, host, "http://localhost:8080"),
Path = ["{{{replacedPathName}}}"],
Body = {{^formParams.isEmpty}}{form, [{{#formParams}}{{#required}}{{^-first}}, {{/-first}}{<<"{{{baseName}}}">>, {{paramName}}{{/required}}{{/formParams}}]++{{packageName}}_utils:optional_params([{{#formParams}}{{^required}}{{^-first}}, {{/-first}}'{{{baseName}}}'{{/required}}{{/formParams}}], _OptionalParams)}{{/formParams.isEmpty}}{{#formParams.isEmpty}}{{#bodyParams.isEmpty}}[]{{/bodyParams.isEmpty}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/bodyParams.isEmpty}}{{/formParams.isEmpty}},
ContentType = {{#hasConsumes}}hd([{{#consumes}}{{^-first}}, {{/-first}}"{{mediaType}}"{{/consumes}}]){{/hasConsumes}}{{^hasConsumes}}<<"text/plain">>{{/hasConsumes}},
{{^queryParams.isEmpty}}
QueryString = [{{#queryParams}}{{^-first}}, {{/-first}}<<"{{{baseName}}}=">>, {{{paramName}}}, <<"&">>{{/queryParams}}],
{{/queryParams.isEmpty}}
{{packageName}}_utils:request(Method, [Host, ?BASE_URL, Path{{^queryParams.isEmpty}}, <<"?">>, QueryString{{/queryParams.isEmpty}}], jsx:encode(Body), ContentType).
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,21 @@
{ application, {{packageName}}
, [ {description, {{#appDescription}}"{{appDescription}}"{{/appDescription}}{{^appDescription}}"OpenAPI client library for EQC testing"{{/appDescription}}}
, {vsn, "{{#apiVersion}}{{apiVersion}}{{/apiVersion}}{{^apiVersion}}0.1.0{{/apiVersion}}"}
, {registered, []}
, { applications
, [ kernel
, stdlib
, ssl
, jsx
]
}
, { env
, [ {host, "http://{{#host}}{{{host}}}{{/host}}{{^host}}localhost:8080{{/host}}"}
, {basic_auth, {"admin", "admin"}}
]
}
, {modules, []}
, {maintainers, []}
, {licenses, [{{#licenseInfo}}"{{licenseInfo}}"{{/licenseInfo}}]}
, {links, [{{#infoUrl}}"{{infoUrl}}"{{/infoUrl}}]}
]}.

View File

@ -0,0 +1,157 @@
-module({{packageName}}_gen).
-compile({no_auto_import,[date/0]}).
-include_lib("proper/include/proper_common.hrl").
%%==============================================================================
%% Exports
%%==============================================================================
-export([ binary/0
, binary/1
, binary/2
, integer/0
, integer/1
, integer/2
, boolean/0
, list/0
, list/1
, list/2
, list/3
, map/0
, date/0
, datetime/0
, any/0
, elements/1
]).
-define(CHARS, [$a, $b, $c]).
%%==============================================================================
%% Generators
%%==============================================================================
binary() -> binary(10).
binary(Min, Max) ->
?LET( {X, N}
, { proper_types:elements(?CHARS)
, proper_types:choose(Min, Max)
}
, iolist_to_binary(lists:duplicate(N, X))
).
binary(N) ->
?LET( X
, proper_types:elements(?CHARS)
, iolist_to_binary(lists:duplicate(N, X))
).
integer() -> proper_types:int().
integer(0) -> proper_types:nat();
integer(Min) ->
?LET( N
, proper_types:nat()
, proper_types:choose(Min, Min + N)
).
integer(Min, Max) -> proper_types:choose(Min, Max).
boolean() -> proper_types:bool().
list() -> list(any()).
list(Type) -> proper_types:list(Type).
list(Type, Min) ->
?LET( N
, integer(0)
, ?LET(X, list(Type, Min, Min + N), X)
).
list(Type, Min, Max) when Min =< Max ->
?LET( {X, Y}
, { proper_types:vector(Min, Type)
, proper_types:resize(Max - Min, proper_types:list(Type))
}
, X ++ Y
).
map() -> proper_types:map(any(), any()).
date() ->
?LET( X
, ?SUCHTHAT( X
, { year()
, proper_types:choose(1, 12)
, proper_types:choose(1, 31)
}
, calendar:valid_date(X)
)
, begin
{Year, Month, Day} = X,
YearBin = num_binary_format(Year, "4"),
MonthBin = num_binary_format(Month, "2"),
DayBin = num_binary_format(Day, "2"),
<<YearBin/binary, "-", MonthBin/binary, "-", DayBin/binary>>
end
).
datetime() ->
Date = date(),
Hour = hour(),
?LET( X
, {Date, Hour}
, begin
{D, H} = X,
<<D/binary, "T", H/binary, "+0000">>
end
).
any() ->
Any = [ binary()
, integer()
, boolean()
%% We don't include lists and maps to avoid huge values
%% , list()
%% , map()
, date()
, datetime()
],
proper_types:oneof(Any).
elements(Items) ->
proper_types:elements(Items).
%%==============================================================================
%% Internal
%%==============================================================================
year() ->
?LET( X
, proper_types:nat()
, 1970 + X
).
hour() ->
?LET( X
, { proper_types:choose(0, 23)
, proper_types:choose(0, 59)
, proper_types:choose(0, 59)
, proper_types:choose(0, 999)
}
, begin
{Hours, Mins, Secs, Millis} = X,
HoursBin = num_binary_format(Hours, "2"),
MinsBin = num_binary_format(Mins, "2"),
SecsBin = num_binary_format(Secs, "2"),
MillisBin = num_binary_format(Millis, "3"),
<<HoursBin/binary, ":", MinsBin/binary, ":",
SecsBin/binary, ".", MillisBin/binary>>
end
).
num_binary_format(X, N) ->
list_to_binary(io_lib:format("~" ++ N ++ "..0B", [X])).

View File

@ -0,0 +1,24 @@
-compile({no_auto_import,[date/0]}).
-import( {{packageName}}_gen
, [ binary/0
, binary/1
, binary/2
, integer/0
, integer/1
, integer/2
, boolean/0
, list/0
, list/1
, list/2
, list/3
, map/0
, date/0
, datetime/0
, any/0
, elements/1
]
).
-type date() :: calendar:date().
-type datetime() :: calendar:datetime().

View File

@ -0,0 +1,26 @@
{{#models}}
{{#model}}
-module({{classname}}).
-include("{{packageName}}.hrl").
-export([{{classname}}/0]).
-export_type([{{classname}}/0]).
-type {{classname}}() ::{{#isEnum}}
binary().{{/isEnum}}{{^isEnum}}{{#isArrayModel}}
list({{arrayModelType}}).{{/isArrayModel}}{{^isArrayModel}}
[ {{#vars}}{{^-first}}
| {{/-first}}{'{{name}}', {{dataType}} }{{/vars}}
].{{/isArrayModel}}{{/isEnum}}
{{classname}}() ->{{#isEnum}}
elements([{{#allowableValues.values}}{{^-first}}, {{/-first}}<<"{{.}}">>{{/allowableValues.values}}]).
{{/isEnum}}{{#isArrayModel}}
list({{arrayModelType}}{{#minItems}}, {{minItems}}{{#maxItems}}, {{maxItems}}{{/maxItems}}{{/minItems}}).{{/isArrayModel}}{{^isEnum}}{{^isArrayModel}}
[ {{#vars}}{{^-first}}
, {{/-first}}{'{{baseName}}', {{#isString}}{{#isEnum}}elements([{{#allowableValues.values}}{{^-first}}, {{/-first}}<<"{{.}}">>{{/allowableValues.values}}]){{/isEnum}}{{^isEnum}}binary({{#minLength}}{{minLength}}{{#maxLength}}, {{maxLength}}{{/maxLength}}{{/minLength}}){{/isEnum}}{{/isString}}{{^isString}}{{baseType}}{{/isString}} }{{/vars}}
].{{/isArrayModel}}{{/isEnum}}
{{/model}}
{{/models}}

View File

@ -0,0 +1,7 @@
{erl_opts, [debug_info, warnings_as_errors]}.
{deps, [{jsx, "2.9.0"}, {proper, "1.3.0"}]}.
{shell, [{apps, [{{packageName}}]}]}.
{plugins, [rebar3_proper]}.

View File

@ -0,0 +1,25 @@
%%==============================================================================
%% Setup
%%==============================================================================
setup() -> ok.
%%==============================================================================
%% Cleanup
%%==============================================================================
cleanup() -> ok.
%%==============================================================================
%% Initial State
%%==============================================================================
initial_state() -> #{}.
%%==============================================================================
%% State transitions callbacks
%%
%% operation_pre(State) -> true.
%% operation_next(State, Result, Args) -> State.
%% operation_post(State, Args, Result) -> true.
%%==============================================================================

View File

@ -0,0 +1,105 @@
-module({{classname}}_statem).
-behaviour(proper_statem).
-include("{{packageName}}.hrl").
-include_lib("proper/include/proper_common.hrl").
-include_lib("stdlib/include/assert.hrl").
-compile(export_all).
-compile(nowarn_export_all).
%%==============================================================================
%% PropEr callbacks
%%==============================================================================
command(State) ->
Funs0 = [ {F, list_to_atom(atom_to_list(F) ++ "_args")}
|| {F, _} <- ?MODULE:module_info(exports)
],
Funs1 = [ X || {_, FArgs} = X <- Funs0,
erlang:function_exported(?MODULE, FArgs, 1)
],
proper_types:oneof([ {call, ?MODULE, F, ?MODULE:FArgs(State)}
|| {F, FArgs} <- Funs1
]).
precondition(S, {call, M, F, Args}) ->
Pre = list_to_atom(atom_to_list(F) ++ "_pre"),
case erlang:function_exported(M, Pre, 1) of
true -> M:Pre(S);
false -> true
end
andalso
case erlang:function_exported(M, Pre, 2) of
true -> M:Pre(S, Args);
false -> true
end.
next_state(S, Res, {call, M, F, Args}) ->
Next = list_to_atom(atom_to_list(F) ++ "_next"),
case erlang:function_exported(M, Next, 3) of
true -> M:Next(S, Res, Args);
false -> S
end.
postcondition(S, {call, M, F, Args}, Res) ->
Post = list_to_atom(atom_to_list(F) ++ "_post"),
case erlang:function_exported(M, Post, 3) of
true -> M:Post(S, Args, Res);
false -> true
end.
{{#operations}}
{{#operation}}
%%==============================================================================
%% {{operationId}}
%%==============================================================================
{{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{paramName}}{{/required}}{{/allParams}}) ->
{{classname}}_api:{{operationId}}({{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{paramName}}{{/required}}{{/allParams}}).
{{operationId}}_args(S) ->
Args = [{{#allParams}}{{#required}}{{^-first}}, {{/-first}}{{dataType}}{{/required}}{{/allParams}}],
case erlang:function_exported(?MODULE, '{{operationId}}_args_custom', 2) of
true -> ?MODULE:{{operationId}}_args_custom(S, Args);
false -> Args
end.
{{/operation}}
{{/operations}}
%%==============================================================================
%% The statem's property
%%==============================================================================
prop_main() ->
setup(),
?FORALL( Cmds
, proper_statem:commands(?MODULE)
, begin
cleanup(),
{ History
, State
, Result
} = proper_statem:run_commands(?MODULE, Cmds),
?WHENFAIL(
io:format("History: ~p\nState: ~p\nResult: ~p\nCmds: ~p\n",
[ History
, State
, Result
, proper_statem:command_names(Cmds)
]),
proper:aggregate( proper_statem:command_names(Cmds)
, Result =:= ok
)
)
end
).
%%==============================================================================
%% Include file with setup, cleanup, initial_state
%% and state transitions callbacks
%%==============================================================================
-include("{{classname}}_statem.hrl").

View File

@ -0,0 +1,7 @@
-module(prop_{{packageName}}).
-export([prop_test/0]).
prop_test() ->
{ok, _} = application:ensure_all_started({{packageName}}),
{{packageName}}_statem:prop_main().

View File

@ -0,0 +1,66 @@
-module({{packageName}}_utils).
-export([ request/2
, request/4
]).
-type response() :: #{ status := integer()
, headers := map()
, body := iolist()
}.
-export_type([response/0]).
-spec request(atom(), string()) -> response().
request(Method, Url) ->
request(Method, Url, undefined, undefined).
-spec request(atom(), iolist(), iolist(), string()) -> response().
request(Method, Url0, Body, ContentType) ->
Url = binary_to_list(iolist_to_binary(Url0)),
Headers = headers(),
Request = case Body of
undefined -> {Url, Headers};
_ -> {Url, Headers, ContentType, Body}
end,
HTTPOptions = [{autoredirect, true}],
Options = [],
%% Disable pipelining to avoid the socket getting closed during long runs
ok = httpc:set_options([ {max_keep_alive_length, 0}
, {max_pipeline_length, 0}
, {max_sessions, 0}
]),
Result = httpc:request(Method, Request, HTTPOptions, Options),
{ok, {{=<% %>=}}{{_Ver, Status, _Phrase}, RespHeaders, RespBody}}<%={{ }}=%> = Result,
Response = #{ status => Status
, headers => maps:from_list(RespHeaders)
, body => RespBody
},
decode_body(Response).
-spec headers() -> [{string(), string()}].
headers() ->
[ {"Accept", "application/json"}
| basic_auth()
].
-spec basic_auth() -> [{string(), string()}].
basic_auth() ->
case application:get_env({{packageName}}, basic_auth, undefined) of
undefined -> [];
{Username, Password} ->
Credentials = base64:encode_to_string(Username ++ ":" ++ Password),
[{"Authorization", "Basic " ++ Credentials}]
end.
-spec decode_body(response()) -> response().
decode_body(#{ headers := #{"content-type" := "application/json"}
, body := Body
} = Response) ->
Json = jsx:decode( unicode:characters_to_binary(Body)
, [return_maps, {labels, atom}]
),
Response#{body_json => Json};
decode_body(Response) ->
Response.

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1 @@
3.3.0-SNAPSHOT

View File

@ -0,0 +1,6 @@
# OpenAPI client library for Erlang with Erlang QuickCheck generators
## Overview
An Erlang client stub and Erlang QuickCheck generators, generated by
[OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec.

View File

@ -0,0 +1,7 @@
{erl_opts, [debug_info, warnings_as_errors]}.
{deps, [{jsx, "2.9.0"}, {proper, "1.3.0"}]}.
{shell, [{apps, [petstore]}]}.
{plugins, [rebar3_proper]}.

View File

@ -0,0 +1,19 @@
-module(petstore_api_response).
-include("petstore.hrl").
-export([petstore_api_response/0]).
-export_type([petstore_api_response/0]).
-type petstore_api_response() ::
[ {'code', integer() }
| {'type', binary() }
| {'message', binary() }
].
petstore_api_response() ->
[ {'code', integer() }
, {'type', binary() }
, {'message', binary() }
].

View File

@ -0,0 +1,17 @@
-module(petstore_category).
-include("petstore.hrl").
-export([petstore_category/0]).
-export_type([petstore_category/0]).
-type petstore_category() ::
[ {'id', integer() }
| {'name', binary() }
].
petstore_category() ->
[ {'id', integer() }
, {'name', binary() }
].

View File

@ -0,0 +1,25 @@
-module(petstore_order).
-include("petstore.hrl").
-export([petstore_order/0]).
-export_type([petstore_order/0]).
-type petstore_order() ::
[ {'id', integer() }
| {'petId', integer() }
| {'quantity', integer() }
| {'shipDate', datetime() }
| {'status', binary() }
| {'complete', boolean() }
].
petstore_order() ->
[ {'id', integer() }
, {'petId', integer() }
, {'quantity', integer() }
, {'shipDate', datetime() }
, {'status', elements([<<"placed">>, <<"approved">>, <<"delivered">>]) }
, {'complete', boolean() }
].

View File

@ -0,0 +1,25 @@
-module(petstore_pet).
-include("petstore.hrl").
-export([petstore_pet/0]).
-export_type([petstore_pet/0]).
-type petstore_pet() ::
[ {'id', integer() }
| {'category', petstore_category:petstore_category() }
| {'name', binary() }
| {'photoUrls', list(binary()) }
| {'tags', list(petstore_tag:petstore_tag()) }
| {'status', binary() }
].
petstore_pet() ->
[ {'id', integer() }
, {'category', petstore_category:petstore_category() }
, {'name', binary() }
, {'photoUrls', list(binary()) }
, {'tags', list(petstore_tag:petstore_tag()) }
, {'status', elements([<<"available">>, <<"pending">>, <<"sold">>]) }
].

View File

@ -0,0 +1,17 @@
-module(petstore_tag).
-include("petstore.hrl").
-export([petstore_tag/0]).
-export_type([petstore_tag/0]).
-type petstore_tag() ::
[ {'id', integer() }
| {'name', binary() }
].
petstore_tag() ->
[ {'id', integer() }
, {'name', binary() }
].

View File

@ -0,0 +1,29 @@
-module(petstore_user).
-include("petstore.hrl").
-export([petstore_user/0]).
-export_type([petstore_user/0]).
-type petstore_user() ::
[ {'id', integer() }
| {'username', binary() }
| {'firstName', binary() }
| {'lastName', binary() }
| {'email', binary() }
| {'password', binary() }
| {'phone', binary() }
| {'userStatus', integer() }
].
petstore_user() ->
[ {'id', integer() }
, {'username', binary() }
, {'firstName', binary() }
, {'lastName', binary() }
, {'email', binary() }
, {'password', binary() }
, {'phone', binary() }
, {'userStatus', integer() }
].

View File

@ -0,0 +1,21 @@
{ application, petstore
, [ {description, "This is a sample server Petstore server. For this sample, you can use the api key &#x60;special-key&#x60; to test the authorization filters."}
, {vsn, "0.1.0"}
, {registered, []}
, { applications
, [ kernel
, stdlib
, ssl
, jsx
]
}
, { env
, [ {host, "http://petstore.swagger.io"}
, {basic_auth, {"admin", "admin"}}
]
}
, {modules, []}
, {maintainers, []}
, {licenses, ["Apache-2.0"]}
, {links, []}
]}.

View File

@ -0,0 +1,24 @@
-compile({no_auto_import,[date/0]}).
-import( petstore_gen
, [ binary/0
, binary/1
, binary/2
, integer/0
, integer/1
, integer/2
, boolean/0
, list/0
, list/1
, list/2
, list/3
, map/0
, date/0
, datetime/0
, any/0
, elements/1
]
).
-type date() :: calendar:date().
-type datetime() :: calendar:datetime().

View File

@ -0,0 +1,119 @@
-module(petstore_api).
-export([ create_user/1
, create_users_with_array_input/1
, create_users_with_list_input/1
, delete_user/1
, get_user_by_name/1
, login_user/2
, logout_user/0
, update_user/2
]).
-define(BASE_URL, "/v2").
%% @doc Create user
%% This can only be done by the logged in user.
-spec create_user(petstore_user:petstore_user()) ->
petstore_utils:response().
create_user(PetstoreUser) ->
Method = post,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user"],
Body = PetstoreUser,
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Creates list of users with given input array
%%
-spec create_users_with_array_input(list(petstore_user:petstore_user())) ->
petstore_utils:response().
create_users_with_array_input(PetstoreUserArray) ->
Method = post,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/createWithArray"],
Body = PetstoreUserArray,
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Creates list of users with given input array
%%
-spec create_users_with_list_input(list(petstore_user:petstore_user())) ->
petstore_utils:response().
create_users_with_list_input(PetstoreUserArray) ->
Method = post,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/createWithList"],
Body = PetstoreUserArray,
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Delete user
%% This can only be done by the logged in user.
-spec delete_user(binary()) ->
petstore_utils:response().
delete_user(Username) ->
Method = delete,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/", Username, ""],
Body = [],
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Get user by user name
%%
-spec get_user_by_name(binary()) ->
petstore_utils:response().
get_user_by_name(Username) ->
Method = get,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/", Username, ""],
Body = [],
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Logs user into the system
%%
-spec login_user(binary(), binary()) ->
petstore_utils:response().
login_user(Username, Password) ->
Method = get,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/login"],
Body = [],
ContentType = <<"text/plain">>,
QueryString = [<<"username=">>, Username, <<"&">>, <<"password=">>, Password, <<"&">>],
petstore_utils:request(Method, [Host, ?BASE_URL, Path, <<"?">>, QueryString], jsx:encode(Body), ContentType).
%% @doc Logs out current logged in user session
%%
-spec logout_user() ->
petstore_utils:response().
logout_user() ->
Method = get,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/logout"],
Body = [],
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).
%% @doc Updated user
%% This can only be done by the logged in user.
-spec update_user(binary(), petstore_user:petstore_user()) ->
petstore_utils:response().
update_user(Username, PetstoreUser) ->
Method = put,
Host = application:get_env(petstore, host, "http://localhost:8080"),
Path = ["/user/", Username, ""],
Body = PetstoreUser,
ContentType = <<"text/plain">>,
petstore_utils:request(Method, [Host, ?BASE_URL, Path], jsx:encode(Body), ContentType).

View File

@ -0,0 +1,157 @@
-module(petstore_gen).
-compile({no_auto_import,[date/0]}).
-include_lib("proper/include/proper_common.hrl").
%%==============================================================================
%% Exports
%%==============================================================================
-export([ binary/0
, binary/1
, binary/2
, integer/0
, integer/1
, integer/2
, boolean/0
, list/0
, list/1
, list/2
, list/3
, map/0
, date/0
, datetime/0
, any/0
, elements/1
]).
-define(CHARS, [$a, $b, $c]).
%%==============================================================================
%% Generators
%%==============================================================================
binary() -> binary(10).
binary(Min, Max) ->
?LET( {X, N}
, { proper_types:elements(?CHARS)
, proper_types:choose(Min, Max)
}
, iolist_to_binary(lists:duplicate(N, X))
).
binary(N) ->
?LET( X
, proper_types:elements(?CHARS)
, iolist_to_binary(lists:duplicate(N, X))
).
integer() -> proper_types:int().
integer(0) -> proper_types:nat();
integer(Min) ->
?LET( N
, proper_types:nat()
, proper_types:choose(Min, Min + N)
).
integer(Min, Max) -> proper_types:choose(Min, Max).
boolean() -> proper_types:bool().
list() -> list(any()).
list(Type) -> proper_types:list(Type).
list(Type, Min) ->
?LET( N
, integer(0)
, ?LET(X, list(Type, Min, Min + N), X)
).
list(Type, Min, Max) when Min =< Max ->
?LET( {X, Y}
, { proper_types:vector(Min, Type)
, proper_types:resize(Max - Min, proper_types:list(Type))
}
, X ++ Y
).
map() -> proper_types:map(any(), any()).
date() ->
?LET( X
, ?SUCHTHAT( X
, { year()
, proper_types:choose(1, 12)
, proper_types:choose(1, 31)
}
, calendar:valid_date(X)
)
, begin
{Year, Month, Day} = X,
YearBin = num_binary_format(Year, "4"),
MonthBin = num_binary_format(Month, "2"),
DayBin = num_binary_format(Day, "2"),
<<YearBin/binary, "-", MonthBin/binary, "-", DayBin/binary>>
end
).
datetime() ->
Date = date(),
Hour = hour(),
?LET( X
, {Date, Hour}
, begin
{D, H} = X,
<<D/binary, "T", H/binary, "+0000">>
end
).
any() ->
Any = [ binary()
, integer()
, boolean()
%% We don't include lists and maps to avoid huge values
%% , list()
%% , map()
, date()
, datetime()
],
proper_types:oneof(Any).
elements(Items) ->
proper_types:elements(Items).
%%==============================================================================
%% Internal
%%==============================================================================
year() ->
?LET( X
, proper_types:nat()
, 1970 + X
).
hour() ->
?LET( X
, { proper_types:choose(0, 23)
, proper_types:choose(0, 59)
, proper_types:choose(0, 59)
, proper_types:choose(0, 999)
}
, begin
{Hours, Mins, Secs, Millis} = X,
HoursBin = num_binary_format(Hours, "2"),
MinsBin = num_binary_format(Mins, "2"),
SecsBin = num_binary_format(Secs, "2"),
MillisBin = num_binary_format(Millis, "3"),
<<HoursBin/binary, ":", MinsBin/binary, ":",
SecsBin/binary, ".", MillisBin/binary>>
end
).
num_binary_format(X, N) ->
list_to_binary(io_lib:format("~" ++ N ++ "..0B", [X])).

View File

@ -0,0 +1,199 @@
-module(petstore_statem).
-behaviour(proper_statem).
-include("petstore.hrl").
-include_lib("proper/include/proper_common.hrl").
-include_lib("stdlib/include/assert.hrl").
-compile(export_all).
-compile(nowarn_export_all).
%%==============================================================================
%% PropEr callbacks
%%==============================================================================
command(State) ->
Funs0 = [ {F, list_to_atom(atom_to_list(F) ++ "_args")}
|| {F, _} <- ?MODULE:module_info(exports)
],
Funs1 = [ X || {_, FArgs} = X <- Funs0,
erlang:function_exported(?MODULE, FArgs, 1)
],
proper_types:oneof([ {call, ?MODULE, F, ?MODULE:FArgs(State)}
|| {F, FArgs} <- Funs1
]).
precondition(S, {call, M, F, Args}) ->
Pre = list_to_atom(atom_to_list(F) ++ "_pre"),
case erlang:function_exported(M, Pre, 1) of
true -> M:Pre(S);
false -> true
end
andalso
case erlang:function_exported(M, Pre, 2) of
true -> M:Pre(S, Args);
false -> true
end.
next_state(S, Res, {call, M, F, Args}) ->
Next = list_to_atom(atom_to_list(F) ++ "_next"),
case erlang:function_exported(M, Next, 3) of
true -> M:Next(S, Res, Args);
false -> S
end.
postcondition(S, {call, M, F, Args}, Res) ->
Post = list_to_atom(atom_to_list(F) ++ "_post"),
case erlang:function_exported(M, Post, 3) of
true -> M:Post(S, Args, Res);
false -> true
end.
%%==============================================================================
%% create_user
%%==============================================================================
create_user(PetstoreUser) ->
petstore_api:create_user(PetstoreUser).
create_user_args(S) ->
Args = [petstore_user:petstore_user()],
case erlang:function_exported(?MODULE, 'create_user_args_custom', 2) of
true -> ?MODULE:create_user_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% create_users_with_array_input
%%==============================================================================
create_users_with_array_input(PetstoreUserArray) ->
petstore_api:create_users_with_array_input(PetstoreUserArray).
create_users_with_array_input_args(S) ->
Args = [list(petstore_user:petstore_user())],
case erlang:function_exported(?MODULE, 'create_users_with_array_input_args_custom', 2) of
true -> ?MODULE:create_users_with_array_input_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% create_users_with_list_input
%%==============================================================================
create_users_with_list_input(PetstoreUserArray) ->
petstore_api:create_users_with_list_input(PetstoreUserArray).
create_users_with_list_input_args(S) ->
Args = [list(petstore_user:petstore_user())],
case erlang:function_exported(?MODULE, 'create_users_with_list_input_args_custom', 2) of
true -> ?MODULE:create_users_with_list_input_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% delete_user
%%==============================================================================
delete_user(Username) ->
petstore_api:delete_user(Username).
delete_user_args(S) ->
Args = [binary()],
case erlang:function_exported(?MODULE, 'delete_user_args_custom', 2) of
true -> ?MODULE:delete_user_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% get_user_by_name
%%==============================================================================
get_user_by_name(Username) ->
petstore_api:get_user_by_name(Username).
get_user_by_name_args(S) ->
Args = [binary()],
case erlang:function_exported(?MODULE, 'get_user_by_name_args_custom', 2) of
true -> ?MODULE:get_user_by_name_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% login_user
%%==============================================================================
login_user(Username, Password) ->
petstore_api:login_user(Username, Password).
login_user_args(S) ->
Args = [binary(), binary()],
case erlang:function_exported(?MODULE, 'login_user_args_custom', 2) of
true -> ?MODULE:login_user_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% logout_user
%%==============================================================================
logout_user() ->
petstore_api:logout_user().
logout_user_args(S) ->
Args = [],
case erlang:function_exported(?MODULE, 'logout_user_args_custom', 2) of
true -> ?MODULE:logout_user_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% update_user
%%==============================================================================
update_user(Username, PetstoreUser) ->
petstore_api:update_user(Username, PetstoreUser).
update_user_args(S) ->
Args = [binary(), petstore_user:petstore_user()],
case erlang:function_exported(?MODULE, 'update_user_args_custom', 2) of
true -> ?MODULE:update_user_args_custom(S, Args);
false -> Args
end.
%%==============================================================================
%% The statem's property
%%==============================================================================
prop_main() ->
setup(),
?FORALL( Cmds
, proper_statem:commands(?MODULE)
, begin
cleanup(),
{ History
, State
, Result
} = proper_statem:run_commands(?MODULE, Cmds),
?WHENFAIL(
io:format("History: ~p\nState: ~p\nResult: ~p\nCmds: ~p\n",
[ History
, State
, Result
, proper_statem:command_names(Cmds)
]),
proper:aggregate( proper_statem:command_names(Cmds)
, Result =:= ok
)
)
end
).
%%==============================================================================
%% Include file with setup, cleanup, initial_state
%% and state transitions callbacks
%%==============================================================================
-include("petstore_statem.hrl").

View File

@ -0,0 +1,25 @@
%%==============================================================================
%% Setup
%%==============================================================================
setup() -> ok.
%%==============================================================================
%% Cleanup
%%==============================================================================
cleanup() -> ok.
%%==============================================================================
%% Initial State
%%==============================================================================
initial_state() -> #{}.
%%==============================================================================
%% State transitions callbacks
%%
%% operation_pre(State) -> true.
%% operation_next(State, Result, Args) -> State.
%% operation_post(State, Args, Result) -> true.
%%==============================================================================

View File

@ -0,0 +1,66 @@
-module(petstore_utils).
-export([ request/2
, request/4
]).
-type response() :: #{ status := integer()
, headers := map()
, body := iolist()
}.
-export_type([response/0]).
-spec request(atom(), string()) -> response().
request(Method, Url) ->
request(Method, Url, undefined, undefined).
-spec request(atom(), iolist(), iolist(), string()) -> response().
request(Method, Url0, Body, ContentType) ->
Url = binary_to_list(iolist_to_binary(Url0)),
Headers = headers(),
Request = case Body of
undefined -> {Url, Headers};
_ -> {Url, Headers, ContentType, Body}
end,
HTTPOptions = [{autoredirect, true}],
Options = [],
%% Disable pipelining to avoid the socket getting closed during long runs
ok = httpc:set_options([ {max_keep_alive_length, 0}
, {max_pipeline_length, 0}
, {max_sessions, 0}
]),
Result = httpc:request(Method, Request, HTTPOptions, Options),
{ok, {{_Ver, Status, _Phrase}, RespHeaders, RespBody}} = Result,
Response = #{ status => Status
, headers => maps:from_list(RespHeaders)
, body => RespBody
},
decode_body(Response).
-spec headers() -> [{string(), string()}].
headers() ->
[ {"Accept", "application/json"}
| basic_auth()
].
-spec basic_auth() -> [{string(), string()}].
basic_auth() ->
case application:get_env(petstore, basic_auth, undefined) of
undefined -> [];
{Username, Password} ->
Credentials = base64:encode_to_string(Username ++ ":" ++ Password),
[{"Authorization", "Basic " ++ Credentials}]
end.
-spec decode_body(response()) -> response().
decode_body(#{ headers := #{"content-type" := "application/json"}
, body := Body
} = Response) ->
Json = jsx:decode( unicode:characters_to_binary(Body)
, [return_maps, {labels, atom}]
),
Response#{body_json => Json};
decode_body(Response) ->
Response.

View File

@ -0,0 +1,7 @@
-module(prop_petstore).
-export([prop_test/0]).
prop_test() ->
{ok, _} = application:ensure_all_started(petstore),
petstore_statem:prop_main().