diff --git a/bin/erlang-petstore-server.sh b/bin/erlang-petstore-server.sh new file mode 100755 index 00000000000..1fcbc2f3bfd --- /dev/null +++ b/bin/erlang-petstore-server.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +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/swagger-codegen-cli/target/swagger-codegen-cli.jar" + +if [ ! -f "$executable" ] +then + mvn 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/swagger-codegen/src/main/resources/erlang-server -i modules/swagger-codegen/src/test/resources/2_0/petstore.yaml -l erlang-server -o samples/server/petstore/erlang-server" + +java $JAVA_OPTS -jar $executable $ags diff --git a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ErlangServerCodegen.java b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ErlangServerCodegen.java index d510b42fac2..65162cdd228 100644 --- a/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ErlangServerCodegen.java +++ b/modules/swagger-codegen/src/main/java/io/swagger/codegen/languages/ErlangServerCodegen.java @@ -163,8 +163,8 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig */ @Override public String getHelp() { - return "Generates an Erlang server library using the swagger-tools project. By default, " + - "it will also generate service classes--which you can disable with the `-Dnoservice` environment variable."; + return "Generates an Erlang server library (beta) using the Swagger Codegen project. By default, " + + "it will also generate service classes, which can be disabled with the `-Dnoservice` environment variable."; } @Override @@ -264,4 +264,17 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig protected String toPrivFilePath(String name, String extension) { return "priv" + File.separator + name + "." + extension; } + + @Override + public String escapeQuotationMark(String input) { + // remove ' to avoid code injection + return input.replace("'", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + // ref: http://stackoverflow.com/a/30421295/677735 + return input.replace("-ifdef", "- if def").replace("-endif", "- end if"); + } + } diff --git a/modules/swagger-codegen/src/main/resources/erlang-server/README.mustache b/modules/swagger-codegen/src/main/resources/erlang-server/README.mustache index d9858d4cc03..af53effabc7 100644 --- a/modules/swagger-codegen/src/main/resources/erlang-server/README.mustache +++ b/modules/swagger-codegen/src/main/resources/erlang-server/README.mustache @@ -1,12 +1,16 @@ # Swagger rest server library for Erlang -## TODO -- [ ] Write specs for static exported fuctions -- [ ] Write specs for generated exported fuctions -- [ ] Add datetime/date validation -- [ ] Separate gigantic `api` to submodules and refactor the routing -- [ ] Add tests for the validators -- [ ] Add integrational test for the whole cycle -- [ ] Add validations of definitions with inheritance -- [ ] Add proper response validation (this `list` hack is so weird) -- [ ] Fix enum validation. It doesn't work correctly when the parans is in qs/header +## Overview + +An Erlang server stub generated by [Swagger Codegen](https://github.com/swagger-api/swagger-codegen) given an OpenAPI/Swagger spec. + +Dependency: [Cowboy](https://github.com/ninenines/cowboy) + +## Prerequisites + +TODO + +## Getting started + +TODO + diff --git a/samples/server/petstore/erlang-server/.swagger-codegen-ignore b/samples/server/petstore/erlang-server/.swagger-codegen-ignore new file mode 100644 index 00000000000..c5fa491b4c5 --- /dev/null +++ b/samples/server/petstore/erlang-server/.swagger-codegen-ignore @@ -0,0 +1,23 @@ +# Swagger Codegen Ignore +# Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen + +# 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 Swagger Codgen 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/server/petstore/erlang-server/LICENSE b/samples/server/petstore/erlang-server/LICENSE new file mode 100644 index 00000000000..8dada3edaf5 --- /dev/null +++ b/samples/server/petstore/erlang-server/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/samples/server/petstore/erlang-server/README.md b/samples/server/petstore/erlang-server/README.md new file mode 100644 index 00000000000..af53effabc7 --- /dev/null +++ b/samples/server/petstore/erlang-server/README.md @@ -0,0 +1,16 @@ +# Swagger rest server library for Erlang + +## Overview + +An Erlang server stub generated by [Swagger Codegen](https://github.com/swagger-api/swagger-codegen) given an OpenAPI/Swagger spec. + +Dependency: [Cowboy](https://github.com/ninenines/cowboy) + +## Prerequisites + +TODO + +## Getting started + +TODO + diff --git a/samples/server/petstore/erlang-server/priv/swagger.json b/samples/server/petstore/erlang-server/priv/swagger.json new file mode 100644 index 00000000000..de3bdd27a50 --- /dev/null +++ b/samples/server/petstore/erlang-server/priv/swagger.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"description":"This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.","version":"1.0.0","title":"Swagger Petstore","termsOfService":"http://swagger.io/terms/","contact":{"email":"apiteam@swagger.io"},"license":{"name":"Apache 2.0","url":"http://www.apache.org/licenses/LICENSE-2.0.html"}},"host":"petstore.swagger.io","basePath":"/v2","tags":[{"name":"pet","description":"Everything about your Pets","externalDocs":{"description":"Find out more","url":"http://swagger.io"}},{"name":"store","description":"Access to Petstore orders"},{"name":"user","description":"Operations about user","externalDocs":{"description":"Find out more about our store","url":"http://swagger.io"}}],"schemes":["http"],"paths":{"/pet":{"post":{"tags":["pet"],"summary":"Add a new pet to the store","description":"","operationId":"addPet","consumes":["application/json","application/xml"],"produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"put":{"tags":["pet"],"summary":"Update an existing pet","description":"","operationId":"updatePet","consumes":["application/json","application/xml"],"produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"Pet object that needs to be added to the store","required":true,"schema":{"$ref":"#/definitions/Pet"}}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"},"405":{"description":"Validation exception"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByStatus":{"get":{"tags":["pet"],"summary":"Finds Pets by status","description":"Multiple status values can be provided with comma separated strings","operationId":"findPetsByStatus","produces":["application/xml","application/json"],"parameters":[{"name":"status","in":"query","description":"Status values that need to be considered for filter","required":true,"type":"array","items":{"type":"string","default":"available","enum":["available","pending","sold"]},"collectionFormat":"csv"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}},"400":{"description":"Invalid status value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/findByTags":{"get":{"tags":["pet"],"summary":"Finds Pets by tags","description":"Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.","operationId":"findPetsByTags","produces":["application/xml","application/json"],"parameters":[{"name":"tags","in":"query","description":"Tags to filter by","required":true,"type":"array","items":{"type":"string"},"collectionFormat":"csv"}],"responses":{"200":{"description":"successful operation","schema":{"type":"array","items":{"$ref":"#/definitions/Pet"}}},"400":{"description":"Invalid tag value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}":{"get":{"tags":["pet"],"summary":"Find pet by ID","description":"Returns a single pet","operationId":"getPetById","produces":["application/xml","application/json"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to return","required":true,"type":"integer","format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Pet"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Pet not found"}},"security":[{"api_key":[]}]},"post":{"tags":["pet"],"summary":"Updates a pet in the store with form data","description":"","operationId":"updatePetWithForm","consumes":["application/x-www-form-urlencoded"],"produces":["application/xml","application/json"],"parameters":[{"name":"petId","in":"path","description":"ID of pet that needs to be updated","required":true,"type":"integer","format":"int64"},{"name":"name","in":"formData","description":"Updated name of the pet","required":false,"type":"string"},{"name":"status","in":"formData","description":"Updated status of the pet","required":false,"type":"string"}],"responses":{"405":{"description":"Invalid input"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]},"delete":{"tags":["pet"],"summary":"Deletes a pet","description":"","operationId":"deletePet","produces":["application/xml","application/json"],"parameters":[{"name":"api_key","in":"header","required":false,"type":"string"},{"name":"petId","in":"path","description":"Pet id to delete","required":true,"type":"integer","format":"int64"}],"responses":{"400":{"description":"Invalid pet value"}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/pet/{petId}/uploadImage":{"post":{"tags":["pet"],"summary":"uploads an image","description":"","operationId":"uploadFile","consumes":["multipart/form-data"],"produces":["application/json"],"parameters":[{"name":"petId","in":"path","description":"ID of pet to update","required":true,"type":"integer","format":"int64"},{"name":"additionalMetadata","in":"formData","description":"Additional data to pass to server","required":false,"type":"string"},{"name":"file","in":"formData","description":"file to upload","required":false,"type":"file"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/ApiResponse"}}},"security":[{"petstore_auth":["write:pets","read:pets"]}]}},"/store/inventory":{"get":{"tags":["store"],"summary":"Returns pet inventories by status","description":"Returns a map of status codes to quantities","operationId":"getInventory","produces":["application/json"],"parameters":[],"responses":{"200":{"description":"successful operation","schema":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}}}},"security":[{"api_key":[]}]}},"/store/order":{"post":{"tags":["store"],"summary":"Place an order for a pet","description":"","operationId":"placeOrder","produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"order placed for purchasing the pet","required":true,"schema":{"$ref":"#/definitions/Order"}}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid Order"}}}},"/store/order/{orderId}":{"get":{"tags":["store"],"summary":"Find purchase order by ID","description":"For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions","operationId":"getOrderById","produces":["application/xml","application/json"],"parameters":[{"name":"orderId","in":"path","description":"ID of pet that needs to be fetched","required":true,"type":"integer","maximum":5.0,"minimum":1.0,"format":"int64"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/Order"}},"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}},"delete":{"tags":["store"],"summary":"Delete purchase order by ID","description":"For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors","operationId":"deleteOrder","produces":["application/xml","application/json"],"parameters":[{"name":"orderId","in":"path","description":"ID of the order that needs to be deleted","required":true,"type":"string","minimum":1.0}],"responses":{"400":{"description":"Invalid ID supplied"},"404":{"description":"Order not found"}}}},"/user":{"post":{"tags":["user"],"summary":"Create user","description":"This can only be done by the logged in user.","operationId":"createUser","produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"Created user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"default":{"description":"successful operation"}}}},"/user/createWithArray":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithArrayInput","produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}}},"/user/createWithList":{"post":{"tags":["user"],"summary":"Creates list of users with given input array","description":"","operationId":"createUsersWithListInput","produces":["application/xml","application/json"],"parameters":[{"in":"body","name":"body","description":"List of user object","required":true,"schema":{"type":"array","items":{"$ref":"#/definitions/User"}}}],"responses":{"default":{"description":"successful operation"}}}},"/user/login":{"get":{"tags":["user"],"summary":"Logs user into the system","description":"","operationId":"loginUser","produces":["application/xml","application/json"],"parameters":[{"name":"username","in":"query","description":"The user name for login","required":true,"type":"string"},{"name":"password","in":"query","description":"The password for login in clear text","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"type":"string"},"headers":{"X-Rate-Limit":{"type":"integer","format":"int32","description":"calls per hour allowed by the user"},"X-Expires-After":{"type":"string","format":"date-time","description":"date in UTC when toekn expires"}}},"400":{"description":"Invalid username/password supplied"}}}},"/user/logout":{"get":{"tags":["user"],"summary":"Logs out current logged in user session","description":"","operationId":"logoutUser","produces":["application/xml","application/json"],"parameters":[],"responses":{"default":{"description":"successful operation"}}}},"/user/{username}":{"get":{"tags":["user"],"summary":"Get user by user name","description":"","operationId":"getUserByName","produces":["application/xml","application/json"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be fetched. Use user1 for testing. ","required":true,"type":"string"}],"responses":{"200":{"description":"successful operation","schema":{"$ref":"#/definitions/User"}},"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}},"put":{"tags":["user"],"summary":"Updated user","description":"This can only be done by the logged in user.","operationId":"updateUser","produces":["application/xml","application/json"],"parameters":[{"name":"username","in":"path","description":"name that need to be deleted","required":true,"type":"string"},{"in":"body","name":"body","description":"Updated user object","required":true,"schema":{"$ref":"#/definitions/User"}}],"responses":{"400":{"description":"Invalid user supplied"},"404":{"description":"User not found"}}},"delete":{"tags":["user"],"summary":"Delete user","description":"This can only be done by the logged in user.","operationId":"deleteUser","produces":["application/xml","application/json"],"parameters":[{"name":"username","in":"path","description":"The name that needs to be deleted","required":true,"type":"string"}],"responses":{"400":{"description":"Invalid username supplied"},"404":{"description":"User not found"}}}}},"securityDefinitions":{"api_key":{"type":"apiKey","name":"api_key","in":"header"},"petstore_auth":{"type":"oauth2","authorizationUrl":"http://petstore.swagger.io/api/oauth/dialog","flow":"implicit","scopes":{"write:pets":"modify pets in your account","read:pets":"read your pets"}}},"definitions":{"Order":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"petId":{"type":"integer","format":"int64"},"quantity":{"type":"integer","format":"int32"},"shipDate":{"type":"string","format":"date-time"},"status":{"type":"string","description":"Order Status","enum":["placed","approved","delivered"]},"complete":{"type":"boolean","default":false}},"xml":{"name":"Order"}},"Category":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Category"}},"User":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"phone":{"type":"string"},"userStatus":{"type":"integer","format":"int32","description":"User Status"}},"xml":{"name":"User"}},"Tag":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"}},"Pet":{"type":"object","required":["name","photoUrls"],"properties":{"id":{"type":"integer","format":"int64"},"category":{"$ref":"#/definitions/Category"},"name":{"type":"string","example":"doggie"},"photoUrls":{"type":"array","xml":{"name":"photoUrl","wrapped":true},"items":{"type":"string"}},"tags":{"type":"array","xml":{"name":"tag","wrapped":true},"items":{"$ref":"#/definitions/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"Pet"}},"ApiResponse":{"type":"object","properties":{"code":{"type":"integer","format":"int32"},"type":{"type":"string"},"message":{"type":"string"}}}},"externalDocs":{"description":"Find out more about Swagger","url":"http://swagger.io"}} diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config new file mode 100644 index 00000000000..1c7f7d922e9 --- /dev/null +++ b/samples/server/petstore/erlang-server/rebar.config @@ -0,0 +1,4 @@ +{deps, [ + {jsx, {git, "https://github.com/talentdeficit/jsx.git", {branch, "v2.8.0"}}}, + {jesse, {git, "https://github.com/for-GET/jesse.git", {tag, "1.4.0"}}} +]}. diff --git a/samples/server/petstore/erlang-server/src/swagger.app.src b/samples/server/petstore/erlang-server/src/swagger.app.src new file mode 100644 index 00000000000..9016bde7219 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger.app.src @@ -0,0 +1,19 @@ +{application, swagger, [ + {description, "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters."}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + ssl, + inets, + jsx, + jesse, + cowboy + ]}, + {env, [ + ]}, + {modules, []}, + {licenses, ["Apache 2.0"]}, + {links, []} +]}. diff --git a/samples/server/petstore/erlang-server/src/swagger_api.erl b/samples/server/petstore/erlang-server/src/swagger_api.erl new file mode 100644 index 00000000000..b3020fbbf69 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_api.erl @@ -0,0 +1,733 @@ +-module(swagger_api). + +-export([request_params/1]). +-export([request_param_info/2]). +-export([populate_request/3]). +-export([validate_response/4]). + +-type operation_id() :: atom(). +-type request_param() :: atom(). + +-export_type([operation_id/0]). + +-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. + + +request_params('AddPet') -> + [ + 'Pet' + ]; + +request_params('DeletePet') -> + [ + 'petId', + 'api_key' + ]; + +request_params('FindPetsByStatus') -> + [ + 'status' + ]; + +request_params('FindPetsByTags') -> + [ + 'tags' + ]; + +request_params('GetPetById') -> + [ + 'petId' + ]; + +request_params('UpdatePet') -> + [ + 'Pet' + ]; + +request_params('UpdatePetWithForm') -> + [ + 'petId', + 'name', + 'status' + ]; + +request_params('UploadFile') -> + [ + 'petId', + 'additionalMetadata', + 'file' + ]; + + +request_params('DeleteOrder') -> + [ + 'orderId' + ]; + +request_params('GetInventory') -> + [ + ]; + +request_params('GetOrderById') -> + [ + 'orderId' + ]; + +request_params('PlaceOrder') -> + [ + 'Order' + ]; + + +request_params('CreateUser') -> + [ + 'User' + ]; + +request_params('CreateUsersWithArrayInput') -> + [ + 'list' + ]; + +request_params('CreateUsersWithListInput') -> + [ + 'list' + ]; + +request_params('DeleteUser') -> + [ + 'username' + ]; + +request_params('GetUserByName') -> + [ + 'username' + ]; + +request_params('LoginUser') -> + [ + 'username', + 'password' + ]; + +request_params('LogoutUser') -> + [ + ]; + +request_params('UpdateUser') -> + [ + 'username', + 'User' + ]; + +request_params(_) -> + error(unknown_operation). + +-type rule() :: + {type, 'binary'} | + {type, 'integer'} | + {type, 'float'} | + {type, 'binary'} | + {type, 'boolean'} | + {type, 'date'} | + {type, 'datetime'} | + {enum, [atom()]} | + {max, Max :: number()} | + {exclusive_max, Max :: number()} | + {min, Min :: number()} | + {exclusive_min, Min :: number()} | + {max_length, MaxLength :: integer()} | + {min_length, MaxLength :: integer()} | + {pattern, Pattern :: string()} | + schema | + required | + not_required. + +-spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> #{ + source => qs_val | binding | header | body, + rules => [rule()] +}. + + + +request_param_info('AddPet', 'Pet') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info('DeletePet', 'petId') -> + #{ + source => binding , + rules => [ + {type, 'integer'}, + required + ] + }; + +request_param_info('DeletePet', 'api_key') -> + #{ + source => header, + rules => [ + {type, 'binary'}, + not_required + ] + }; + +request_param_info('FindPetsByStatus', 'status') -> + #{ + source => qs_val , + rules => [ + {enum, ['available', 'pending', 'sold'] }, + required + ] + }; + +request_param_info('FindPetsByTags', 'tags') -> + #{ + source => qs_val , + rules => [ + required + ] + }; + +request_param_info('GetPetById', 'petId') -> + #{ + source => binding , + rules => [ + {type, 'integer'}, + required + ] + }; + +request_param_info('UpdatePet', 'Pet') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info('UpdatePetWithForm', 'petId') -> + #{ + source => binding , + rules => [ + {type, 'integer'}, + required + ] + }; + +request_param_info('UpdatePetWithForm', 'name') -> + #{ + source => , + rules => [ + {type, 'binary'}, + not_required + ] + }; + +request_param_info('UpdatePetWithForm', 'status') -> + #{ + source => , + rules => [ + {type, 'binary'}, + not_required + ] + }; + +request_param_info('UploadFile', 'petId') -> + #{ + source => binding , + rules => [ + {type, 'integer'}, + required + ] + }; + +request_param_info('UploadFile', 'additionalMetadata') -> + #{ + source => , + rules => [ + {type, 'binary'}, + not_required + ] + }; + +request_param_info('UploadFile', 'file') -> + #{ + source => , + rules => [ + not_required + ] + }; + + +request_param_info('DeleteOrder', 'orderId') -> + #{ + source => binding , + rules => [ + {type, 'binary'}, + {min, 1.0 }, + required + ] + }; + +request_param_info('GetOrderById', 'orderId') -> + #{ + source => binding , + rules => [ + {type, 'integer'}, + {max, 5.0 }, + {min, 1.0 }, + required + ] + }; + +request_param_info('PlaceOrder', 'Order') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + + +request_param_info('CreateUser', 'User') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info('CreateUsersWithArrayInput', 'list') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info('CreateUsersWithListInput', 'list') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info('DeleteUser', 'username') -> + #{ + source => binding , + rules => [ + {type, 'binary'}, + required + ] + }; + +request_param_info('GetUserByName', 'username') -> + #{ + source => binding , + rules => [ + {type, 'binary'}, + required + ] + }; + +request_param_info('LoginUser', 'username') -> + #{ + source => qs_val , + rules => [ + {type, 'binary'}, + required + ] + }; + +request_param_info('LoginUser', 'password') -> + #{ + source => qs_val , + rules => [ + {type, 'binary'}, + required + ] + }; + +request_param_info('UpdateUser', 'username') -> + #{ + source => binding , + rules => [ + {type, 'binary'}, + required + ] + }; + +request_param_info('UpdateUser', 'User') -> + #{ + source => body, + rules => [ + schema, + required + ] + }; + +request_param_info(OperationID, Name) -> + error({unknown_param, OperationID, Name}). + +-spec populate_request( + OperationID :: operation_id(), + Req :: cowboy_req:req(), + ValidatorState :: jesse_state:state() +) -> + {ok, Model :: #{}, Req :: cowboy_req:req()} | + {error, Reason :: any(), Req :: cowboy_req:req()}. + +populate_request(OperationID, Req, ValidatorState) -> + Params = request_params(OperationID), + populate_request_params(OperationID, Params, Req, ValidatorState, #{}). + +populate_request_params(_, [], Req, _, Model) -> + {ok, Model, Req}; + +populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) -> + case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of + {ok, K, V, Req} -> + populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model)); + Error -> + Error + end. + +populate_request_param(OperationID, Name, Req0, ValidatorState) -> + #{rules := Rules, source := Source} = request_param_info(OperationID, Name), + {Value, Req} = get_value(Source, Name, Req0), + case prepare_param(Rules, Name, Value, ValidatorState) of + {ok, Result} -> {ok, Name, Result, Req}; + {error, Reason} -> + {error, Reason, Req} + end. + +-spec validate_response( + OperationID :: operation_id(), + Code :: 200..599, + Body :: jesse:json_term(), + ValidatorState :: jesse_state:state() +) -> ok | no_return(). + + +validate_response('AddPet', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('DeletePet', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('FindPetsByStatus', 200, Body, ValidatorState) -> + validate_response_body('list', 'Pet', Body, ValidatorState); +validate_response('FindPetsByStatus', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('FindPetsByTags', 200, Body, ValidatorState) -> + validate_response_body('list', 'Pet', Body, ValidatorState); +validate_response('FindPetsByTags', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('GetPetById', 200, Body, ValidatorState) -> + validate_response_body('Pet', 'Pet', Body, ValidatorState); +validate_response('GetPetById', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetPetById', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('UpdatePet', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePet', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePet', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('UpdatePetWithForm', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('UploadFile', 200, Body, ValidatorState) -> + validate_response_body('ApiResponse', 'ApiResponse', Body, ValidatorState); + + +validate_response('DeleteOrder', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeleteOrder', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('GetInventory', 200, Body, ValidatorState) -> + validate_response_body('map', 'integer', Body, ValidatorState); + +validate_response('GetOrderById', 200, Body, ValidatorState) -> + validate_response_body('Order', 'Order', Body, ValidatorState); +validate_response('GetOrderById', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetOrderById', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('PlaceOrder', 200, Body, ValidatorState) -> + validate_response_body('Order', 'Order', Body, ValidatorState); +validate_response('PlaceOrder', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + + +validate_response('CreateUser', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('CreateUsersWithArrayInput', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('CreateUsersWithListInput', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('DeleteUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeleteUser', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('GetUserByName', 200, Body, ValidatorState) -> + validate_response_body('User', 'User', Body, ValidatorState); +validate_response('GetUserByName', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetUserByName', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('LoginUser', 200, Body, ValidatorState) -> + validate_response_body('binary', 'string', Body, ValidatorState); +validate_response('LoginUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('LogoutUser', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + +validate_response('UpdateUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdateUser', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); + + +validate_response(_OperationID, _Code, _Body, _ValidatorState) -> + ok. + +validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> + [ + validate(schema, ReturnBaseType, Item, ValidatorState) + || Item <- Body]; + +validate_response_body(_, ReturnBaseType, Body, ValidatorState) -> + validate(schema, ReturnBaseType, Body, ValidatorState). + +%%% +validate(Rule = required, Name, Value, _ValidatorState) -> + case Value of + undefined -> validation_error(Rule, Name); + _ -> ok + end; + +validate(not_required, _Name, _Value, _ValidatorState) -> + ok; + +validate(_, _Name, undefined, _ValidatorState) -> + ok; + +validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) -> + try + {ok, swagger_utils:to_int(Value)} + catch + error:badarg -> + validation_error(Rule, Name) + end; + +validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> + try + {ok, swagger_utils:to_float(Value)} + catch + error:badarg -> + validation_error(Rule, Name) + end; + +validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(_Rule = {type, 'boolean'}, _Name, Value, _ValidatorState) when is_boolean(Value) -> + {ok, Value}; + +validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> + V = binary_to_lower(Value), + try + case binary_to_existing_atom(V, utf8) of + B when is_boolean(B) -> {ok, B}; + _ -> validation_error(Rule, Name) + end + catch + error:badarg -> + validation_error(Rule, Name) + end; + +validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {type, 'datetime'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {enum, Values}, Name, Value, _ValidatorState) -> + try + FormattedValue = erlang:binary_to_existing_atom(Value, utf8), + case lists:member(FormattedValue, Values) of + true -> {ok, FormattedValue}; + false -> validation_error(Rule, Name) + end + catch + error:badarg -> + validation_error(Rule, Name) + end; + +validate(Rule = {max, Max}, Name, Value, _ValidatorState) -> + case Value >= Max of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) -> + case Value > ExclusiveMax of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {min, Min}, Name, Value, _ValidatorState) -> + case Value =< Min of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) -> + case Value =< ExclusiveMin of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) -> + case size(Value) =< MaxLength of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) -> + case size(Value) >= MinLength of + true -> ok; + false -> validation_error(Rule, Name) + end; + +validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> + {ok, MP} = re:compile(Pattern), + case re:run(Value, MP) of + {match, _} -> ok; + _ -> validation_error(Rule, Name) + end; + +validate(Rule = schema, Name, Value, ValidatorState) -> + Definition = list_to_binary("#/definitions/" ++ swagger_utils:to_list(Name)), + try + _ = validate_with_schema(Value, Definition, ValidatorState), + ok + catch + throw:[{schema_invalid, _, Error} | _] -> + Info = #{ + type => schema_invalid, + error => Error + }, + validation_error(Rule, Name, Info); + throw:[{data_invalid, Schema, Error, _, Path} | _] -> + Info = #{ + type => data_invalid, + error => Error, + schema => Schema, + path => Path + }, + validation_error(Rule, Name, Info) + end; + +validate(Rule, Name, _Value, _ValidatorState) -> + error_logger:info_msg("Can't validate ~p with ~p", [Name, Rule]), + error({unknown_validation_rule, Rule}). + +-spec validation_error(Rule :: any(), Name :: any()) -> no_return(). + +validation_error(ViolatedRule, Name) -> + validation_error(ViolatedRule, Name, #{}). + +-spec validation_error(Rule :: any(), Name :: any(), Info :: #{}) -> no_return(). + +validation_error(ViolatedRule, Name, Info) -> + throw({wrong_param, Name, ViolatedRule, Info}). + +get_value(body, _Name, Req0) -> + {ok, Body, Req} = cowboy_req:body(Req0), + Value = prepare_body(Body), + {Value, Req}; + +get_value(qs_val, Name, Req0) -> + {QS, Req} = cowboy_req:qs_vals(Req0), + Value = swagger_utils:get_opt(swagger_utils:to_qs(Name), QS), + {Value, Req}; + +get_value(header, Name, Req0) -> + {Headers, Req} = cowboy_req:headers(Req0), + Value = swagger_utils:get_opt(swagger_utils:to_header(Name), Headers), + {Value, Req}; + +get_value(binding, Name, Req0) -> + {Bindings, Req} = cowboy_req:bindings(Req0), + Value = swagger_utils:get_opt(swagger_utils:to_binding(Name), Bindings), + {Value, Req}. + +prepare_body(Body) -> + case Body of + <<"">> -> <<"">>; + _ -> jsx:decode(Body, [return_maps]) + end. + +validate_with_schema(Body, Definition, ValidatorState) -> + jesse_schema_validator:validate_with_state( + [{<<"$ref">>, Definition}], + Body, + ValidatorState + ). + +prepare_param(Rules, Name, Value, ValidatorState) -> + try + Result = lists:foldl( + fun(Rule, Acc) -> + case validate(Rule, Name, Acc, ValidatorState) of + ok -> Acc; + {ok, Prepared} -> Prepared + end + end, + Value, + Rules + ), + {ok, Result} + catch + throw:Reason -> + {error, Reason} + end. + +binary_to_lower(V) when is_binary(V) -> + list_to_binary(string:to_lower(swagger_utils:to_list(V))). diff --git a/samples/server/petstore/erlang-server/src/swagger_auth.erl b/samples/server/petstore/erlang-server/src/swagger_auth.erl new file mode 100644 index 00000000000..275ad19f38f --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_auth.erl @@ -0,0 +1,50 @@ +-module(swagger_auth). + +-export([authorize_api_key/5]). + +-spec authorize_api_key( + LogicHandler :: atom(), + OperationID :: swagger_api:operation_id(), + From :: header | qs_val, + KeyParam :: iodata() | atom(), + Req ::cowboy_req:req() +)-> {true, Context :: #{binary() => any()}, Req ::cowboy_req:req()} | + {false, AuthHeader :: binary(), Req ::cowboy_req:req()}. + +authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) -> + {ApiKey, Req} = get_api_key(From, KeyParam, Req0), + case ApiKey of + undefined -> + AuthHeader = <<"">>, + {false, AuthHeader, Req}; + _ -> + Result = swagger_logic_handler:authorize_api_key( + LogicHandler, + OperationID, + ApiKey + ), + case Result of + {true, Context} -> + {true, Context, Req}; + false -> + AuthHeader = <<"">>, + {false, AuthHeader, Req} + end + end. + +get_api_key(header, KeyParam, Req0) -> + {Headers, Req} = cowboy_req:headers(Req0), + { + swagger_utils:get_opt( + swagger_utils:to_header(KeyParam), + Headers + ), + Req + }; + +get_api_key(qs_val, KeyParam, Req0) -> + {QS, Req} = cowboy_req:qs_vals(Req0), + { swagger_utils:get_opt(KeyParam, QS), Req}. + + + diff --git a/samples/server/petstore/erlang-server/src/swagger_default_logic_handler.erl b/samples/server/petstore/erlang-server/src/swagger_default_logic_handler.erl new file mode 100644 index 00000000000..b68b8c00cdb --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_default_logic_handler.erl @@ -0,0 +1,24 @@ +-module(swagger_default_logic_handler). + +-behaviour(swagger_logic_handler). + +-export([handle_request/3]). +-export([authorize_api_key/2]). + +-spec authorize_api_key(OperationID :: swagger_api:operation_id(), ApiKey :: binary()) -> {true, #{}}. + +authorize_api_key(_, _) -> {true, #{}}. + +-spec handle_request( + OperationID :: swagger_api:operation_id(), + Req :: cowboy_req:req(), + Context :: #{} +) -> + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: #{}}. + +handle_request(OperationID, Req, Context) -> + error_logger:error_msg( + "Got not implemented request to process: ~p~n", + [{OperationID, Req, Context}] + ), + {501, [], #{}}. diff --git a/samples/server/petstore/erlang-server/src/swagger_logic_handler.erl b/samples/server/petstore/erlang-server/src/swagger_logic_handler.erl new file mode 100644 index 00000000000..4ff40c85ec6 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_logic_handler.erl @@ -0,0 +1,38 @@ +-module(swagger_logic_handler). + +-export([handle_request/4]). +-export([authorize_api_key/3]). +-type context() :: #{binary() => any()}. +-type handler_response() ::{ + Status :: cowboy:http_status(), + Headers :: cowboy:http_headers(), + Body :: #{} +}. + +-export_type([handler_response/0]). + +-callback authorize_api_key( + OperationID :: swagger_api:operation_id(), + ApiKey :: binary() +) -> + Result :: boolean() | {boolean(), context()}. + + +-callback handle_request(OperationID :: swagger_api:operation_id(), Request :: any(), Context :: context()) -> + handler_response(). + +-spec handle_request( + Handler :: atom(), + OperationID :: swagger_api:operation_id(), + Request :: any(), + Context :: context() +) -> + handler_response(). + +handle_request(Handler, OperationID, Req, Context) -> + Handler:handle_request(OperationID, Req, Context). + +-spec authorize_api_key(Handler :: atom(), OperationID :: swagger_api:operation_id(), ApiKey :: binary()) -> + Result :: false | {true, context()}. +authorize_api_key(Handler, OperationID, ApiKey) -> + Handler:authorize_api_key(OperationID, ApiKey). diff --git a/samples/server/petstore/erlang-server/src/swagger_pet_handler.erl b/samples/server/petstore/erlang-server/src/swagger_pet_handler.erl new file mode 100644 index 00000000000..4351da7da6c --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_pet_handler.erl @@ -0,0 +1,408 @@ +%% basic handler +-module(swagger_pet_handler). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/3]). +-export([rest_init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +%% Handlers +-export([handle_request_json/2]). + +-record(state, { + operation_id :: swagger_api:operation_id(), + logic_handler :: atom(), + validator_state :: jesse_state:state(), + context=#{} :: #{} +}). + +-type state() :: state(). + +-spec init(TransportName :: atom(), Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {upgrade, protocol, cowboy_rest, Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()}. + +init(_Transport, Req, Opts) -> + {upgrade, protocol, cowboy_rest, Req, Opts}. + +-spec rest_init(Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {ok, Req :: cowboy_req:req(), State :: state()}. + +rest_init(Req0, {Operations, LogicHandler, ValidatorState}) -> + {Method, Req} = cowboy_req:method(Req0), + OperationID = maps:get(Method, Operations, undefined), + + error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {ok, Req, State}. + +-spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> + {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. + + +allowed_methods( + Req, + State = #state{ + operation_id = 'AddPet' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'DeletePet' + } +) -> + {[<<"DELETE">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'FindPetsByStatus' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'FindPetsByTags' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'GetPetById' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'UpdatePet' + } +) -> + {[<<"PUT">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'UpdatePetWithForm' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'UploadFile' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State) -> + {[], Req, State}. + +-spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: true | {false, AuthHeader :: iodata()}, + Req :: cowboy_req:req(), + State :: state() + }. + +is_authorized( + Req0, + State = #state{ + operation_id = 'AddPet' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'DeletePet' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'FindPetsByStatus' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'FindPetsByTags' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'GetPetById' = OperationID, + logic_handler = LogicHandler + } +) -> + From = header, + Result = swagger_auth:authorize_api_key( + LogicHandler, + OperationID, + From, + "api_key", + Req0 + ), + case Result of + {true, Context, Req} -> {true, Req, State#state{context = Context}}; + {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + end; + +is_authorized( + Req0, + State = #state{ + operation_id = 'UpdatePet' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'UpdatePetWithForm' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'UploadFile' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized(Req, State) -> + {{false, <<"">>}, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'AddPet' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'DeletePet' + } +) -> + Headers = ["api_key"], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'FindPetsByStatus' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'FindPetsByTags' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'GetPetById' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'UpdatePet' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'UpdatePetWithForm' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'UploadFile' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req, State) -> + {false, Req, State}. + +-spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), ProvideResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_provided(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). + +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% + +-type result_ok() :: { + ok, + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} +}. + +-type result_error() :: {error, Reason :: any()}. + +-type processed_response() :: {halt, cowboy_req:req(), state()}. + +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). + +process_response(Response, Req0, State = #state{operation_id = OperationID}) -> + case Response of + {ok, {Code, Headers, Body}} -> + {ok, Req} = cowboy_req:reply(Code, Headers, Body, Req0), + {halt, Req, State}; + {error, Message} -> + error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), + + {ok, Req} = cowboy_req:reply(400, Req0), + {halt, Req, State} + end. + +-spec handle_request_json(cowboy_req:req(), state()) -> {halt, cowboy_req:req(), state()}. + +handle_request_json( + Req0, + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState, + context = Context + } +) -> + case swagger_api:populate_request(OperationID, Req0, ValidatorState) of + {ok, Populated, Req1} -> + {Code, Headers, Body} = swagger_logic_handler:handle_request( + LogicHandler, + OperationID, + Populated, + Context + ), + _ = swagger_api:validate_response( + OperationID, + Code, + Body, + ValidatorState + ), + PreparedBody = jsx:encode(Body), + Response = {ok, {Code, Headers, PreparedBody}}, + process_response(Response, Req1, State); + {error, Reason, Req1} -> + process_response({error, Reason}, Req1, State) + end. + +validate_headers(_, Req) -> {true, Req}. diff --git a/samples/server/petstore/erlang-server/src/swagger_router.erl b/samples/server/petstore/erlang-server/src/swagger_router.erl new file mode 100644 index 00000000000..7a1b004a5f2 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_router.erl @@ -0,0 +1,169 @@ +-module(swagger_router). + +-export([get_paths/1]). + +-type operations() :: #{ + Method :: binary() => swagger_api:operation_id() +}. + +-type init_opts() :: { + Operations :: operations(), + LogicHandler :: atom(), + ValidatorState :: jesse_state:state() +}. + +-export_type([init_opts/0]). + +-spec get_paths(LogicHandler :: atom()) -> [{'_',[{ + Path :: string(), + Handler :: atom(), + InitOpts :: init_opts() +}]}]. + +get_paths(LogicHandler) -> + ValidatorState = prepare_validator(), + PreparedPaths = maps:fold( + fun(Path, #{operations := Operations, handler := Handler}, Acc) -> + [{Path, Handler, Operations} | Acc] + end, + [], + group_paths() + ), + [ + {'_', + [{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths] + } + ]. + +group_paths() -> + maps:fold( + fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) -> + case maps:find(Path, Acc) of + {ok, PathInfo0 = #{operations := Operations0}} -> + Operations = Operations0#{Method => OperationID}, + PathInfo = PathInfo0#{operations => Operations}, + Acc#{Path => PathInfo}; + error -> + Operations = #{Method => OperationID}, + PathInfo = #{handler => Handler, operations => Operations}, + Acc#{Path => PathInfo} + end + end, + #{}, + get_operations() + ). + +get_operations() -> + #{ + 'AddPet' => #{ + path => "/v2/pet", + method => <<"POST">>, + handler => 'swagger_pet_handler' + }, + 'DeletePet' => #{ + path => "/v2/pet/:petId", + method => <<"DELETE">>, + handler => 'swagger_pet_handler' + }, + 'FindPetsByStatus' => #{ + path => "/v2/pet/findByStatus", + method => <<"GET">>, + handler => 'swagger_pet_handler' + }, + 'FindPetsByTags' => #{ + path => "/v2/pet/findByTags", + method => <<"GET">>, + handler => 'swagger_pet_handler' + }, + 'GetPetById' => #{ + path => "/v2/pet/:petId", + method => <<"GET">>, + handler => 'swagger_pet_handler' + }, + 'UpdatePet' => #{ + path => "/v2/pet", + method => <<"PUT">>, + handler => 'swagger_pet_handler' + }, + 'UpdatePetWithForm' => #{ + path => "/v2/pet/:petId", + method => <<"POST">>, + handler => 'swagger_pet_handler' + }, + 'UploadFile' => #{ + path => "/v2/pet/:petId/uploadImage", + method => <<"POST">>, + handler => 'swagger_pet_handler' + }, + 'DeleteOrder' => #{ + path => "/v2/store/order/:orderId", + method => <<"DELETE">>, + handler => 'swagger_store_handler' + }, + 'GetInventory' => #{ + path => "/v2/store/inventory", + method => <<"GET">>, + handler => 'swagger_store_handler' + }, + 'GetOrderById' => #{ + path => "/v2/store/order/:orderId", + method => <<"GET">>, + handler => 'swagger_store_handler' + }, + 'PlaceOrder' => #{ + path => "/v2/store/order", + method => <<"POST">>, + handler => 'swagger_store_handler' + }, + 'CreateUser' => #{ + path => "/v2/user", + method => <<"POST">>, + handler => 'swagger_user_handler' + }, + 'CreateUsersWithArrayInput' => #{ + path => "/v2/user/createWithArray", + method => <<"POST">>, + handler => 'swagger_user_handler' + }, + 'CreateUsersWithListInput' => #{ + path => "/v2/user/createWithList", + method => <<"POST">>, + handler => 'swagger_user_handler' + }, + 'DeleteUser' => #{ + path => "/v2/user/:username", + method => <<"DELETE">>, + handler => 'swagger_user_handler' + }, + 'GetUserByName' => #{ + path => "/v2/user/:username", + method => <<"GET">>, + handler => 'swagger_user_handler' + }, + 'LoginUser' => #{ + path => "/v2/user/login", + method => <<"GET">>, + handler => 'swagger_user_handler' + }, + 'LogoutUser' => #{ + path => "/v2/user/logout", + method => <<"GET">>, + handler => 'swagger_user_handler' + }, + 'UpdateUser' => #{ + path => "/v2/user/:username", + method => <<"PUT">>, + handler => 'swagger_user_handler' + } + }. + +prepare_validator() -> + R = jsx:decode(element(2, file:read_file(get_swagger_path()))), + jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]). + + +get_swagger_path() -> + {ok, AppName} = application:get_application(?MODULE), + filename:join(swagger_utils:priv_dir(AppName), "swagger.json"). + + diff --git a/samples/server/petstore/erlang-server/src/swagger_server.erl b/samples/server/petstore/erlang-server/src/swagger_server.erl new file mode 100644 index 00000000000..4f56a326034 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_server.erl @@ -0,0 +1,64 @@ +-module(swagger_server). + + +-define(DEFAULT_ACCEPTORS_POOLSIZE, 100). +-define(DEFAULT_LOGIC_HANDLER, swagger_default_logic_handler). + +-export([child_spec/2]). + +-spec child_spec( ID :: any(), #{ + ip => inet:ip_address(), + port => inet:port_number(), + net_opts => [] +}) -> supervisor:child_spec(). + +child_spec(ID, #{ + ip := IP , + port := Port, + net_opts := NetOpts +} = Params) -> + AcceptorsPool = ?DEFAULT_ACCEPTORS_POOLSIZE, + {Transport, TransportOpts} = get_socket_transport(IP, Port, NetOpts), + LogicHandler = maps:get(logic_handler, Params, ?DEFAULT_LOGIC_HANDLER), + ExtraOpts = maps:get(cowboy_extra_opts, Params, []), + CowboyOpts = get_cowboy_config(LogicHandler, ExtraOpts), + ranch:child_spec({?MODULE, ID}, AcceptorsPool, + Transport, TransportOpts, cowboy_protocol, CowboyOpts). + +get_socket_transport(IP, Port, Options) -> + Opts = [ + {ip, IP}, + {port, Port} + ], + case swagger_utils:get_opt(ssl, Options) of + SslOpts = [_|_] -> + {ranch_ssl, Opts ++ SslOpts}; + undefined -> + {ranch_tcp, Opts} + end. + +get_cowboy_config(LogicHandler, ExtraOpts) -> + get_cowboy_config(LogicHandler, ExtraOpts, get_default_opts(LogicHandler)). + +get_cowboy_config(_LogicHandler, [], Opts) -> + Opts; + +get_cowboy_config(LogicHandler, [{env, Env} | Rest], Opts) -> + NewEnv = case proplists:get_value(dispatch, Env) of + undefined -> [get_default_dispatch(LogicHandler) | Env]; + _ -> Env + end, + get_cowboy_config(LogicHandler, Rest, store_key(env, NewEnv, Opts)); + +get_cowboy_config(LogicHandler, [{Key, Value}| Rest], Opts) -> + get_cowboy_config(LogicHandler, Rest, store_key(Key, Value, Opts)). + +get_default_dispatch(LogicHandler) -> + Paths = swagger_router:get_paths(LogicHandler), + {dispatch, cowboy_router:compile(Paths)}. + +get_default_opts(LogicHandler) -> + [{env, [get_default_dispatch(LogicHandler)]}]. + +store_key(Key, Value, Opts) -> + lists:keystore(Key, 1, Opts, {Key, Value}). diff --git a/samples/server/petstore/erlang-server/src/swagger_store_handler.erl b/samples/server/petstore/erlang-server/src/swagger_store_handler.erl new file mode 100644 index 00000000000..d70470d6384 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_store_handler.erl @@ -0,0 +1,304 @@ +%% basic handler +-module(swagger_store_handler). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/3]). +-export([rest_init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +%% Handlers +-export([handle_request_json/2]). + +-record(state, { + operation_id :: swagger_api:operation_id(), + logic_handler :: atom(), + validator_state :: jesse_state:state(), + context=#{} :: #{} +}). + +-type state() :: state(). + +-spec init(TransportName :: atom(), Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {upgrade, protocol, cowboy_rest, Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()}. + +init(_Transport, Req, Opts) -> + {upgrade, protocol, cowboy_rest, Req, Opts}. + +-spec rest_init(Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {ok, Req :: cowboy_req:req(), State :: state()}. + +rest_init(Req0, {Operations, LogicHandler, ValidatorState}) -> + {Method, Req} = cowboy_req:method(Req0), + OperationID = maps:get(Method, Operations, undefined), + + error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {ok, Req, State}. + +-spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> + {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. + + +allowed_methods( + Req, + State = #state{ + operation_id = 'DeleteOrder' + } +) -> + {[<<"DELETE">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'GetInventory' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'GetOrderById' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'PlaceOrder' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State) -> + {[], Req, State}. + +-spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: true | {false, AuthHeader :: iodata()}, + Req :: cowboy_req:req(), + State :: state() + }. + +is_authorized( + Req0, + State = #state{ + operation_id = 'DeleteOrder' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'GetInventory' = OperationID, + logic_handler = LogicHandler + } +) -> + From = header, + Result = swagger_auth:authorize_api_key( + LogicHandler, + OperationID, + From, + "api_key", + Req0 + ), + case Result of + {true, Context, Req} -> {true, Req, State#state{context = Context}}; + {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + end; + +is_authorized( + Req0, + State = #state{ + operation_id = 'GetOrderById' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'PlaceOrder' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized(Req, State) -> + {{false, <<"">>}, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'DeleteOrder' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'GetInventory' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'GetOrderById' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'PlaceOrder' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req, State) -> + {false, Req, State}. + +-spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), ProvideResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_provided(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). + +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% + +-type result_ok() :: { + ok, + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} +}. + +-type result_error() :: {error, Reason :: any()}. + +-type processed_response() :: {halt, cowboy_req:req(), state()}. + +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). + +process_response(Response, Req0, State = #state{operation_id = OperationID}) -> + case Response of + {ok, {Code, Headers, Body}} -> + {ok, Req} = cowboy_req:reply(Code, Headers, Body, Req0), + {halt, Req, State}; + {error, Message} -> + error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), + + {ok, Req} = cowboy_req:reply(400, Req0), + {halt, Req, State} + end. + +-spec handle_request_json(cowboy_req:req(), state()) -> {halt, cowboy_req:req(), state()}. + +handle_request_json( + Req0, + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState, + context = Context + } +) -> + case swagger_api:populate_request(OperationID, Req0, ValidatorState) of + {ok, Populated, Req1} -> + {Code, Headers, Body} = swagger_logic_handler:handle_request( + LogicHandler, + OperationID, + Populated, + Context + ), + _ = swagger_api:validate_response( + OperationID, + Code, + Body, + ValidatorState + ), + PreparedBody = jsx:encode(Body), + Response = {ok, {Code, Headers, PreparedBody}}, + process_response(Response, Req1, State); + {error, Reason, Req1} -> + process_response({error, Reason}, Req1, State) + end. + +validate_headers(_, Req) -> {true, Req}. diff --git a/samples/server/petstore/erlang-server/src/swagger_user_handler.erl b/samples/server/petstore/erlang-server/src/swagger_user_handler.erl new file mode 100644 index 00000000000..11c8d659416 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_user_handler.erl @@ -0,0 +1,396 @@ +%% basic handler +-module(swagger_user_handler). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/3]). +-export([rest_init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +%% Handlers +-export([handle_request_json/2]). + +-record(state, { + operation_id :: swagger_api:operation_id(), + logic_handler :: atom(), + validator_state :: jesse_state:state(), + context=#{} :: #{} +}). + +-type state() :: state(). + +-spec init(TransportName :: atom(), Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {upgrade, protocol, cowboy_rest, Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()}. + +init(_Transport, Req, Opts) -> + {upgrade, protocol, cowboy_rest, Req, Opts}. + +-spec rest_init(Req :: cowboy_req:req(), Opts :: swagger_router:init_opts()) -> + {ok, Req :: cowboy_req:req(), State :: state()}. + +rest_init(Req0, {Operations, LogicHandler, ValidatorState}) -> + {Method, Req} = cowboy_req:method(Req0), + OperationID = maps:get(Method, Operations, undefined), + + error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {ok, Req, State}. + +-spec allowed_methods(Req :: cowboy_req:req(), State :: state()) -> + {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. + + +allowed_methods( + Req, + State = #state{ + operation_id = 'CreateUser' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'CreateUsersWithArrayInput' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'CreateUsersWithListInput' + } +) -> + {[<<"POST">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'DeleteUser' + } +) -> + {[<<"DELETE">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'GetUserByName' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'LoginUser' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'LogoutUser' + } +) -> + {[<<"GET">>], Req, State}; + +allowed_methods( + Req, + State = #state{ + operation_id = 'UpdateUser' + } +) -> + {[<<"PUT">>], Req, State}; + +allowed_methods(Req, State) -> + {[], Req, State}. + +-spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: true | {false, AuthHeader :: iodata()}, + Req :: cowboy_req:req(), + State :: state() + }. + +is_authorized( + Req0, + State = #state{ + operation_id = 'CreateUser' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'CreateUsersWithArrayInput' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'CreateUsersWithListInput' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'DeleteUser' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'GetUserByName' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'LoginUser' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'LogoutUser' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized( + Req0, + State = #state{ + operation_id = 'UpdateUser' = OperationID, + logic_handler = LogicHandler + } +) -> + +is_authorized(Req, State) -> + {{false, <<"">>}, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'CreateUser' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'CreateUsersWithArrayInput' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'CreateUsersWithListInput' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'DeleteUser' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'GetUserByName' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'LoginUser' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'LogoutUser' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers( + Req0, + State = #state{ + operation_id = 'UpdateUser' + } +) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req, State) -> + {false, Req, State}. + +-spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), ProvideResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_provided(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. + +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). + +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. + +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% + +-type result_ok() :: { + ok, + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} +}. + +-type result_error() :: {error, Reason :: any()}. + +-type processed_response() :: {halt, cowboy_req:req(), state()}. + +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). + +process_response(Response, Req0, State = #state{operation_id = OperationID}) -> + case Response of + {ok, {Code, Headers, Body}} -> + {ok, Req} = cowboy_req:reply(Code, Headers, Body, Req0), + {halt, Req, State}; + {error, Message} -> + error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]), + + {ok, Req} = cowboy_req:reply(400, Req0), + {halt, Req, State} + end. + +-spec handle_request_json(cowboy_req:req(), state()) -> {halt, cowboy_req:req(), state()}. + +handle_request_json( + Req0, + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState, + context = Context + } +) -> + case swagger_api:populate_request(OperationID, Req0, ValidatorState) of + {ok, Populated, Req1} -> + {Code, Headers, Body} = swagger_logic_handler:handle_request( + LogicHandler, + OperationID, + Populated, + Context + ), + _ = swagger_api:validate_response( + OperationID, + Code, + Body, + ValidatorState + ), + PreparedBody = jsx:encode(Body), + Response = {ok, {Code, Headers, PreparedBody}}, + process_response(Response, Req1, State); + {error, Reason, Req1} -> + process_response({error, Reason}, Req1, State) + end. + +validate_headers(_, Req) -> {true, Req}. diff --git a/samples/server/petstore/erlang-server/src/swagger_utils.erl b/samples/server/petstore/erlang-server/src/swagger_utils.erl new file mode 100644 index 00000000000..f7897fddd84 --- /dev/null +++ b/samples/server/petstore/erlang-server/src/swagger_utils.erl @@ -0,0 +1,173 @@ +-module(swagger_utils). + +-export([to_binary/1]). +-export([to_list/1]). +-export([to_float/1]). +-export([to_int/1]). +-export([to_lower/1]). +-export([to_upper/1]). +-export([set_resp_headers/2]). +-export([to_header/1]). +-export([to_qs/1]). +-export([to_binding/1]). +-export([get_opt/2]). +-export([get_opt/3]). +-export([priv_dir/0]). +-export([priv_dir/1]). +-export([priv_path/1]). + + +-spec to_binary(iodata() | atom() | number()) -> binary(). + +to_binary(V) when is_binary(V) -> V; +to_binary(V) when is_list(V) -> iolist_to_binary(V); +to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8); +to_binary(V) when is_integer(V) -> integer_to_binary(V); +to_binary(V) when is_float(V) -> float_to_binary(V). + +-spec to_list(iodata() | atom() | number()) -> string(). + +to_list(V) when is_list(V) -> V; +to_list(V) -> binary_to_list(to_binary(V)). + +-spec to_float(iodata()) -> number(). + +to_float(V) -> + Data = iolist_to_binary([V]), + case binary:split(Data, <<$.>>) of + [Data] -> + binary_to_integer(Data); + [<<>>, _] -> + binary_to_float(<<$0, Data/binary>>); + _ -> + binary_to_float(Data) + end. + +%% + +-spec to_int(integer() | binary() | list()) -> integer(). + +to_int(Data) when is_integer(Data) -> + Data; +to_int(Data) when is_binary(Data) -> + binary_to_integer(Data); +to_int(Data) when is_list(Data) -> + list_to_integer(Data). + +-spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req(). + +set_resp_headers([], Req) -> + Req; +set_resp_headers([{K, V} | T], Req0) -> + Req = cowboy_req:set_resp_header(K, V, Req0), + set_resp_headers(T, Req). + +-spec to_header(iodata() | atom() | number()) -> binary(). + +to_header(Name) -> + Prepared = to_binary(Name), + to_lower(Prepared). + +-spec to_qs(iodata() | atom() | number()) -> binary(). + +to_qs(Name) -> + to_binary(Name). + +-spec to_binding(iodata() | atom() | number()) -> atom(). + +to_binding(Name) -> + Prepared = to_binary(Name), + binary_to_atom(Prepared, utf8). + +-spec get_opt(any(), []) -> any(). + +get_opt(Key, Opts) -> + get_opt(Key, Opts, undefined). + +-spec get_opt(any(), [], any()) -> any(). + +get_opt(Key, Opts, Default) -> + case lists:keyfind(Key, 1, Opts) of + {_, Value} -> Value; + false -> Default + end. + +-spec priv_dir() -> file:filename(). + +priv_dir() -> + {ok, AppName} = application:get_application(), + priv_dir(AppName). + +-spec priv_dir(Application :: atom()) -> file:filename(). + +priv_dir(AppName) -> + case code:priv_dir(AppName) of + Value when is_list(Value) -> + Value ++ "/"; + _Error -> + select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"]) + end. + +-spec priv_path(Relative :: file:filename()) -> file:filename(). + +priv_path(Relative) -> + filename:join(priv_dir(), Relative). + +-include_lib("kernel/include/file.hrl"). + +select_priv_dir(Paths) -> + case lists:dropwhile(fun test_priv_dir/1, Paths) of + [Path | _] -> Path; + _ -> exit(no_priv_dir) + end. + +test_priv_dir(Path) -> + case file:read_file_info(Path) of + {ok, #file_info{type = directory}} -> + false; + _ -> + true + end. + + +%% + +-spec to_lower(binary()) -> binary(). + +to_lower(S) -> + to_case(lower, S, <<>>). + +-spec to_upper(binary()) -> binary(). + +to_upper(S) -> + to_case(upper, S, <<>>). + +to_case(_Case, <<>>, Acc) -> + Acc; + +to_case(_Case, <>, _Acc) when C > 127 -> + error(badarg); + +to_case(Case = lower, <>, Acc) -> + to_case(Case, Rest, <>); + +to_case(Case = upper, <>, Acc) -> + to_case(Case, Rest, <>). + +to_lower_char(C) when is_integer(C), $A =< C, C =< $Z -> + C + 32; +to_lower_char(C) when is_integer(C), 16#C0 =< C, C =< 16#D6 -> + C + 32; +to_lower_char(C) when is_integer(C), 16#D8 =< C, C =< 16#DE -> + C + 32; +to_lower_char(C) -> + C. + +to_upper_char(C) when is_integer(C), $a =< C, C =< $z -> + C - 32; +to_upper_char(C) when is_integer(C), 16#E0 =< C, C =< 16#F6 -> + C - 32; +to_upper_char(C) when is_integer(C), 16#F8 =< C, C =< 16#FE -> + C - 32; +to_upper_char(C) -> + C.