diff --git a/bin/erlang-petstore-proper.sh b/bin/erlang-petstore-proper.sh new file mode 100755 index 0000000000..cb6c9b27f0 --- /dev/null +++ b/bin/erlang-petstore-proper.sh @@ -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 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangProperCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangProperCodegen.java new file mode 100644 index 0000000000..f580ea6095 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangProperCodegen.java @@ -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 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 postProcessOperationsWithModels(Map objs, List allModels) { + Map operations = (Map) objs.get("operations"); + List os = (List) operations.get("operation"); + List 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 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; + } + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 1659fc4169..6bd50bc8e2 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -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 diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/README.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/README.mustache new file mode 100644 index 0000000000..cc63f05bdc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/README.mustache @@ -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. diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/api.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/api.mustache new file mode 100644 index 0000000000..7861cfbeb3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/api.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/app.src.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/app.src.mustache new file mode 100644 index 0000000000..0c9bc9c2ae --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/app.src.mustache @@ -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}}]} +]}. diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/gen.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/gen.mustache new file mode 100644 index 0000000000..c7da29fd14 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/gen.mustache @@ -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"), + <> + end + ). + +datetime() -> + Date = date(), + Hour = hour(), + ?LET( X + , {Date, Hour} + , begin + {D, H} = X, + <> + 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"), + <> + end + ). + +num_binary_format(X, N) -> + list_to_binary(io_lib:format("~" ++ N ++ "..0B", [X])). diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/include.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/include.mustache new file mode 100644 index 0000000000..ee07f9f5c3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/include.mustache @@ -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(). diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/model.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/model.mustache new file mode 100644 index 0000000000..5e4024ed8d --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/model.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/rebar.config.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/rebar.config.mustache new file mode 100644 index 0000000000..4a1593adca --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/rebar.config.mustache @@ -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]}. diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/statem.hrl.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/statem.hrl.mustache new file mode 100644 index 0000000000..7b4dd931aa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/statem.hrl.mustache @@ -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. +%%============================================================================== diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/statem.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/statem.mustache new file mode 100644 index 0000000000..98ebdf1c54 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/statem.mustache @@ -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"). diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/test.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/test.mustache new file mode 100644 index 0000000000..ac2d2330b3 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/test.mustache @@ -0,0 +1,7 @@ +-module(prop_{{packageName}}). + +-export([prop_test/0]). + +prop_test() -> + {ok, _} = application:ensure_all_started({{packageName}}), + {{packageName}}_statem:prop_main(). diff --git a/modules/openapi-generator/src/main/resources/erlang-proper/utils.mustache b/modules/openapi-generator/src/main/resources/erlang-proper/utils.mustache new file mode 100644 index 0000000000..f8adf39c54 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/erlang-proper/utils.mustache @@ -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. diff --git a/samples/client/petstore/erlang-proper/.openapi-generator-ignore b/samples/client/petstore/erlang-proper/.openapi-generator-ignore new file mode 100644 index 0000000000..7484ee590a --- /dev/null +++ b/samples/client/petstore/erlang-proper/.openapi-generator-ignore @@ -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 diff --git a/samples/client/petstore/erlang-proper/.openapi-generator/VERSION b/samples/client/petstore/erlang-proper/.openapi-generator/VERSION new file mode 100644 index 0000000000..6d94c9c2e1 --- /dev/null +++ b/samples/client/petstore/erlang-proper/.openapi-generator/VERSION @@ -0,0 +1 @@ +3.3.0-SNAPSHOT \ No newline at end of file diff --git a/samples/client/petstore/erlang-proper/README.md b/samples/client/petstore/erlang-proper/README.md new file mode 100644 index 0000000000..cc63f05bdc --- /dev/null +++ b/samples/client/petstore/erlang-proper/README.md @@ -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. diff --git a/samples/client/petstore/erlang-proper/rebar.config b/samples/client/petstore/erlang-proper/rebar.config new file mode 100644 index 0000000000..0060c9afec --- /dev/null +++ b/samples/client/petstore/erlang-proper/rebar.config @@ -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]}. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_api_response.erl b/samples/client/petstore/erlang-proper/src/model/petstore_api_response.erl new file mode 100644 index 0000000000..495861559a --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_api_response.erl @@ -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() } + ]. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_category.erl b/samples/client/petstore/erlang-proper/src/model/petstore_category.erl new file mode 100644 index 0000000000..d048667c35 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_category.erl @@ -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() } + ]. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_order.erl b/samples/client/petstore/erlang-proper/src/model/petstore_order.erl new file mode 100644 index 0000000000..360e5cf428 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_order.erl @@ -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() } + ]. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_pet.erl b/samples/client/petstore/erlang-proper/src/model/petstore_pet.erl new file mode 100644 index 0000000000..77bbd704fe --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_pet.erl @@ -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">>]) } + ]. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_tag.erl b/samples/client/petstore/erlang-proper/src/model/petstore_tag.erl new file mode 100644 index 0000000000..31bfe4fadb --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_tag.erl @@ -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() } + ]. diff --git a/samples/client/petstore/erlang-proper/src/model/petstore_user.erl b/samples/client/petstore/erlang-proper/src/model/petstore_user.erl new file mode 100644 index 0000000000..df35576aa4 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/model/petstore_user.erl @@ -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() } + ]. diff --git a/samples/client/petstore/erlang-proper/src/petstore.app.src b/samples/client/petstore/erlang-proper/src/petstore.app.src new file mode 100644 index 0000000000..dc56ef9bd8 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore.app.src @@ -0,0 +1,21 @@ +{ application, petstore +, [ {description, "This is a sample server Petstore server. For this sample, you can use the api key `special-key` 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, []} +]}. diff --git a/samples/client/petstore/erlang-proper/src/petstore.hrl b/samples/client/petstore/erlang-proper/src/petstore.hrl new file mode 100644 index 0000000000..d57c511a5a --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore.hrl @@ -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(). diff --git a/samples/client/petstore/erlang-proper/src/petstore_api.erl b/samples/client/petstore/erlang-proper/src/petstore_api.erl new file mode 100644 index 0000000000..fe3fe661b3 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore_api.erl @@ -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). + diff --git a/samples/client/petstore/erlang-proper/src/petstore_gen.erl b/samples/client/petstore/erlang-proper/src/petstore_gen.erl new file mode 100644 index 0000000000..fae40e3567 --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore_gen.erl @@ -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"), + <> + end + ). + +datetime() -> + Date = date(), + Hour = hour(), + ?LET( X + , {Date, Hour} + , begin + {D, H} = X, + <> + 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"), + <> + end + ). + +num_binary_format(X, N) -> + list_to_binary(io_lib:format("~" ++ N ++ "..0B", [X])). diff --git a/samples/client/petstore/erlang-proper/src/petstore_statem.erl b/samples/client/petstore/erlang-proper/src/petstore_statem.erl new file mode 100644 index 0000000000..d7e769cdae --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore_statem.erl @@ -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"). diff --git a/samples/client/petstore/erlang-proper/src/petstore_statem.hrl b/samples/client/petstore/erlang-proper/src/petstore_statem.hrl new file mode 100644 index 0000000000..7b4dd931aa --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore_statem.hrl @@ -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. +%%============================================================================== diff --git a/samples/client/petstore/erlang-proper/src/petstore_utils.erl b/samples/client/petstore/erlang-proper/src/petstore_utils.erl new file mode 100644 index 0000000000..054228c13d --- /dev/null +++ b/samples/client/petstore/erlang-proper/src/petstore_utils.erl @@ -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. diff --git a/samples/client/petstore/erlang-proper/test/prop_petstore.erl b/samples/client/petstore/erlang-proper/test/prop_petstore.erl new file mode 100644 index 0000000000..558e99f1c3 --- /dev/null +++ b/samples/client/petstore/erlang-proper/test/prop_petstore.erl @@ -0,0 +1,7 @@ +-module(prop_petstore). + +-export([prop_test/0]). + +prop_test() -> + {ok, _} = application:ensure_all_started(petstore), + petstore_statem:prop_main().