diff --git a/.gitignore b/.gitignore index 26c01a81696d..ebe0dc1ff9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -258,3 +258,6 @@ samples/client/petstore/c/*.so # Ruby samples/openapi3/client/petstore/ruby/Gemfile.lock samples/openapi3/client/petstore/ruby-faraday/Gemfile.lock + +# Crystal +samples/client/petstore/crystal/lib diff --git a/.travis.yml b/.travis.yml index afe97dc27200..edbd8c27dd53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -62,6 +62,17 @@ addons: - petstore.swagger.io before_install: + # to run petstore server locally via docker + - echo "$DOCKER_HUB_PASSWORD" | docker login --username=$DOCKER_HUB_USERNAME --password-stdin || true + - docker pull swaggerapi/petstore + - docker run -d -e SWAGGER_HOST=http://petstore.swagger.io -e SWAGGER_BASE_PATH=/v2 -p 80:8080 swaggerapi/petstore + - docker ps -a + # install crystal + - curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash + - curl -sL "https://keybase.io/crystal/pgp_keys.asc" | sudo apt-key add - + - echo "deb https://dist.crystal-lang.org/apt crystal main" | sudo tee /etc/apt/sources.list.d/crystal.list + - sudo apt-get update + - sudo apt install crystal - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.0 - export PATH="$HOME/.yarn/bin:$PATH" # install rust @@ -75,11 +86,6 @@ before_install: - npm config set registry http://registry.npmjs.org/ # set python 3.6.3 as default - source ~/virtualenv/python3.6/bin/activate - # to run petstore server locally via docker - - echo "$DOCKER_HUB_PASSWORD" | docker login --username=$DOCKER_HUB_USERNAME --password-stdin || true - - docker pull swaggerapi/petstore - - docker run -d -e SWAGGER_HOST=http://petstore.swagger.io -e SWAGGER_BASE_PATH=/v2 -p 80:8080 swaggerapi/petstore - - docker ps -a # -- skip bash test to shorten build time # Add bats test framework and cURL for Bash script integration tests #- sudo add-apt-repository ppa:duggan/bats --yes diff --git a/CI/circle_parallel.sh b/CI/circle_parallel.sh index 70b541743d8b..25705a08528d 100755 --- a/CI/circle_parallel.sh +++ b/CI/circle_parallel.sh @@ -41,13 +41,14 @@ elif [ "$NODE_INDEX" = "2" ]; then curl -sSL https://get.haskellstack.org/ | sh stack upgrade stack --version - # install r + # prepare r sudo sh -c 'echo "deb http://cran.rstudio.com/bin/linux/ubuntu trusty/" >> /etc/apt/sources.list' gpg --keyserver keyserver.ubuntu.com --recv-key E084DAB9 gpg -a --export E084DAB9 | sudo apt-key add - sudo apt-get update sudo apt-get -y install r-base R --version + # install curl sudo apt-get -y build-dep libcurl4-gnutls-dev sudo apt-get -y install libcurl4-gnutls-dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9644a5cb366d..30258ec9fd6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,7 @@ Code change should conform to the programming style guide of the respective lang - C++: https://google.github.io/styleguide/cppguide.html - C++ (Tizen): https://wiki.tizen.org/Native_Platform_Coding_Idiom_and_Style_Guide#C.2B.2B_Coding_Style - Clojure: https://github.com/bbatsov/clojure-style-guide +- Crystal: https://crystal-lang.org/reference/conventions/coding_style.html - Dart: https://www.dartlang.org/guides/language/effective-dart/style - Elixir: https://github.com/christopheradams/elixir_style_guide - Eiffel: https://www.eiffel.org/doc/eiffel/Coding%20Standards diff --git a/README.md b/README.md index 718761800439..51785331a98e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ OpenAPI Generator allows generation of API client libraries (SDK generation), se | | Languages/Frameworks | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **API clients** | **ActionScript**, **Ada**, **Apex**, **Bash**, **C**, **C#** (.net 2.0, 3.5 or later, .NET Standard 1.3 - 2.0, .NET Core 2.0), **C++** (cpp-restsdk, Qt5, Tizen), **Clojure**, **Dart**, **Elixir**, **Elm**, **Eiffel**, **Erlang**, **Go**, **Groovy**, **Haskell** (http-client, Servant), **Java** (Jersey1.x, Jersey2.x, OkHttp, Retrofit1.x, Retrofit2.x, Feign, RestTemplate, RESTEasy, Vertx, Google API Client Library for Java, Rest-assured, Spring 5 Web Client, MicroProfile Rest Client), **k6**, **Kotlin**, **Lua**, **Nim**, **Node.js/JavaScript** (ES5, ES6, AngularJS with Google Closure Compiler annotations, Flow types, Apollo GraphQL DataStore), **Objective-C**, **OCaml**, **Perl**, **PHP**, **PowerShell**, **Python**, **R**, **Ruby**, **Rust** (rust, rust-server), **Scala** (akka, http4s, scalaz, sttp, swagger-async-httpclient), **Swift** (2.x, 3.x, 4.x, 5.x), **Typescript** (AngularJS, Angular (2.x - 8.x), Aurelia, Axios, Fetch, Inversify, jQuery, Node, Rxjs) | +| **API clients** | **ActionScript**, **Ada**, **Apex**, **Bash**, **C**, **C#** (.net 2.0, 3.5 or later, .NET Standard 1.3 - 2.0, .NET Core 2.0), **C++** (cpp-restsdk, Qt5, Tizen), **Clojure**, **Crystal**, **Dart**, **Elixir**, **Elm**, **Eiffel**, **Erlang**, **Go**, **Groovy**, **Haskell** (http-client, Servant), **Java** (Jersey1.x, Jersey2.x, OkHttp, Retrofit1.x, Retrofit2.x, Feign, RestTemplate, RESTEasy, Vertx, Google API Client Library for Java, Rest-assured, Spring 5 Web Client, MicroProfile Rest Client), **k6**, **Kotlin**, **Lua**, **Nim**, **Node.js/JavaScript** (ES5, ES6, AngularJS with Google Closure Compiler annotations, Flow types, Apollo GraphQL DataStore), **Objective-C**, **OCaml**, **Perl**, **PHP**, **PowerShell**, **Python**, **R**, **Ruby**, **Rust** (rust, rust-server), **Scala** (akka, http4s, scalaz, sttp, swagger-async-httpclient), **Swift** (2.x, 3.x, 4.x, 5.x), **Typescript** (AngularJS, Angular (2.x - 8.x), Aurelia, Axios, Fetch, Inversify, jQuery, Node, Rxjs) | | **Server stubs** | **Ada**, **C#** (ASP.NET Core, NancyFx), **C++** (Pistache, Restbed, Qt5 QHTTPEngine), **Erlang**, **F#** (Giraffe), **Go** (net/http, Gin), **Haskell** (Servant), **Java** (MSF4J, Spring, Undertow, JAX-RS: CDI, CXF, Inflector, Jersey, RestEasy, Play Framework, [PKMST](https://github.com/ProKarma-Inc/pkmst-getting-started-examples), [Vert.x](https://vertx.io/)), **Kotlin** (Spring Boot, Ktor, Vertx), **PHP** (Laravel, Lumen, Slim, Silex, [Symfony](https://symfony.com/), [Zend Expressive](https://github.com/zendframework/zend-expressive)), **Python** (Flask), **NodeJS**, **Ruby** (Sinatra, Rails5), **Rust** (rust-server), **Scala** (Akka, [Finch](https://github.com/finagle/finch), [Lagom](https://github.com/lagom/lagom), [Play](https://www.playframework.com/), Scalatra) | | **API documentation generators** | **HTML**, **Confluence Wiki**, **Asciidoc** | | **Configuration files** | [**Apache2**](https://httpd.apache.org/) | @@ -842,6 +842,7 @@ Here is a list of template creators: * C# (.NET Standard 1.3 ): @Gronsak * C# (.NET 4.5 refactored): @jimschubert [:heart:](https://www.patreon.com/jimschubert) * Clojure: @xhh + * Crystal: @wing328 * Dart: @yissachar * Dart (refactor): @joernahrens * Dart 2: @swipesight diff --git a/bin/configs/crystal.yaml b/bin/configs/crystal.yaml new file mode 100644 index 000000000000..74badcce662d --- /dev/null +++ b/bin/configs/crystal.yaml @@ -0,0 +1,9 @@ +generatorName: crystal +outputDir: samples/client/petstore/crystal +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/crystal +additionalProperties: + shardVersion: 1.0.0 + moduleName: Petstore + shardName: petstore +strictSpecBehavior: false diff --git a/docs/contributing.md b/docs/contributing.md index 482ffecf4dd5..a791b0d6f1ba 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -53,6 +53,7 @@ Code change should conform to the programming style guide of the respective lang - C++: https://google.github.io/styleguide/cppguide.html - C++ (Tizen): https://wiki.tizen.org/Native_Platform_Coding_Idiom_and_Style_Guide#C.2B.2B_Coding_Style - Clojure: https://github.com/bbatsov/clojure-style-guide +- Crystal: https://crystal-lang.org/reference/conventions/coding_style.html - Dart: https://www.dartlang.org/guides/language/effective-dart/style - Elixir: https://github.com/christopheradams/elixir_style_guide - Eiffel: https://www.eiffel.org/doc/eiffel/Coding%20Standards diff --git a/docs/generators.md b/docs/generators.md index 8a68e5295329..37d5902116f9 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -16,6 +16,7 @@ The following generators are available: * [cpp-restsdk](generators/cpp-restsdk.md) * [cpp-tizen](generators/cpp-tizen.md) * [cpp-ue4 (beta)](generators/cpp-ue4.md) +* [crystal (beta)](generators/crystal.md) * [csharp](generators/csharp.md) * [csharp-dotnet2 (deprecated)](generators/csharp-dotnet2.md) * [csharp-netcore](generators/csharp-netcore.md) diff --git a/docs/generators/crystal.md b/docs/generators/crystal.md new file mode 100644 index 000000000000..8965a800fff1 --- /dev/null +++ b/docs/generators/crystal.md @@ -0,0 +1,226 @@ +--- +title: Config Options for crystal +sidebar_label: crystal +--- + +These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details. + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| +|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|hideGenerationTimestamp|Hides the generation timestamp when files are generated.| |true| +|legacyDiscriminatorBehavior|Set to true for generators with better support for discriminators. (Python, Java, Go, PowerShell, C#have this enabled by default).|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| +|shardAuthor|shard author (only one is supported).| |null| +|shardAuthorEmail|shard author email (only one is supported).| |null| +|shardDescription|shard description.| |This shard maps to a REST API| +|shardHomepage|shard homepage.| |http://org.openapitools| +|shardLicense|shard license.| |unlicense| +|shardName|shard name (e.g. twitter_client| |openapi_client| +|shardVersion|shard version.| |1.0.0| +|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| +|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | +|array|Array| +|map|Hash| +|set|Set| + + +## LANGUAGE PRIMITIVES + + + +## RESERVED WORDS + + + +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✓|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✓|ToolingExtension +|MockServer|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Array|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✗|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✓|OAS2,OAS3 +|Union|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✓|OAS3 +|OAuth2_Implicit|✓|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✓|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✓|OAS2,OAS3 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java new file mode 100644 index 000000000000..7a5b347180d2 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java @@ -0,0 +1,891 @@ +/* + * 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 + * + * https://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 io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.*; +import org.openapitools.codegen.meta.GeneratorMetadata; +import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.meta.features.*; +import org.openapitools.codegen.templating.mustache.PrefixWithHashLambda; +import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.*; + +import static org.openapitools.codegen.utils.StringUtils.camelize; +import static org.openapitools.codegen.utils.StringUtils.underscore; + +public class CrystalClientCodegen extends DefaultCodegen { + private static final Logger LOGGER = LoggerFactory.getLogger(CrystalClientCodegen.class); + private static final String NUMERIC_ENUM_PREFIX = "N"; + protected static int emptyMethodNameCounter = 0; + + protected String shardName; + protected String moduleName; + protected String shardVersion = "1.0.0"; + protected String specFolder = "spec"; + protected String srcFolder = "src"; + protected String shardLicense = "unlicense"; + protected String shardHomepage = "https://openapitools.org"; + protected String shardSummary = "A Crystal SDK for the REST API"; + protected String shardDescription = "This shard maps to a REST API"; + protected String shardAuthor = ""; + protected String shardAuthorEmail = ""; + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + + public static final String SHARD_NAME = "shardName"; + public static final String SHARD_VERSION = "shardVersion"; + public static final String SHARD_LICENSE = "shardLicense"; + public static final String SHARD_HOMEPAGE = "shardHomepage"; + public static final String SHARD_SUMMARY = "shardSummary"; + public static final String SHARD_DESCRIPTION = "shardDescription"; + public static final String SHARD_AUTHOR = "shardAuthor"; + public static final String SHARD_AUTHOR_EMAIL = "shardAuthorEmail"; + + public CrystalClientCodegen() { + super(); + + modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML, WireFormatFeature.Custom)) + .securityFeatures(EnumSet.of( + SecurityFeature.BasicAuth, + SecurityFeature.BearerToken, + SecurityFeature.ApiKey, + SecurityFeature.OAuth2_Implicit + )) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling, + GlobalFeature.ParameterizedServer, + GlobalFeature.MultiServer + ) + .includeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism + ) + .excludeParameterFeatures( + ParameterFeature.Cookie + ) + .includeClientModificationFeatures( + ClientModificationFeature.BasePath, + ClientModificationFeature.UserAgent + ) + ); + + generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) + .stability(Stability.BETA) + .build(); + + supportsInheritance = true; + + // clear import mapping (from default generator) as crystal does not use it + // at the moment + importMapping.clear(); + + embeddedTemplateDir = templateDir = "crystal"; + outputFolder = "generated-code" + File.separator + "crystal"; + + modelPackage = "models"; + apiPackage = "api"; + modelTemplateFiles.put("model.mustache", ".cr"); + apiTemplateFiles.put("api.mustache", ".cr"); + + modelTestTemplateFiles.put("model_test.mustache", ".cr"); + apiTestTemplateFiles.put("api_test.mustache", ".cr"); + + // TODO support auto-generated doc + //modelDocTemplateFiles.put("model_doc.mustache", ".md"); + //apiDocTemplateFiles.put("api_doc.mustache", ".md"); + + // default HIDE_GENERATION_TIMESTAMP to true + hideGenerationTimestamp = Boolean.TRUE; + + // reserved word. Ref: https://github.com/crystal-lang/crystal/wiki/Crystal-for-Rubyists#available-keywords + reservedWords = new HashSet( + Arrays.asList( + "abstract", "do", "if", "nil?", "select", "union", + "alias", "else", "in", "of", "self", "unless", + "as", "elsif", "include", "out", "sizeof", "until", + "as?", "end", "instance", "sizeof", "pointerof", "struct", "verbatim", + "asm", "ensure", "is_a?", "private", "super", "when", + "begin", "enum", "lib", "protected", "then", "while", + "break", "extend", "macro", "require", "true", "with", + "case", "false", "module", "rescue", "type", "yield", + "class", "for", "next", "responds_to?", "typeof", + "def", "fun", "nil", "return", "uninitialized") + ); + + languageSpecificPrimitives.clear(); + languageSpecificPrimitives.add("String"); + languageSpecificPrimitives.add("Boolean"); + languageSpecificPrimitives.add("Integer"); + languageSpecificPrimitives.add("Float"); + languageSpecificPrimitives.add("Date"); + languageSpecificPrimitives.add("Time"); + languageSpecificPrimitives.add("Array"); + languageSpecificPrimitives.add("Hash"); + languageSpecificPrimitives.add("File"); + languageSpecificPrimitives.add("Object"); + + typeMapping.clear(); + typeMapping.put("string", "String"); + typeMapping.put("boolean", "Bool"); + typeMapping.put("char", "Char"); + typeMapping.put("int", "Int32"); + typeMapping.put("integer", "Int32"); + typeMapping.put("long", "Int64"); + typeMapping.put("short", "Int32"); + typeMapping.put("float", "Float32"); + typeMapping.put("double", "Float64"); + typeMapping.put("number", "Float64"); + typeMapping.put("date", "Time"); + typeMapping.put("DateTime", "Time"); + typeMapping.put("array", "Array"); + typeMapping.put("List", "Array"); + typeMapping.put("set", "Set"); + typeMapping.put("map", "Hash"); + typeMapping.put("object", "Object"); + typeMapping.put("file", "File"); + typeMapping.put("binary", "String"); + typeMapping.put("ByteArray", "String"); + typeMapping.put("UUID", "String"); + typeMapping.put("URI", "String"); + + instantiationTypes.put("map", "Hash"); + instantiationTypes.put("array", "Array"); + instantiationTypes.put("set", "Set"); + + // remove modelPackage and apiPackage added by default + cliOptions.removeIf(opt -> CodegenConstants.MODEL_PACKAGE.equals(opt.getOpt()) || + CodegenConstants.API_PACKAGE.equals(opt.getOpt())); + + cliOptions.add(new CliOption(SHARD_NAME, "shard name (e.g. twitter_client"). + defaultValue("openapi_client")); + + cliOptions.add(new CliOption(SHARD_VERSION, "shard version.").defaultValue("1.0.0")); + + cliOptions.add(new CliOption(SHARD_LICENSE, "shard license."). + defaultValue("unlicense")); + + cliOptions.add(new CliOption(SHARD_HOMEPAGE, "shard homepage."). + defaultValue("http://org.openapitools")); + + cliOptions.add(new CliOption(SHARD_DESCRIPTION, "shard description."). + defaultValue("This shard maps to a REST API")); + + cliOptions.add(new CliOption(SHARD_AUTHOR, "shard author (only one is supported).")); + + cliOptions.add(new CliOption(SHARD_AUTHOR_EMAIL, "shard author email (only one is supported).")); + + cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC). + defaultValue(Boolean.TRUE.toString())); + } + + @Override + public void processOpts() { + super.processOpts(); + + if (StringUtils.isEmpty(System.getenv("CRYSTAL_POST_PROCESS_FILE"))) { + LOGGER.info("Hint: Environment variable 'CRYSTAL_POST_PROCESS_FILE' (optional) not defined. E.g. to format the source code, please try 'export CRYSTAL_POST_PROCESS_FILE=\"/usr/local/bin/crystal tool format\"' (Linux/Mac)"); + } + + if (additionalProperties.containsKey(SHARD_NAME)) { + setShardName((String) additionalProperties.get(SHARD_NAME)); + } + additionalProperties.put(SHARD_NAME, shardName); + + if (additionalProperties.containsKey(SHARD_VERSION)) { + setShardVersion((String) additionalProperties.get(SHARD_VERSION)); + } else { + // not set, pass the default value to template + additionalProperties.put(SHARD_VERSION, shardVersion); + } + + if (additionalProperties.containsKey(SHARD_LICENSE)) { + setShardLicense((String) additionalProperties.get(SHARD_LICENSE)); + } + + if (additionalProperties.containsKey(SHARD_HOMEPAGE)) { + setShardHomepage((String) additionalProperties.get(SHARD_HOMEPAGE)); + } + + if (additionalProperties.containsKey(SHARD_SUMMARY)) { + setShardSummary((String) additionalProperties.get(SHARD_SUMMARY)); + } + + if (additionalProperties.containsKey(SHARD_DESCRIPTION)) { + setShardDescription((String) additionalProperties.get(SHARD_DESCRIPTION)); + } + + if (additionalProperties.containsKey(SHARD_AUTHOR)) { + setShardAuthor((String) additionalProperties.get(SHARD_AUTHOR)); + } + + if (additionalProperties.containsKey(SHARD_AUTHOR_EMAIL)) { + setShardAuthorEmail((String) additionalProperties.get(SHARD_AUTHOR_EMAIL)); + } + + // make api and model doc path available in mustache template + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + + // use constant model/api package (folder path) + setModelPackage("models"); + setApiPackage("api"); + + supportingFiles.add(new SupportingFile("shard_name.mustache", srcFolder, shardName + ".cr")); + String shardFolder = srcFolder + File.separator + shardName; + supportingFiles.add(new SupportingFile("api_error.mustache", shardFolder, "api_error.cr")); + supportingFiles.add(new SupportingFile("configuration.mustache", shardFolder, "configuration.cr")); + supportingFiles.add(new SupportingFile("api_client.mustache", shardFolder, "api_client.cr")); + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + supportingFiles.add(new SupportingFile("travis.mustache", "", ".travis.yml")); + supportingFiles.add(new SupportingFile("shard.mustache", "", "shard.yml")); + + // crystal spec files + supportingFiles.add(new SupportingFile("spec_helper.mustache", specFolder, "spec_helper.cr") + .doNotOverwrite()); + + // add lambda for mustache templates + additionalProperties.put("lambdaPrefixWithHash", new PrefixWithHashLambda()); + + } + + @Override + public String getHelp() { + return "Generates a Crystal client library (beta)."; + } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String getName() { + return "crystal"; + } + + @Override + public String apiFileFolder() { + return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator + apiPackage.replace("/", File.separator); + } + + @Override + public String modelFileFolder() { + return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator + modelPackage.replace("/", File.separator); + } + + @Override + public String apiTestFileFolder() { + return outputFolder + File.separator + specFolder + File.separator + apiPackage.replace("/", File.separator); + } + + @Override + public String modelTestFileFolder() { + return outputFolder + File.separator + specFolder + File.separator + modelPackage.replace("/", File.separator); + } + + @Override + public String apiDocFileFolder() { + return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + } + + @Override + public String modelDocFileFolder() { + return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + } + + @Override + public String getSchemaType(Schema schema) { + String openAPIType = super.getSchemaType(schema); + String type = null; + if (typeMapping.containsKey(openAPIType)) { + type = typeMapping.get(openAPIType); + if (languageSpecificPrimitives.contains(type)) { + return type; + } + } else { + type = openAPIType; + } + + if (type == null) { + return null; + } + + return toModelName(type); + } + + @Override + public String toModelName(final String name) { + String modelName; + modelName = sanitizeName(name); + + if (!StringUtils.isEmpty(modelNamePrefix)) { + modelName = modelNamePrefix + "_" + modelName; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + modelName = modelName + "_" + modelNameSuffix; + } + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(modelName)) { + modelName = camelize("Model" + modelName); + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + modelName); + return modelName; + } + + // model name starts with number + if (modelName.matches("^\\d.*")) { + LOGGER.warn(modelName + " (model name starts with number) cannot be used as model name. Renamed to " + camelize("model_" + modelName)); + modelName = "model_" + modelName; // e.g. 200Response => Model200Response (after camelize) + } + + // camelize the model name + // phone_number => PhoneNumber + return camelize(modelName); + } + + @Override + public String toModelFilename(String name) { + return underscore(toModelName(name)); + } + + @Override + public String toModelDocFilename(String name) { + return toModelName(name); + } + + @Override + public String toApiFilename(final String name) { + // replace - with _ e.g. created-at => created_at + String filename = name; + if (apiNameSuffix != null && apiNameSuffix.length() > 0) { + filename = filename + "_" + apiNameSuffix; + } + + filename = filename.replaceAll("-", "_"); + + // e.g. PhoneNumberApi.cr => phone_number_api.cr + return underscore(filename); + } + + @Override + public String toApiDocFilename(String name) { + return toApiName(name); + } + + @Override + public String toApiTestFilename(String name) { + return toApiFilename(name) + "_spec"; + } + + @Override + public String toModelTestFilename(String name) { + return toModelFilename(name) + "_spec"; + } + + @Override + public String toApiName(String name) { + return super.toApiName(name); + } + + @Override + public String toEnumValue(String value, String datatype) { + if ("Integer".equals(datatype) || "Float".equals(datatype)) { + return value; + } else { + return "\"" + escapeText(value) + "\""; + } + } + + @Override + public String toEnumVarName(String name, String datatype) { + if (name.length() == 0) { + return "EMPTY"; + } + + // number + if ("Integer".equals(datatype) || "Float".equals(datatype)) { + String varName = name; + varName = varName.replaceAll("-", "MINUS_"); + varName = varName.replaceAll("\\+", "PLUS_"); + varName = varName.replaceAll("\\.", "_DOT_"); + return NUMERIC_ENUM_PREFIX + varName; + } + + // string + String enumName = sanitizeName(underscore(name).toUpperCase(Locale.ROOT)); + enumName = enumName.replaceFirst("^_", ""); + enumName = enumName.replaceFirst("_$", ""); + + if (enumName.matches("\\d.*")) { // starts with number + return NUMERIC_ENUM_PREFIX + enumName; + } else { + return enumName; + } + } + + @Override + public String toEnumName(CodegenProperty property) { + String enumName = underscore(toModelName(property.name)).toUpperCase(Locale.ROOT); + enumName = enumName.replaceFirst("^_", ""); + enumName = enumName.replaceFirst("_$", ""); + + if (enumName.matches("\\d.*")) { // starts with number + return NUMERIC_ENUM_PREFIX + enumName; + } else { + return enumName; + } + } + + @Override + public Map postProcessModels(Map objs) { + // process enum in models + return postProcessModelsEnum(objs); + } + + @Override + public String toOperationId(String operationId) { + // rename to empty_method_name_1 (e.g.) if method name is empty + if (StringUtils.isEmpty(operationId)) { + operationId = underscore("empty_method_name_" + emptyMethodNameCounter++); + LOGGER.warn("Empty method name (operationId) found. Renamed to " + operationId); + return operationId; + } + + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(operationId)) { + String newOperationId = underscore("call_" + operationId); + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + newOperationId); + return newOperationId; + } + + // operationId starts with a number + if (operationId.matches("^\\d.*")) { + LOGGER.warn(operationId + " (starting with a number) cannot be used as method name. Renamed to " + underscore(sanitizeName("call_" + operationId))); + operationId = "call_" + operationId; + } + + return underscore(sanitizeName(operationId)); + } + + @Override + public String toApiImport(String name) { + return shardName + "/" + apiPackage() + "/" + toApiFilename(name); + } + + public void setShardName(String shardName) { + this.shardName = shardName; + } + + public void setModuleName(String moduleName) { + this.moduleName = moduleName; + } + + public void setShardVersion(String shardVersion) { + this.shardVersion = shardVersion; + } + + public void setShardDescription(String shardDescription) { + this.shardDescription = shardDescription; + } + + public void setShardSummary(String shardSummary) { + this.shardSummary = shardSummary; + } + + public void setShardLicense(String shardLicense) { + this.shardLicense = shardLicense; + } + + public void setShardHomepage(String shardHomepage) { + this.shardHomepage = shardHomepage; + } + + public void setShardAuthor(String shardAuthor) { + this.shardAuthor = shardAuthor; + } + + public void setShardAuthorEmail(String shardAuthorEmail) { + this.shardAuthorEmail = shardAuthorEmail; + } + + @Override + protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema schema) { + final Schema additionalProperties = getAdditionalProperties(schema); + + if (additionalProperties != null) { + codegenModel.additionalPropertiesType = getSchemaType(additionalProperties); + } + } + + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + objs = super.postProcessOperationsWithModels(objs, allModels); + Map operations = (Map) objs.get("operations"); + HashMap modelMaps = new HashMap(); + HashMap processedModelMaps = new HashMap(); + + for (Object o : allModels) { + HashMap h = (HashMap) o; + CodegenModel m = (CodegenModel) h.get("model"); + modelMaps.put(m.classname, m); + } + + List operationList = (List) operations.get("operation"); + for (CodegenOperation op : operationList) { + for (CodegenParameter p : op.allParams) { + p.vendorExtensions.put("x-crystal-example", constructExampleCode(p, modelMaps, processedModelMaps)); + } + processedModelMaps.clear(); + for (CodegenParameter p : op.requiredParams) { + p.vendorExtensions.put("x-crystal-example", constructExampleCode(p, modelMaps, processedModelMaps)); + } + processedModelMaps.clear(); + for (CodegenParameter p : op.optionalParams) { + p.vendorExtensions.put("x-crystal-example", constructExampleCode(p, modelMaps, processedModelMaps)); + } + processedModelMaps.clear(); + for (CodegenParameter p : op.bodyParams) { + p.vendorExtensions.put("x-crystal-example", constructExampleCode(p, modelMaps, processedModelMaps)); + } + processedModelMaps.clear(); + for (CodegenParameter p : op.pathParams) { + p.vendorExtensions.put("x-crystal-example", constructExampleCode(p, modelMaps, processedModelMaps)); + } + processedModelMaps.clear(); + } + + return objs; + } + + private String constructExampleCode(CodegenParameter codegenParameter, HashMap modelMaps, HashMap processedModelMap) { + if (codegenParameter.isArray) { // array + return "[" + constructExampleCode(codegenParameter.items, modelMaps, processedModelMap) + "]"; + } else if (codegenParameter.isMap) { + return "{ key: " + constructExampleCode(codegenParameter.items, modelMaps, processedModelMap) + "}"; + } else if (codegenParameter.isPrimitiveType) { // primitive type + if (codegenParameter.isEnum) { + // When inline enum, set example to first allowable value + List values = (List) codegenParameter.allowableValues.get("values"); + codegenParameter.example = String.valueOf(values.get(0)); + } + if (codegenParameter.isString || "String".equalsIgnoreCase(codegenParameter.baseType)) { + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return "'" + codegenParameter.example + "'"; + } + return "'" + codegenParameter.paramName + "_example'"; + } else if (codegenParameter.isBoolean) { // boolean + if (Boolean.parseBoolean(codegenParameter.example)) { + return "true"; + } + return "false"; + } else if (codegenParameter.isUri) { + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return "'" + codegenParameter.example + "'"; + } + return "'https://example.com'"; + } else if (codegenParameter.isDateTime) { + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return "Time.parse('" + codegenParameter.example + "')"; + } + return "Time.now"; + } else if (codegenParameter.isDate) { + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return "Date.parse('" + codegenParameter.example + "')"; + } + return "Date.today"; + } else if (codegenParameter.isFile) { + return "File.new('/path/to/some/file')"; + } else if (codegenParameter.isInteger) { + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return codegenParameter.example; + } + return "37"; + } else { // number + if (!StringUtils.isEmpty(codegenParameter.example) && !"null".equals(codegenParameter.example)) { + return codegenParameter.example; + } + return "3.56"; + } + } else { // model + // look up the model + if (modelMaps.containsKey(codegenParameter.dataType)) { + return constructExampleCode(modelMaps.get(codegenParameter.dataType), modelMaps, processedModelMap); + } else { + //LOGGER.error("Error in constructing examples. Failed to look up the model " + codegenParameter.dataType); + return "TODO"; + } + } + } + + private String constructExampleCode(CodegenProperty codegenProperty, HashMap modelMaps, HashMap processedModelMap) { + if (codegenProperty.isArray) { // array + return "[" + constructExampleCode(codegenProperty.items, modelMaps, processedModelMap) + "]"; + } else if (codegenProperty.isMap) { + return "{ key: " + constructExampleCode(codegenProperty.items, modelMaps, processedModelMap) + "}"; + } else if (codegenProperty.isPrimitiveType) { // primitive type + if (codegenProperty.isEnum) { + // When inline enum, set example to first allowable value + List values = (List) codegenProperty.allowableValues.get("values"); + codegenProperty.example = String.valueOf(values.get(0)); + } + if (codegenProperty.isString || "String".equalsIgnoreCase(codegenProperty.baseType)) { + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return "'" + codegenProperty.example + "'"; + } else { + return "'" + codegenProperty.name + "_example'"; + } + } else if (codegenProperty.isBoolean) { // boolean + if (Boolean.parseBoolean(codegenProperty.example)) { + return "true"; + } else { + return "false"; + } + } else if (codegenProperty.isUri) { + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return "'" + codegenProperty.example + "'"; + } + return "'https://example.com'"; + } else if (codegenProperty.isDateTime) { + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return "Time.parse('" + codegenProperty.example + "')"; + } + return "Time.now"; + } else if (codegenProperty.isDate) { + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return "Date.parse('" + codegenProperty.example + "')"; + } + return "Date.today"; + } else if (codegenProperty.isFile) { + return "File.new('/path/to/some/file')"; + } else if (codegenProperty.isInteger) { + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return codegenProperty.example; + } + return "37"; + } else { // number + if (!StringUtils.isEmpty(codegenProperty.example) && !"null".equals(codegenProperty.example)) { + return codegenProperty.example; + } + return "3.56"; + } + } else { // model + // look up the model + if (modelMaps.containsKey(codegenProperty.dataType)) { + return constructExampleCode(modelMaps.get(codegenProperty.dataType), modelMaps, processedModelMap); + } else { + //LOGGER.error("Error in constructing examples. Failed to look up the model " + codegenParameter.dataType); + return "TODO"; + } + } + } + + private String constructExampleCode(CodegenModel codegenModel, HashMap modelMaps, HashMap processedModelMap) { + // break infinite recursion. Return, in case a model is already processed in the current context. + String model = codegenModel.name; + if (processedModelMap.containsKey(model)) { + int count = processedModelMap.get(model); + if (count == 1) { + processedModelMap.put(model, 2); + } else if (count == 2) { + return ""; + } else { + throw new RuntimeException("Invalid count when constructing example: " + count); + } + } else if (codegenModel.isEnum) { + List> enumVars = (List>) codegenModel.allowableValues.get("enumVars"); + return moduleName + "::" + codegenModel.classname + "::" + enumVars.get(0).get("name"); + } else if (codegenModel.oneOf != null && !codegenModel.oneOf.isEmpty()) { + String subModel = (String) codegenModel.oneOf.toArray()[0]; + String oneOf = constructExampleCode(modelMaps.get(subModel), modelMaps, processedModelMap); + return oneOf; + } else { + processedModelMap.put(model, 1); + } + + List propertyExamples = new ArrayList<>(); + for (CodegenProperty codegenProperty : codegenModel.requiredVars) { + propertyExamples.add(codegenProperty.name + ": " + constructExampleCode(codegenProperty, modelMaps, processedModelMap)); + } + String example = moduleName + "::" + toModelName(model) + ".new"; + if (!propertyExamples.isEmpty()) { + example += "({" + StringUtils.join(propertyExamples, ", ") + "})"; + } + return example; + } + + @Override + public String escapeReservedWord(String name) { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; + } + + @Override + public String getTypeDeclaration(Schema schema) { + if (ModelUtils.isArraySchema(schema)) { + Schema inner = ((ArraySchema) schema).getItems(); + return getSchemaType(schema) + "(" + getTypeDeclaration(inner) + ")"; + } else if (ModelUtils.isMapSchema(schema)) { + Schema inner = getAdditionalProperties(schema); + return getSchemaType(schema) + "(String, " + getTypeDeclaration(inner) + ")"; + } + + return super.getTypeDeclaration(schema); + } + + @Override + public String toInstantiationType(Schema schema) { + if (ModelUtils.isMapSchema(schema)) { + return instantiationTypes.get("map"); + } else if (ModelUtils.isArraySchema(schema)) { + String parentType; + if (ModelUtils.isSet(schema)) { + parentType = "set"; + } else { + parentType = "array"; + } + return instantiationTypes.get(parentType); + } + return super.toInstantiationType(schema); + } + + @Override + public String toDefaultValue(Schema p) { + p = ModelUtils.getReferencedSchema(this.openAPI, p); + if (ModelUtils.isIntegerSchema(p) || ModelUtils.isNumberSchema(p) || ModelUtils.isBooleanSchema(p)) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (ModelUtils.isStringSchema(p)) { + if (p.getDefault() != null) { + if (p.getDefault() instanceof Date) { + Date date = (Date) p.getDefault(); + LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return "Date.parse(\"" + String.format(Locale.ROOT, localDate.toString(), "") + "\")"; + } else if (p.getDefault() instanceof java.time.OffsetDateTime) { + return "Time.parse(\"" + String.format(Locale.ROOT, ((java.time.OffsetDateTime) p.getDefault()).atZoneSameInstant(ZoneId.systemDefault()).toString(), "") + "\")"; + } else { + return "'" + escapeText((String) p.getDefault()) + "'"; + } + } + } + + return null; + } + + @Override + public String toEnumDefaultValue(String value, String datatype) { + return datatype + "::" + value; + } + + @Override + public String toVarName(final String name) { + String varName; + // sanitize name + varName = sanitizeName(name); + // if it's all uppper case, convert to lower case + if (name.matches("^[A-Z_]*$")) { + varName = varName.toLowerCase(Locale.ROOT); + } + + // camelize (lower first character) the variable name + // petId => pet_id + varName = underscore(varName); + + // for reserved word or word starting with number, append _ + if (isReservedWord(varName) || varName.matches("^\\d.*")) { + varName = escapeReservedWord(varName); + } + + return varName; + } + + public String toRegularExpression(String pattern) { + return addRegularExpressionDelimiter(pattern); + } + + @Override + public String toParamName(String name) { + // should be the same as variable name + return toVarName(name); + } + + @Override + public String escapeQuotationMark(String input) { + // remove ' to avoid code injection + return input.replace("'", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("=end", "=_end").replace("=begin", "=_begin").replace("#{", "\\#{"); + } + + @Override + public void postProcessFile(File file, String fileType) { + if (file == null) { + return; + } + String crystalPostProcessFile = System.getenv("CRYSTAL_POST_PROCESS_FILE"); + if (StringUtils.isEmpty(crystalPostProcessFile)) { + return; // skip if CRYSTAL_POST_PROCESS_FILE env variable is not defined + } + // only process files with cr extension + if ("cr".equals(FilenameUtils.getExtension(file.toString()))) { + String command = crystalPostProcessFile + " " + file.toString(); + try { + Process p = Runtime.getRuntime().exec(command); + int exitValue = p.waitFor(); + if (exitValue != 0) { + BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + LOGGER.error("Error running the command ({}). Exit value: {}, Error output: {}", command, exitValue, sb.toString()); + } else { + LOGGER.info("Successfully executed: " + command); + } + } catch (Exception e) { + LOGGER.error("Error running the command ({}). Exception: {}", command, e.getMessage()); + } + } + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/mustache/PrefixWithHashLambda.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/mustache/PrefixWithHashLambda.java new file mode 100644 index 000000000000..c3418de09b78 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/mustache/PrefixWithHashLambda.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech) + * Copyright 2018 SmartBear Software + * + * 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 + * + * https://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.templating.mustache; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template.Fragment; + +import java.io.IOException; +import java.io.Writer; + +/** + * Replaces duplicate whitespace characters in a fragment with single space. + * + * Register: + *
+ * additionalProperties.put("lambdaPrefixWithHash", new PrefixWithHashLambda());
+ * 
+ * + * Use: + *
+ * {{#lambdaPrefixWithHash}}{{name}}{{/lambdaPrefixWithHash}}
+ * 
+ */ +public class PrefixWithHashLambda implements Mustache.Lambda { + private static final String WITH_HASH = "\n#"; + + private static final String NEWLINE_REGEX = "\\R"; + + @Override + public void execute(Fragment fragment, Writer writer) throws IOException { + writer.write(fragment.execute().replaceAll(NEWLINE_REGEX, WITH_HASH)); + } + +} 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 8fe400f1148c..f9c061816fb8 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 @@ -7,6 +7,7 @@ org.openapitools.codegen.languages.AsciidocDocumentationCodegen org.openapitools.codegen.languages.AspNetCoreServerCodegen org.openapitools.codegen.languages.AvroSchemaCodegen org.openapitools.codegen.languages.BashClientCodegen +org.openapitools.codegen.languages.CrystalClientCodegen org.openapitools.codegen.languages.CLibcurlClientCodegen org.openapitools.codegen.languages.ClojureClientCodegen org.openapitools.codegen.languages.ConfluenceWikiCodegen diff --git a/modules/openapi-generator/src/main/resources/crystal/Gemfile.mustache b/modules/openapi-generator/src/main/resources/crystal/Gemfile.mustache new file mode 100644 index 000000000000..c2e3127cdcfe --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/Gemfile.mustache @@ -0,0 +1,9 @@ +source 'https://rubygems.org' + +gemspec + +group :development, :test do + gem 'rake', '~> 13.0.1' + gem 'pry-byebug' + gem 'rubocop', '~> 0.66.0' +end diff --git a/modules/openapi-generator/src/main/resources/crystal/README.mustache b/modules/openapi-generator/src/main/resources/crystal/README.mustache new file mode 100644 index 000000000000..20705236b9ce --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/README.mustache @@ -0,0 +1,46 @@ +# {{shardName}} + +The Crystsal module for the {{appName}} + +{{#appDescriptionWithNewLines}} +{{{appDescriptionWithNewLines}}} +{{/appDescriptionWithNewLines}} + +This SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: + +- API version: {{appVersion}} +- Package version: {{shardVersion}} +{{^hideGenerationTimestamp}} +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} +- Build package: {{generatorClass}} +{{#infoUrl}} +For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +## Installation + +### Install from Git + +Add the following to shard.yaml + +```yaml +dependencies: + {{{shardName}}}: + github: {{#gitUserId}}{{.}}{{/gitUserId}}{{^gitUserId}}YOUR_GIT_USERNAME{{/gitUserId}}/{{#gitRepoId}}{{.}}{{/gitRepoId}}{{^gitRepoId}}YOUR_GIT_REPO{{/gitRepoId}} + version: ~> {{shardVersion}} +``` + +## Development + +Install dependencies + +```shell +shards +``` + +Run the tests: + +```shell +crystal spec +``` diff --git a/modules/openapi-generator/src/main/resources/crystal/Rakefile.mustache b/modules/openapi-generator/src/main/resources/crystal/Rakefile.mustache new file mode 100644 index 000000000000..c72ca30d454e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/Rakefile.mustache @@ -0,0 +1,10 @@ +require "bundler/gem_tasks" + +begin + require 'rspec/core/rake_task' + + RSpec::Core::RakeTask.new(:spec) + task default: :spec +rescue LoadError + # no rspec available +end diff --git a/modules/openapi-generator/src/main/resources/crystal/api.mustache b/modules/openapi-generator/src/main/resources/crystal/api.mustache new file mode 100644 index 000000000000..550f017c4c62 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api.mustache @@ -0,0 +1,185 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "uri" + +module {{moduleName}} +{{#operations}} + class {{classname}} + property api_client : ApiClient + + def initialize(api_client = ApiClient.default) + @api_client = api_client + end +{{#operation}} + {{#summary}} + # {{{summary}}} + {{/summary}} + {{#notes}} + # {{{notes}}} + {{/notes}} + {{#allParams}} + {{#required}} + # @param {{paramName}} [{{{dataType}}}{{^required}}?{{/required}}] {{description}} + {{/required}} + {{/allParams}} + # @return [{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}] + def {{operationId}}({{#allParams}}{{paramName}} : {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) + {{#returnType}}data, _status_code, _headers = {{/returnType}}{{operationId}}_with_http_info({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) + {{#returnType}}data{{/returnType}}{{^returnType}}nil{{/returnType}} + end + + {{#summary}} + # {{summary}} + {{/summary}} + {{#notes}} + # {{notes}} + {{/notes}} + {{#allParams}} + {{#required}} + # @param {{paramName}} [{{{dataType}}}{{^required}}?{{/required}}] {{description}} + {{/required}} + {{/allParams}} + # @return [Array<({{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}, Integer, Hash)>] {{#returnType}}{{{returnType}}} data{{/returnType}}{{^returnType}}nil{{/returnType}}, response status code and response headers + def {{operationId}}_with_http_info({{#allParams}}{{paramName}} : {{{dataType}}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) + if @api_client.config.debugging + Log.debug {"Calling API: {{classname}}.{{operationId}} ..."} + end + {{#allParams}} + {{^isNullable}} + {{#required}} + # verify the required parameter "{{paramName}}" is set + if @api_client.config.client_side_validation && {{{paramName}}}.nil? + raise ArgumentError.new("Missing the required parameter '{{paramName}}' when calling {{classname}}.{{operationId}}") + end + {{#isEnum}} + {{^isContainer}} + # verify enum value + allowable_values = [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}] + if @api_client.config.client_side_validation && !allowable_values.include?({{{paramName}}}) + raise ArgumentError.new("invalid value for \"{{{paramName}}}\", must be one of #{allowable_values}") + end + {{/isContainer}} + {{/isEnum}} + {{/required}} + {{/isNullable}} + {{^required}} + {{#isEnum}} + {{#collectionFormat}} + allowable_values = [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}] + if @api_client.config.client_side_validation && {{{paramName}}} && {{{paramName}}}.all? { |item| allowable_values.include?(item) } + raise ArgumentError.new("invalid value for \"{{{paramName}}}\", must include one of #{allowable_values}") + end + {{/collectionFormat}} + {{^collectionFormat}} + allowable_values = [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}] + if @api_client.config.client_side_validation && {{{paramName}}} && !allowable_values.include?({{{paramName}}}]) + raise ArgumentError.new("invalid value for \"{{{paramName}}}\", must be one of #{allowable_values}") + end + {{/collectionFormat}} + {{/isEnum}} + {{/required}} + {{#hasValidation}} + {{#maxLength}} + if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.length > {{{maxLength}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be smaller than or equal to {{{maxLength}}}.") + end + + {{/maxLength}} + {{#minLength}} + if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.to_s.length < {{{minLength}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, the character length must be great than or equal to {{{minLength}}}.") + end + + {{/minLength}} + {{#maximum}} + if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.") + end + + {{/maximum}} + {{#minimum}} + if @api_client.config.client_side_validation && {{^required}}!{{{paramName}}}.nil? && {{/required}}{{{paramName}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.") + end + + {{/minimum}} + {{#pattern}} + pattern = Regexp.new({{{pattern}}}) + if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}} !~ pattern + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, must conform to the pattern #{pattern}.") + end + + {{/pattern}} + {{#maxItems}} + if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.length > {{{maxItems}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, number of items must be less than or equal to {{{maxItems}}}.") + end + + {{/maxItems}} + {{#minItems}} + if @api_client.config.client_side_validation && {{^required}}{{{paramName}}}.nil? && {{/required}}{{{paramName}}}.length < {{{minItems}}} + raise ArgumentError.new("invalid value for \"{{{paramName}}}\" when calling {{classname}}.{{operationId}}, number of items must be greater than or equal to {{{minItems}}}.") + end + + {{/minItems}} + {{/hasValidation}} + {{/allParams}} + # resource path + local_var_path = "{{{path}}}"{{#pathParams}}.sub("{" + "{{baseName}}" + "}", URI.encode({{paramName}}.to_s){{^strictSpecBehavior}}.gsub("%2F", "/"){{/strictSpecBehavior}}){{/pathParams}} + + # query parameters + query_params = Hash(Symbol, String).new + {{#queryParams}} + query_params[:"{{{baseName}}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/queryParams}} + + # header parameters + header_params = Hash(String, String).new + {{#hasProduces}} + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept([{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]) + {{/hasProduces}} + {{#hasConsumes}} + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type([{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]) + {{/hasConsumes}} + {{#headerParams}} + header_params["{{{baseName}}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/headerParams}} + + # form parameters + form_params = Hash(Symbol, String).new + {{#formParams}} + form_params[:"{{baseName}}"] = {{#collectionFormat}}@api_client.build_collection_param({{{paramName}}}, :{{{collectionFormat}}}){{/collectionFormat}}{{^collectionFormat}}{{{paramName}}}{{/collectionFormat}} + {{/formParams}} + + # http body (model) + post_body = {{#bodyParam}}{{{paramName}}}.to_json{{/bodyParam}}{{^bodyParam}}nil{{/bodyParam}} + + # return_type + return_type = {{#returnType}}"{{{.}}}"{{/returnType}}{{^returnType}}nil{{/returnType}} + + # auth_names + auth_names = {{#authMethods}}{{#-first}}[{{/-first}}"{{name}}"{{^-last}}, {{/-last}}{{#-last}}]{{/-last}}{{/authMethods}}{{^authMethods}}[] of String{{/authMethods}} + + data, status_code, headers = @api_client.call_api(:{{httpMethod}}, + local_var_path, + :"{{classname}}.{{operationId}}", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: {{classname}}#{{operationId}}\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return {{#returnType}}{{{.}}}.from_json(data){{/returnType}}{{^returnType}}nil{{/returnType}}, status_code, headers + end +{{^-last}} + +{{/-last}} +{{/operation}} + end +{{/operations}} +end diff --git a/modules/openapi-generator/src/main/resources/crystal/api_client.mustache b/modules/openapi-generator/src/main/resources/crystal/api_client.mustache new file mode 100644 index 000000000000..5352175b00e4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_client.mustache @@ -0,0 +1,402 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "json" +require "time" + +module {{moduleName}} + class ApiClient + # The Configuration object holding settings to be used in the API client. + property config : Configuration + + # Defines the headers to be used in HTTP requests of all API calls by default. + # + # @return [Hash] + property default_headers : Hash(String, String) + + # Initializes the ApiClient + # @option config [Configuration] Configuration for initializing the object, default to Configuration.default + def initialize(@config = Configuration.default) + @user_agent = "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/#{VERSION}/crystal{{/httpUserAgent}}" + @default_headers = { + "User-Agent" => @user_agent + } + end + + def self.default + @@default ||= ApiClient.new + end + + # Check if the given MIME is a JSON MIME. + # JSON MIME examples: + # application/json + # application/json; charset=UTF8 + # APPLICATION/JSON + # */* + # @param [String] mime MIME + # @return [Boolean] True if the MIME is application/json + def json_mime?(mime) + (mime == "*/*") || !(mime =~ /Application\/.*json(?!p)(;.*)?/i).nil? + end + + # Deserialize the response to the given return type. + # + # @param [Response] response HTTP response + # @param [String] return_type some examples: "User", "Array", "Hash" + def deserialize(response, return_type) + body = response.body + + # handle file downloading - return the File instance processed in request callbacks + # note that response body is empty when the file is written in chunks in request on_body callback + if return_type == "File" + content_disposition = response.headers["Content-Disposition"].to_s + if content_disposition && content_disposition =~ /filename=/i + filename = content_disposition.match(/filename=[""]?([^""\s]+)[""]?/i).try &.[0] + prefix = sanitize_filename(filename) + else + prefix = "download-" + end + if !prefix.nil? && prefix.ends_with?("-") + prefix = prefix + "-" + end + encoding = response.headers["Content-Encoding"].to_s + + # TODO add file support + raise ApiError.new(code: 0, message: "File response not yet supported in the client.") if return_type + return nil + + #@tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) + #@tempfile.write(@stream.join.force_encoding(encoding)) + #@tempfile.close + #Log.info { "Temp file written to #{@tempfile.path}, please copy the file to a proper folder "\ + # "with e.g. `FileUtils.cp(tempfile.path, \"/new/file/path\")` otherwise the temp file "\ + # "will be deleted automatically with GC. It's also recommended to delete the temp file "\ + # "explicitly with `tempfile.delete`" } + #return @tempfile + end + + return nil if body.nil? || body.empty? + + # return response body directly for String return type + return body if return_type == "String" + + # ensuring a default content type + content_type = response.headers["Content-Type"] || "application/json" + + raise ApiError.new(code: 0, message: "Content-Type is not supported: #{content_type}") unless json_mime?(content_type) + + begin + data = JSON.parse("[#{body}]")[0] + rescue e : Exception + if %w(String Date Time).includes?(return_type) + data = body + else + raise e + end + end + + convert_to_type data, return_type + end + + # Convert data to the given return type. + # @param [Object] data Data to be converted + # @param [String] return_type Return type + # @return [Mixed] Data in a particular type + def convert_to_type(data, return_type) + return nil if data.nil? + case return_type + when "String" + data.to_s + when "Integer" + data.to_s.to_i + when "Float" + data.to_s.to_f + when "Boolean" + data == true + when "Time" + # parse date time (expecting ISO 8601 format) + Time.parse! data.to_s, "%Y-%m-%dT%H:%M:%S%Z" + when "Date" + # parse date (expecting ISO 8601 format) + Time.parse! data.to_s, "%Y-%m-%d" + when "Object" + # generic object (usually a Hash), return directly + data + when /\AArray<(.+)>\z/ + # e.g. Array + sub_type = $1 + data.map { |item| convert_to_type(item, sub_type) } + when /\AHash\\z/ + # e.g. Hash + sub_type = $1 + ({} of Symbol => String).tap do |hash| + data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + end + else + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(return_type) + klass.respond_to?(:openapi_one_of) ? klass.build(data) : klass.build_from_hash(data) + end + end + + # Sanitize filename by removing path. + # e.g. ../../sun.gif becomes sun.gif + # + # @param [String] filename the filename to be sanitized + # @return [String] the sanitized filename + def sanitize_filename(filename) + if filename.nil? + return nil + else + filename.gsub(/.*[\/\\]/, "") + end + end + + def build_request_url(path : String, operation : Symbol) + # Add leading and trailing slashes to path + path = "/#{path}".gsub(/\/+/, "/") + @config.base_url(operation) + path + end + + # Update hearder and query params based on authentication settings. + # + # @param [Hash] header_params Header parameters + # @param [Hash] query_params Query parameters + # @param [String] auth_names Authentication scheme name + def update_params_for_auth!(header_params, query_params, auth_names) + Array{auth_names}.each do |auth_name| + auth_setting = @config.auth_settings[auth_name] + next unless auth_setting + case auth_setting[:in] + when "header" then header_params[auth_setting[:key]] = auth_setting[:value] + when "query" then query_params[auth_setting[:key]] = auth_setting[:value] + else raise ArgumentError.new("Authentication token must be in `query` of `header`") + end + end + end + + # Sets user agent in HTTP header + # + # @param [String] user_agent User agent (e.g. openapi-generator/ruby/1.0.0) + def user_agent=(user_agent) + @user_agent = user_agent + @default_headers["User-Agent"] = @user_agent + end + + # Return Accept header based on an array of accepts provided. + # @param [Array] accepts array for Accept + # @return [String] the Accept header (e.g. application/json) + def select_header_accept(accepts) : String + #return nil if accepts.nil? || accepts.empty? + # use JSON when present, otherwise use all of the provided + json_accept = accepts.find { |s| json_mime?(s) } + if json_accept.nil? + accepts.join(",") + else + json_accept + end + end + + # Return Content-Type header based on an array of content types provided. + # @param [Array] content_types array for Content-Type + # @return [String] the Content-Type header (e.g. application/json) + def select_header_content_type(content_types) + # use application/json by default + return "application/json" if content_types.nil? || content_types.empty? + # use JSON when present, otherwise use the first one + json_content_type = content_types.find { |s| json_mime?(s) } + json_content_type || content_types.first + end + + # Convert object (array, hash, object, etc) to JSON string. + # @param [Object] model object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_http_body(model) + return model if model.nil? || model.is_a?(String) + local_body = nil + if model.is_a?(Array) + local_body = model.map { |m| object_to_hash(m) } + else + local_body = object_to_hash(model) + end + local_body.to_json + end + + # Convert object(non-array) to hash. + # @param [Object] obj object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_hash(obj) + if obj.respond_to?(:to_hash) + obj.to_hash + else + obj + end + end + + # Build parameter value according to the given collection format. + # @param [String] collection_format one of :csv, :ssv, :tsv, :pipes and :multi + def build_collection_param(param, collection_format) + case collection_format + when :csv + param.join(",") + when :ssv + param.join(" ") + when :tsv + param.join("\t") + when :pipes + param.join("|") + when :multi + # return the array directly as typhoeus will handle it as expected + param + else + fail "unknown collection format: #{collection_format.inspect}" + end + end + + # Call an API with given options. + # + # @return [Array<(Object, Integer, Hash)>] an array of 3 elements: + # the data deserialized from response body (could be nil), response status code and response headers. + def call_api(http_method : Symbol, path : String, operation : Symbol, return_type : String, post_body : String?, auth_names = [] of String, header_params = {} of String => String, query_params = {} of Symbol => String, form_params = {} of Symbol => String) + #ssl_options = { + # :ca_file => @config.ssl_ca_file, + # :verify => @config.ssl_verify, + # :verify_mode => @config.ssl_verify_mode, + # :client_cert => @config.ssl_client_cert, + # :client_key => @config.ssl_client_key + #} + + #connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn| + # conn.basic_auth(config.username, config.password) + # if opts[:header_params]["Content-Type"] == "multipart/form-data" + # conn.request :multipart + # conn.request :url_encoded + # end + # conn.adapter(Faraday.default_adapter) + #end + + if !post_body.nil? && !post_body.empty? + # use JSON string in the payload + form_or_body = post_body + else + # use HTTP forms in the payload + # TDOD use HTTP form encoding + form_or_body = form_params + end + + request = Crest::Request.new(http_method, + build_request_url(path, operation), + params: query_params, + headers: header_params, + #cookies: cookie_params, # TODO add cookies support + form: form_or_body, + logging: @config.debugging, + handle_errors: false + ) + + response = request.execute + + if @config.debugging + Log.debug {"HTTP response body ~BEGIN~\n#{response.body}\n~END~\n"} + end + + if !response.success? + if response.status == 0 + # Errors from libcurl will be made visible here + raise ApiError.new(code: 0, + message: response.body) + else + raise ApiError.new(code: response.status_code, + response_headers: response.headers, + message: response.body) + end + end + + return response.body, response.status_code, response.headers + end + + # Builds the HTTP request + # + # @param [String] http_method HTTP method/verb (e.g. POST) + # @param [String] path URL path (e.g. /account/new) + # @option opts [Hash] :header_params Header parameters + # @option opts [Hash] :query_params Query parameters + # @option opts [Hash] :form_params Query parameters + # @option opts [Object] :body HTTP body (JSON/XML) + # @return [Typhoeus::Request] A Typhoeus Request + def build_request(http_method, path, request, opts = {} of Symbol => String) + url = build_request_url(path, opts) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {} of Symbole => String) + query_params = opts[:query_params] || {} of Symbol => String + form_params = opts[:form_params] || {} of Symbol => String + + update_params_for_auth! header_params, query_params, opts[:auth_names] + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :verbose => @config.debugging + } + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update body: req_body + if @config.debugging + Log.debug {"HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n"} + end + end + request.headers = header_params + request.body = req_body + request.url url + request.params = query_params + download_file(request) if opts[:return_type] == "File" + request + end + + # Builds the HTTP request body + # + # @param [Hash] header_params Header parameters + # @param [Hash] form_params Query parameters + # @param [Object] body HTTP body (JSON/XML) + # @return [String] HTTP body data in the form of string + def build_request_body(header_params, form_params, body) + # http form + if header_params["Content-Type"] == "application/x-www-form-urlencoded" + data = URI.encode_www_form(form_params) + elsif header_params["Content-Type"] == "multipart/form-data" + data = {} of Symbol => String + form_params.each do |key, value| + case value + when ::File, ::Tempfile + # TODO hardcode to application/octet-stream, need better way to detect content type + data[key] = Faraday::UploadIO.new(value.path, "application/octet-stream", value.path) + when ::Array, nil + # let Faraday handle Array and nil parameters + data[key] = value + else + data[key] = value.to_s + end + end + elsif body + data = body.is_a?(String) ? body : body.to_json + else + data = nil + end + data + end + + # TODO fix streaming response + #def download_file(request) + # @stream = [] + + # # handle streaming Responses + # request.options.on_data = Proc.new do |chunk, overall_received_bytes| + # @stream << chunk + # end + #end + end +end diff --git a/modules/openapi-generator/src/main/resources/crystal/api_client_faraday_partial.mustache b/modules/openapi-generator/src/main/resources/crystal/api_client_faraday_partial.mustache new file mode 100644 index 000000000000..dfef217e0374 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_client_faraday_partial.mustache @@ -0,0 +1,138 @@ + # Call an API with given options. + # + # @return [Array<(Object, Integer, Hash)>] an array of 3 elements: + # the data deserialized from response body (could be nil), response status code and response headers. + def call_api(http_method, path, opts = {} of Symbol => String) + ssl_options = { + :ca_file => @config.ssl_ca_file, + :verify => @config.ssl_verify, + :verify_mode => @config.ssl_verify_mode, + :client_cert => @config.ssl_client_cert, + :client_key => @config.ssl_client_key + } + + connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn| + conn.basic_auth(config.username, config.password) + if opts[:header_params]["Content-Type"] == "multipart/form-data" + conn.request :multipart + conn.request :url_encoded + end + conn.adapter(Faraday.default_adapter) + end + + begin + response = connection.public_send(http_method.to_sym.downcase) do |req| + build_request(http_method, path, req, opts) + end + + if @config.debugging + Log.debug {"HTTP response body ~BEGIN~\n#{response.body}\n~END~\n"} + end + + unless response.success? + if response.status == 0 + # Errors from libcurl will be made visible here + fail ApiError.new(code: 0, + message: response.return_message) + else + fail ApiError.new(code: response.status, + response_headers: response.headers, + response_body: response.body), + response.reason_phrase + end + end + rescue Faraday::TimeoutError + fail ApiError.new("Connection timed out") + end + + if opts[:return_type] + data = deserialize(response, opts[:return_type]) + else + data = nil + end + return data, response.status, response.headers + end + + # Builds the HTTP request + # + # @param [String] http_method HTTP method/verb (e.g. POST) + # @param [String] path URL path (e.g. /account/new) + # @option opts [Hash] :header_params Header parameters + # @option opts [Hash] :query_params Query parameters + # @option opts [Hash] :form_params Query parameters + # @option opts [Object] :body HTTP body (JSON/XML) + # @return [Typhoeus::Request] A Typhoeus Request + def build_request(http_method, path, request, opts = {} of Symbol => String) + url = build_request_url(path, opts) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {} of Symbole => String) + query_params = opts[:query_params] || {} of Symbol => String + form_params = opts[:form_params] || {} of Symbol => String + + update_params_for_auth! header_params, query_params, opts[:auth_names] + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :verbose => @config.debugging + } + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update body: req_body + if @config.debugging + Log.debug {"HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n"} + end + end + request.headers = header_params + request.body = req_body + request.url url + request.params = query_params + download_file(request) if opts[:return_type] == "File" + request + end + + # Builds the HTTP request body + # + # @param [Hash] header_params Header parameters + # @param [Hash] form_params Query parameters + # @param [Object] body HTTP body (JSON/XML) + # @return [String] HTTP body data in the form of string + def build_request_body(header_params, form_params, body) + # http form + if header_params["Content-Type"] == "application/x-www-form-urlencoded" + data = URI.encode_www_form(form_params) + elsif header_params["Content-Type"] == "multipart/form-data" + data = {} of Symbol => String + form_params.each do |key, value| + case value + when ::File, ::Tempfile + # TODO hardcode to application/octet-stream, need better way to detect content type + data[key] = Faraday::UploadIO.new(value.path, "application/octet-stream", value.path) + when ::Array, nil + # let Faraday handle Array and nil parameters + data[key] = value + else + data[key] = value.to_s + end + end + elsif body + data = body.is_a?(String) ? body : body.to_json + else + data = nil + end + data + end + + def download_file(request) + @stream = [] + + # handle streaming Responses + request.options.on_data = Proc.new do |chunk, overall_received_bytes| + @stream << chunk + end + end diff --git a/modules/openapi-generator/src/main/resources/crystal/api_client_typhoeus_partial.mustache b/modules/openapi-generator/src/main/resources/crystal/api_client_typhoeus_partial.mustache new file mode 100644 index 000000000000..d8e95e774618 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_client_typhoeus_partial.mustache @@ -0,0 +1,153 @@ + # Call an API with given options. + # + # @return [Array<(Object, Integer, Hash)>] an array of 3 elements: + # the data deserialized from response body (could be nil), response status code and response headers. + def call_api(http_method, path, opts = {} of Symbol => String) + request = build_request(http_method, path, opts) + response = request.run + + if @config.debugging + @config.logger.debug "HTTP response body ~BEGIN~\n#{response.body}\n~END~\n" + end + + unless response.success? + if response.timed_out? + fail ApiError.new("Connection timed out") + elsif response.code == 0 + # Errors from libcurl will be made visible here + fail ApiError.new(code: 0, + message: response.return_message) + else + fail ApiError.new(code: response.code, + response_headers: response.headers, + response_body: response.body), + response.status_message + end + end + + if opts[:return_type] + data = deserialize(response, opts[:return_type]) + else + data = nil + end + return data, response.code, response.headers + end + + # Builds the HTTP request + # + # @param [String] http_method HTTP method/verb (e.g. POST) + # @param [String] path URL path (e.g. /account/new) + # @option opts [Hash] :header_params Header parameters + # @option opts [Hash] :query_params Query parameters + # @option opts [Hash] :form_params Query parameters + # @option opts [Object] :body HTTP body (JSON/XML) + # @return [Typhoeus::Request] A Typhoeus Request + def build_request(http_method, path, opts = {} of Symbol => String) + url = build_request_url(path, opts) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {} of Symbol => String) + query_params = opts[:query_params] || {} of Symbol => String + form_params = opts[:form_params] || {} of Symbol => String + + {{#hasAuthMethods}} + update_params_for_auth! header_params, query_params, opts[:auth_names] + {{/hasAuthMethods}} + + # set ssl_verifyhosts option based on @config.verify_ssl_host (true/false) + _verify_ssl_host = @config.verify_ssl_host ? 2 : 0 + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :ssl_verifypeer => @config.verify_ssl, + :ssl_verifyhost => _verify_ssl_host, + :sslcert => @config.cert_file, + :sslkey => @config.key_file, + :verbose => @config.debugging + } + + # set custom cert, if provided + req_opts[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update body: req_body + if @config.debugging + @config.logger.debug "HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n" + end + end + + request = Typhoeus::Request.new(url, req_opts) + download_file(request) if opts[:return_type] == "File" + request + end + + # Builds the HTTP request body + # + # @param [Hash] header_params Header parameters + # @param [Hash] form_params Query parameters + # @param [Object] body HTTP body (JSON/XML) + # @return [String] HTTP body data in the form of string + def build_request_body(header_params, form_params, body) + # http form + if header_params["Content-Type"] == "application/x-www-form-urlencoded" || + header_params["Content-Type"] == "multipart/form-data" + data = {} of Symbol => String + form_params.each do |key, value| + case value + when ::File, ::Array, nil + # let typhoeus handle File, Array and nil parameters + data[key] = value + else + data[key] = value.to_s + end + end + elsif body + data = body.is_a?(String) ? body : body.to_json + else + data = nil + end + data + end + + # Save response body into a file in (the defined) temporary folder, using the filename + # from the "Content-Disposition" header if provided, otherwise a random filename. + # The response body is written to the file in chunks in order to handle files which + # size is larger than maximum Ruby String or even larger than the maximum memory a Ruby + # process can use. + # + # @see Configuration#temp_folder_path + def download_file(request) + tempfile = nil + encoding = nil + request.on_headers do |response| + content_disposition = response.headers["Content-Disposition"] + if content_disposition && content_disposition =~ /filename=/i + filename = content_disposition[/filename=[""]?([^""\s]+)[""]?/, 1] + prefix = sanitize_filename(filename) + else + prefix = "download-" + end + prefix = prefix + "-" unless prefix.end_with?("-") + encoding = response.body.encoding + tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) + @tempfile = tempfile + end + request.on_body do |chunk| + chunk.force_encoding(encoding) + tempfile.write(chunk) + end + request.on_complete do |response| + if tempfile + tempfile.close + @config.logger.info "Temp file written to #{tempfile.path}, please copy the file to a proper folder "\ + "with e.g. `FileUtils.cp(tempfile.path, \"/new/file/path\")` otherwise the temp file "\ + "will be deleted automatically with GC. It's also recommended to delete the temp file "\ + "explicitly with `tempfile.delete`" + end + end + end diff --git a/modules/openapi-generator/src/main/resources/crystal/api_doc.mustache b/modules/openapi-generator/src/main/resources/crystal/api_doc.mustache new file mode 100644 index 000000000000..bdeeb5689122 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_doc.mustache @@ -0,0 +1,118 @@ +# {{moduleName}}::{{classname}}{{#description}} + +{{description}}{{/description}} + +All URIs are relative to *{{basePath}}* + +| Method | HTTP request | Description | +| ------ | ------------ | ----------- | +{{#operations}} +{{#operation}} +| [**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}} | +{{/operation}} +{{/operations}} + +{{#operations}} +{{#operation}} + +## {{operationId}} + +> {{#returnType}}{{#returnTypeIsPrimitive}}{{returnType}}{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}<{{{returnType}}}>{{/returnTypeIsPrimitive}} {{/returnType}}{{operationId}}{{#hasParams}}({{#requiredParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#optionalParams}}{{#-last}}{{#hasRequiredParams}}, {{/hasRequiredParams}}opts{{/-last}}{{/optionalParams}}){{/hasParams}} + +{{{summary}}}{{#notes}} + +{{{notes}}}{{/notes}} + +### Examples + +```ruby +require 'time' +require '{{{gemName}}}' +{{#hasAuthMethods}} +# setup authorization +{{{moduleName}}}.configure do |config|{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} + # Configure HTTP basic authorization: {{{name}}} + config.username = 'YOUR USERNAME' + config.password = 'YOUR PASSWORD'{{/isBasicBasic}}{{#isBasicBearer}} + # Configure Bearer authorization{{#bearerFormat}} ({{{.}}}){{/bearerFormat}}: {{{name}}} + config.access_token = 'YOUR_BEARER_TOKEN'{{/isBasicBearer}}{{/isBasic}}{{#isApiKey}} + # Configure API key authorization: {{{name}}} + config.api_key['{{{keyParamName}}}'] = 'YOUR API KEY' + # Uncomment the following line to set a prefix for the API key, e.g. 'Bearer' (defaults to nil) + # config.api_key_prefix['{{{keyParamName}}}'] = 'Bearer'{{/isApiKey}}{{#isOAuth}} + # Configure OAuth2 access token for authorization: {{{name}}} + config.access_token = 'YOUR ACCESS TOKEN'{{/isOAuth}} +{{/authMethods}}end +{{/hasAuthMethods}} + +api_instance = {{{moduleName}}}::{{{classname}}}.new +{{#requiredParams}} +{{{paramName}}} = {{{vendorExtensions.x-ruby-example}}} # {{{dataType}}} | {{{description}}} +{{/requiredParams}} +{{#optionalParams}} +{{#-first}} +opts = { +{{/-first}} + {{{paramName}}}: {{{vendorExtensions.x-ruby-example}}}{{^-last}},{{/-last}} # {{{dataType}}} | {{{description}}} +{{#-last}} +} +{{/-last}} +{{/optionalParams}} + +begin + {{#summary}}# {{{.}}}{{/summary}} + {{#returnType}}result = {{/returnType}}api_instance.{{{operationId}}}{{#hasParams}}({{#requiredParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#optionalParams}}{{#-last}}{{#hasRequiredParams}}, {{/hasRequiredParams}}opts{{/-last}}{{/optionalParams}}){{/hasParams}} + {{#returnType}} + p result + {{/returnType}} +rescue {{{moduleName}}}::ApiError => e + puts "Error when calling {{classname}}->{{{operationId}}}: #{e}" +end +``` + +#### Using the {{operationId}}_with_http_info variant + +This returns an Array which contains the response data{{^returnType}} (`nil` in this case){{/returnType}}, status code and headers. + +> {{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}nil{{/returnType}}, Integer, Hash)> {{operationId}}_with_http_info{{#hasParams}}({{#requiredParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#optionalParams}}{{#-last}}{{#hasRequiredParams}}, {{/hasRequiredParams}}opts{{/-last}}{{/optionalParams}}){{/hasParams}} + +```ruby +begin + {{#summary}}# {{{.}}}{{/summary}} + data, status_code, headers = api_instance.{{{operationId}}}_with_http_info{{#hasParams}}({{#requiredParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#optionalParams}}{{#-last}}{{#hasRequiredParams}}, {{/hasRequiredParams}}opts{{/-last}}{{/optionalParams}}){{/hasParams}} + p status_code # => 2xx + p headers # => { ... } + p data # => {{#returnType}}{{#returnTypeIsPrimitive}}{{returnType}}{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}<{{{returnType}}}>{{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}nil{{/returnType}} +rescue {{{moduleName}}}::ApiError => e + puts "Error when calling {{classname}}->{{{operationId}}}_with_http_info: #{e}" +end +``` + +### Parameters + +{{^allParams}} +This endpoint does not need any parameter. +{{/allParams}} +{{#allParams}} +{{#-first}} +| Name | Type | Description | Notes | +| ---- | ---- | ----------- | ----- | +{{/-first}} +| **{{paramName}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isFile}}**{{dataType}}**{{/isFile}}{{^isFile}}[**{{dataType}}**]({{baseType}}.md){{/isFile}}{{/isPrimitiveType}} | {{description}} | {{^required}}[optional]{{/required}}{{#defaultValue}}[default to {{defaultValue}}]{{/defaultValue}} | +{{/allParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{returnType}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{returnType}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}}nil (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{name}}](../README.md#{{name}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + +- **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} +- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +{{/operation}} +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/crystal/api_error.mustache b/modules/openapi-generator/src/main/resources/crystal/api_error.mustache new file mode 100644 index 000000000000..fd39b5bef277 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_error.mustache @@ -0,0 +1,33 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +module {{moduleName}} + class ApiError < Exception + getter code : Int32? + getter response_headers : Hash(String, Array(String) | String)? + + # Usage examples: + # ApiError.new + # ApiError.new(message: "message") + # ApiError.new(code: 500, response_headers: {}, message: "") + # ApiError.new(code: 404, message: "Not Found") + def initialize(@code , @message, @response_headers) + end + + def initialize(@code , @message) + end + + # Override to_s to display a friendly error message + def to_s + msg = "" + msg = msg + "\nHTTP status code: #{code}" if @code + msg = msg + "\nResponse headers: #{response_headers}" if @response_headers + if @message.nil? || @message.empty? + msg = msg + "\nError message: the server returns an error but the HTTP respone body is empty." + else + msg = msg + "\nResponse body: #{@message}" + end + + msg + end + end +end diff --git a/modules/openapi-generator/src/main/resources/crystal/api_info.mustache b/modules/openapi-generator/src/main/resources/crystal/api_info.mustache new file mode 100644 index 000000000000..1b3f9cb5ac45 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_info.mustache @@ -0,0 +1,12 @@ +{{#appName}} +#{{{appName}}} + +{{/appName}} +{{#appDescription}} +#{{{appDescription}}} + +{{/appDescription}} +{{#version}}The version of the OpenAPI document: {{version}}{{/version}} +{{#infoEmail}}Contact: {{{infoEmail}}}{{/infoEmail}} +Generated by: https://openapi-generator.tech +OpenAPI Generator version: {{{generatorVersion}}} diff --git a/modules/openapi-generator/src/main/resources/crystal/api_test.mustache b/modules/openapi-generator/src/main/resources/crystal/api_test.mustache new file mode 100644 index 000000000000..f4e0eb49e648 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/api_test.mustache @@ -0,0 +1,38 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for {{moduleName}}::{{classname}} +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +{{#operations}}describe "{{classname}}" do + describe "test an instance of {{classname}}" do + it "should create an instance of {{classname}}" do + api_instance = {{moduleName}}::{{classname}}.new + # TODO expect(api_instance).to be_instance_of({{moduleName}}::{{classname}}) + end + end + +{{#operation}} + # unit tests for {{operationId}} + {{#summary}} + # {{summary}} + {{/summary}} + {{#notes}} + # {{notes}} + {{/notes}} +{{#allParams}}{{#required}} # @param {{paramName}} {{description}} +{{/required}}{{/allParams}} # @param [Hash] opts the optional parameters +{{#allParams}}{{^required}} # @option opts [{{{dataType}}}] :{{paramName}} {{description}} +{{/required}}{{/allParams}} # @return [{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}nil{{/returnType}}] + describe "{{operationId}} test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +{{/operation}} +end +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/crystal/base_object.mustache b/modules/openapi-generator/src/main/resources/crystal/base_object.mustache new file mode 100644 index 000000000000..c7abd09b36bd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/base_object.mustache @@ -0,0 +1,120 @@ + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + {{#parent}} + super(attributes) + {{/parent}} + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = {{moduleName}}.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {{^parent}}{} of Symbol => String{{/parent}}{{#parent}}super{{/parent}} + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end diff --git a/modules/openapi-generator/src/main/resources/crystal/configuration.mustache b/modules/openapi-generator/src/main/resources/crystal/configuration.mustache new file mode 100644 index 000000000000..a3a815fc0421 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/configuration.mustache @@ -0,0 +1,356 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "log" + +module {{moduleName}} + class Configuration + # Defines url scheme + property scheme : String + + # Defines url host + property host : String + + # Defines url base path + property base_path : String + + # Define server configuration index + property server_index : Int32 + + # Define server operation configuration index + property server_operation_index : Hash(Symbol, String) + + # Default server variables + property server_variables : Hash(Symbol, String) + + # Default server operation variables + property server_operation_variables : Hash(Symbol, String) + + # Defines API keys used with API Key authentications. + # + # @return [Hash] key: parameter name, value: parameter value (API key) + # + # @example parameter name is "api_key", API key is "xxx" (e.g. "api_key=xxx" in query string) + # config.api_key[:"api_key"] = "xxx" + property api_key : Hash(Symbol, String) + + # Defines API key prefixes used with API Key authentications. + # + # @return [Hash] key: parameter name, value: API key prefix + # + # @example parameter name is "Authorization", API key prefix is "Token" (e.g. "Authorization: Token xxx" in headers) + # config.api_key_prefix[:"api_key"] = "Token" + property api_key_prefix : Hash(Symbol, String) + + # Defines the username used with HTTP basic authentication. + # + # @return [String] + property username : String? + + # Defines the password used with HTTP basic authentication. + # + # @return [String] + property password : String? + + # Defines the access token (Bearer) used with OAuth2. + property access_token : String? + + # Set this to enable/disable debugging. When enabled (set to true), HTTP request/response + # details will be logged with `logger.debug` (see the `logger` attribute). + # Default to false. + # + # @return [true, false] + property debugging : Bool + + # Defines the temporary folder to store downloaded files + # (for API endpoints that have file response). + # Default to use `Tempfile`. + # + # @return [String] + property temp_folder_path : String? + + # The time limit for HTTP request in seconds. + # Default to 0 (never times out). + property timeout : Int32 + + # Set this to false to skip client side validation in the operation. + # Default to true. + # @return [true, false] + property client_side_validation : Bool + + ### TLS/SSL setting + # Set this to false to skip verifying SSL certificate when calling API from https server. + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + #TODO attr_accessor :verify_ssl + + ### TLS/SSL setting + # Set this to false to skip verifying SSL host name + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + # TODO attr_accessor :verify_ssl_host + + ### TLS/SSL setting + # Set this to customize the certificate file to verify the peer. + # + # @return [String] the path to the certificate file + # + # @see The `cainfo` option of Typhoeus, `--cert` option of libcurl. Related source code: + # https://github.com/typhoeus/typhoeus/blob/master/lib/typhoeus/easy_factory.rb#L145 + # TODO attr_accessor :ssl_ca_cert + + ### TLS/SSL setting + # Client certificate file (for client certificate) + # TODO attr_accessor :cert_file + + ### TLS/SSL setting + # Client private key file (for client certificate) + # TODO attr_accessor :key_file + + # Set this to customize parameters encoding of array parameter with multi collectionFormat. + # Default to Nil. + # + # @see The params_encoding option of Ethon. Related source code: + # https://github.com/typhoeus/ethon/blob/master/lib/ethon/easy/queryable.rb#L96 + #property params_encoding : String? + + def initialize + @scheme = "{{scheme}}" + @host = "{{host}}{{#port}}:{{{.}}}{{/port}}" + @base_path = "{{contextPath}}" + @server_index = 0 + @server_operation_index = {} of Symbol => String + @server_variables = {} of Symbol => String + @server_operation_variables = {} of Symbol => String + @api_key = {} of Symbol => String + @api_key_prefix = {} of Symbol => String + @timeout = 0 + @client_side_validation = true + @verify_ssl = true + @verify_ssl_host = true + #@params_encoding = nil + #@cert_file = nil + #@key_file = nil + @debugging = false + @username = nil + @password = nil + @access_token = nil + @temp_folder_path = nil + + # TODO revise below to support block + #yield(self) if block_given? + end + + # The default Configuration object. + def self.default + @@default ||= Configuration.new + end + + def configure + yield(self) if block_given? + end + + def scheme=(scheme) + # remove :// from scheme + @scheme = scheme.sub(/:\/\//, "") + end + + def host=(host) + # remove http(s):// and anything after a slash + @host = host.sub(/https?:\/\//, "").split("/").first + end + + def base_path=(base_path) + # Add leading and trailing slashes to base_path + @base_path = "/#{base_path}".gsub(/\/+/, "/") + @base_path = "" if @base_path == "/" + end + + # Returns base URL for specified operation based on server settings + def base_url(operation = Nil) + # TODO revise below to support operation-level server setting + #index = server_operation_index.fetch(operation, server_index) + return "#{scheme}://#{[host, base_path].join("/").gsub(/\/+/, "/")}".sub(/\/+\z/, "") #if index == Nil + + #server_url(index, server_operation_variables.fetch(operation, server_variables), operation_server_settings[operation]) + end + + # Gets API key (with prefix if set). + # @param [String] param_name the parameter name of API key auth + def api_key_with_prefix(param_name) + if @api_key_prefix[param_name] + "#{@api_key_prefix[param_name]} #{@api_key[param_name]}" + else + @api_key[param_name] + end + end + + # Gets Basic Auth token string + def basic_auth_token + "Basic " + ["#{username}:#{password}"].pack("m").delete("\r\n") + end + + # Returns Auth Settings hash for api client. + def auth_settings + Hash{ {{#authMethods}}{{#isApiKey}}"{{name}}" => { + type: "api_key", + in: {{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{#isKeyInQuery}}"query"{{/isKeyInQuery}}, + key: "{{keyParamName}}", + value: api_key_with_prefix("{{keyParamName}}") + }, +{{/isApiKey}} +{{#isBasic}} +{{#isBasicBasic}} + "{{name}}" => + { + type: "basic", + in: "header", + key: "Authorization", + value: basic_auth_token + }, +{{/isBasicBasic}} +{{#isBasicBearer}} + "{{name}}" => + { + type: "bearer", + in: "header", + {{#bearerFormat}} + format: "{{{.}}}", + {{/bearerFormat}} + key: "Authorization", + value: "Bearer #{access_token}" + }, +{{/isBasicBearer}} +{{/isBasic}} +{{#isOAuth}} + "{{name}}" => + { + type: "oauth2", + in: "header", + key: "Authorization", + value: "Bearer #{access_token}" + }, +{{/isOAuth}} +{{/authMethods}} + } + end + + # Returns an array of Server setting + def server_settings + [ + {{#servers}} + { + url: "{{{url}}}", + description: "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + variables: { + {{/-first}} + {{{name}}}: { + description: "{{{description}}}{{^description}}No description provided{{/description}}", + default_value: "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + enum_values: [ + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ] + {{/-last}} + {{/enumValues}} + }{{^-last}},{{/-last}} + {{#-last}} + } + {{/-last}} + {{/variables}} + }{{^-last}},{{/-last}} + {{/servers}} + ] + end + + def operation_server_settings + {{#apiInfo}} + {{#apis}} + {{#operations}} + {{#operation}} + {{#servers}} + {{#-first}} + { + "{{{classname}}}.{{{nickname}}}": [ + {{/-first}} + { + url: "{{{url}}}", + description: "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + variables: { + {{/-first}} + {{{name}}}: { + description: "{{{description}}}{{^description}}No description provided{{/description}}", + default_value: "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + enum_values: [ + {{/-first}} + "{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ] + {{/-last}} + {{/enumValues}} + }{{^-last}},{{/-last}} + {{#-last}} + } + {{/-last}} + {{/variables}} + }{{^-last}},{{/-last}} + {{#-last}} + ], + } + {{/-last}} + {{/servers}} + {{/operation}} + {{/operations}} + {{/apis}} + {{/apiInfo}} + end + + # Returns URL based on server settings + # + # @param index array index of the server settings + # @param variables hash of variable and the corresponding value + def server_url(index, variables = {} of Symbol => String, servers = Nil) + servers = server_settings if servers == Nil + + # check array index out of bound + if (index < 0 || index >= servers.size) + raise ArgumentError.new("Invalid index #{index} when selecting the server. Must be less than #{servers.size}") + end + + server = servers[index] + url = server[:url] + + return url unless server.key? :variables + + # go through variable and assign a value + server[:variables].each do |name, variable| + if variables.key?(name) + if (!server[:variables][name].key?(:enum_values) || server[:variables][name][:enum_values].include?(variables[name])) + url.gsub! "{" + name.to_s + "}", variables[name] + else + raise ArgumentError.new("The variable `#{name}` in the server URL has invalid value #{variables[name]}. Must be #{server[:variables][name][:enum_values]}.") + end + else + # use default value + url.gsub! "{" + name.to_s + "}", server[:variables][name][:default_value] + end + end + + url + end + end +end diff --git a/modules/openapi-generator/src/main/resources/crystal/configuration_spec.mustache b/modules/openapi-generator/src/main/resources/crystal/configuration_spec.mustache new file mode 100644 index 000000000000..22a113e8e324 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/configuration_spec.mustache @@ -0,0 +1,34 @@ +=begin +{{> api_info}} +=end + +require 'spec_helper' + +describe {{moduleName}}::Configuration do + let(:config) { {{moduleName}}::Configuration.default } + + before(:each) do + # uncomment below to setup host and base_path + # require 'URI' + # uri = URI.parse("{{{basePath}}}") + # {{moduleName}}.configure do |c| + # c.host = uri.host + # c.base_path = uri.path + # end + end + + describe '#base_url' do + it 'should have the default value' do + # uncomment below to test default value of the base path + # expect(config.base_url).to eq("{{{basePath}}}") + end + + it 'should remove trailing slashes' do + [nil, '', '/', '//'].each do |base_path| + config.base_path = base_path + # uncomment below to test trailing slashes + # expect(config.base_url).to eq("{{{basePath}}}") + end + end + end +end diff --git a/modules/openapi-generator/src/main/resources/crystal/configuration_tls_faraday_partial.mustache b/modules/openapi-generator/src/main/resources/crystal/configuration_tls_faraday_partial.mustache new file mode 100644 index 000000000000..c35c988f9330 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/configuration_tls_faraday_partial.mustache @@ -0,0 +1,29 @@ + ### TLS/SSL setting + # Set this to false to skip verifying SSL certificate when calling API from https server. + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + #TODO attr_accessor :ssl_verify + + ### TLS/SSL setting + # Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html) + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + #TODO attr_accessor :ssl_verify_mode + + ### TLS/SSL setting + # Set this to customize the certificate file to verify the peer. + # + # @return [String] the path to the certificate file + #TODO attr_accessor :ssl_ca_file + + ### TLS/SSL setting + # Client certificate file (for client certificate) + #TODO attr_accessor :ssl_client_cert + + ### TLS/SSL setting + # Client private key file (for client certificate) + #TODO attr_accessor :ssl_client_key diff --git a/modules/openapi-generator/src/main/resources/crystal/configuration_tls_typhoeus_partial.mustache b/modules/openapi-generator/src/main/resources/crystal/configuration_tls_typhoeus_partial.mustache new file mode 100644 index 000000000000..1ab1b2d03079 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/configuration_tls_typhoeus_partial.mustache @@ -0,0 +1,34 @@ + ### TLS/SSL setting + # Set this to false to skip verifying SSL certificate when calling API from https server. + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + #TODO attr_accessor :verify_ssl + + ### TLS/SSL setting + # Set this to false to skip verifying SSL host name + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + # TODO attr_accessor :verify_ssl_host + + ### TLS/SSL setting + # Set this to customize the certificate file to verify the peer. + # + # @return [String] the path to the certificate file + # + # @see The `cainfo` option of Typhoeus, `--cert` option of libcurl. Related source code: + # https://github.com/typhoeus/typhoeus/blob/master/lib/typhoeus/easy_factory.rb#L145 + # TODO attr_accessor :ssl_ca_cert + + ### TLS/SSL setting + # Client certificate file (for client certificate) + # TODO attr_accessor :cert_file + + ### TLS/SSL setting + # Client private key file (for client certificate) + # TODO attr_accessor :key_file diff --git a/modules/openapi-generator/src/main/resources/crystal/git_push.sh.mustache b/modules/openapi-generator/src/main/resources/crystal/git_push.sh.mustache new file mode 100755 index 000000000000..8b3f689c9121 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/git_push.sh.mustache @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="{{{gitHost}}}" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="{{{gitUserId}}}" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="{{{gitRepoId}}}" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="{{{releaseNote}}}" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/modules/openapi-generator/src/main/resources/crystal/gitignore.mustache b/modules/openapi-generator/src/main/resources/crystal/gitignore.mustache new file mode 100644 index 000000000000..05a17cb8f0a0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/gitignore.mustache @@ -0,0 +1,39 @@ +# Generated by: https://openapi-generator.tech +# + +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +## Specific to RubyMotion: +.dat* +.repl_history +build/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc diff --git a/modules/openapi-generator/src/main/resources/crystal/model.mustache b/modules/openapi-generator/src/main/resources/crystal/model.mustache new file mode 100644 index 000000000000..a19cc5f3f6a6 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/model.mustache @@ -0,0 +1,23 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "time" + +module {{moduleName}} +{{#models}} +{{#model}} +{{#isEnum}} +{{>partial_model_enum_class}} +{{/isEnum}} +{{^isEnum}} +{{#oneOf}} +{{#-first}} +{{>partial_oneof_module}} +{{/-first}} +{{/oneOf}} +{{^oneOf}} +{{>partial_model_generic}} +{{/oneOf}} +{{/isEnum}} +{{/model}} +{{/models}} +end diff --git a/modules/openapi-generator/src/main/resources/crystal/model_doc.mustache b/modules/openapi-generator/src/main/resources/crystal/model_doc.mustache new file mode 100644 index 000000000000..37809685d1c0 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/model_doc.mustache @@ -0,0 +1,12 @@ +{{#models}} +{{#model}} +{{#oneOf}} +{{#-first}} +{{>partial_oneof_module_doc}} +{{/-first}} +{{/oneOf}} +{{^oneOf}} +{{>partial_model_generic_doc}} +{{/oneOf}} +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/crystal/model_test.mustache b/modules/openapi-generator/src/main/resources/crystal/model_test.mustache new file mode 100644 index 000000000000..3cc572b58788 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/model_test.mustache @@ -0,0 +1,75 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for {{moduleName}}::{{classname}} +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +{{#models}} +{{#model}} +describe {{moduleName}}::{{classname}} do +{{^oneOf}} + + describe "test an instance of {{classname}}" do + it "should create an instance of {{classname}}" do + #instance = {{moduleName}}::{{classname}}.new + #expect(instance).to be_instance_of({{moduleName}}::{{classname}}) + end + end +{{#vars}} + describe "test attribute '{{{name}}}'" do + it "should work" do + {{#isEnum}} + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + # validator = Petstore::EnumTest::EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]) + # validator.allowable_values.each do |value| + # expect { instance.{{name}} = value }.not_to raise_error + # end + {{/isEnum}} + {{^isEnum}} + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + {{/isEnum}} + end + end + +{{/vars}} +{{/oneOf}} +{{#oneOf}} +{{#-first}} + describe ".openapi_one_of" do + it "lists the items referenced in the oneOf array" do + expect(described_class.openapi_one_of).to_not be_empty + end + end + + {{#discriminator}} + {{#propertyName}} + describe ".openapi_discriminator_name" do + it "returns the value of the "discriminator" property" do + expect(described_class.openapi_discriminator_name).to_not be_empty + end + end + + {{/propertyName}} + {{#mappedModels}} + {{#-first}} + describe ".openapi_discriminator_mapping" do + it "returns the key/values of the "mapping" property" do + expect(described_class.openapi_discriminator_mapping.values.sort).to eq(described_class.openapi_one_of.sort) + end + end + + {{/-first}} + {{/mappedModels}} + {{/discriminator}} + describe ".build" do + it "returns the correct model" do + end + end +{{/-first}} +{{/oneOf}} +end +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/crystal/partial_model_enum_class.mustache b/modules/openapi-generator/src/main/resources/crystal/partial_model_enum_class.mustache new file mode 100644 index 000000000000..4b8b5a0ffdae --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/partial_model_enum_class.mustache @@ -0,0 +1,20 @@ + class {{classname}}{{#allowableValues}}{{#enumVars}} + {{{name}}} = {{{value}}}.freeze{{/enumVars}} + +{{/allowableValues}} + # Builds the enum from string + # @param [String] The enum value in the form of the string + # @return [String] The enum value + def self.build_from_hash(value) + new.build_from_hash(value) + end + + # Builds the enum from string + # @param [String] The enum value in the form of the string + # @return [String] The enum value + def build_from_hash(value) + constantValues = {{classname}}.constants.select { |c| {{classname}}::const_get(c) == value } + raise "Invalid ENUM value #{value} for class #{{{classname}}}" if constantValues.empty? + value + end + end \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/crystal/partial_model_generic.mustache b/modules/openapi-generator/src/main/resources/crystal/partial_model_generic.mustache new file mode 100644 index 000000000000..18f00b1d5298 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/partial_model_generic.mustache @@ -0,0 +1,296 @@ + {{#description}} + # {{{description}}} + {{/description}} + class {{classname}}{{#parent}} < {{{.}}}{{/parent}} include JSON::Serializable + include JSON::Serializable {{#vars}} + {{#description}} + # {{{description}}} + {{/description}} + @[JSON::Field(key: {{{baseName}}}, type: {{{dataType}}}{{#default}}, default: {{{.}}}{{/default}}{{#isNullable}}, nilable: true, emit_null: true{{/isNullable}})] + property {{{name}}} : {{{dataType}}} + + {{/vars}} +{{#hasEnums}} + class EnumAttributeValidator + getter datatype : String + getter allowable_values : Array(String) + + def initialize(datatype, allowable_values) + @datatype = datatype + @allowable_values = allowable_values.map do |value| + case datatype.to_s + when /Integer/i + value.to_i + when /Float/i + value.to_f + else + value + end + end + end + + def valid?(value) + !value || allowable_values.include?(value) + end + end + +{{/hasEnums}} + {{#anyOf}} + {{#-first}} + # List of class defined in anyOf (OpenAPI v3) + def self.openapi_any_of + [ + {{/-first}} + :"{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ] + end + + {{/-last}} + {{/anyOf}} + {{#allOf}} + {{#-first}} + # List of class defined in allOf (OpenAPI v3) + def self.openapi_all_of + [ + {{/-first}} + :"{{{.}}}"{{^-last}},{{/-last}} + {{#-last}} + ] + end + + {{/-last}} + {{/allOf}} + {{#discriminator}} + {{#propertyName}} + # discriminator's property name in OpenAPI v3 + def self.openapi_discriminator_name + :"{{{.}}}" + end + + {{/propertyName}} + {{/discriminator}} + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize({{#vars}}@{{{name}}} : {{{dataType}}}{{^required}} | Nil{{/required}}{{^-last}}, {{/-last}}{{/vars}}) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = {{^parent}}Array.new{{/parent}}{{#parent}}super{{/parent}} + {{#vars}} + {{^isNullable}} + {{#required}} + if @{{{name}}}.nil? + invalid_properties.push("invalid value for \"{{{name}}}\", {{{name}}} cannot be nil.") + end + + {{/required}} + {{/isNullable}} + {{#hasValidation}} + {{#maxLength}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.length > {{{maxLength}}} + invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be smaller than or equal to {{{maxLength}}}.") + end + + {{/maxLength}} + {{#minLength}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.length < {{{minLength}}} + invalid_properties.push("invalid value for \"{{{name}}}\", the character length must be great than or equal to {{{minLength}}}.") + end + + {{/minLength}} + {{#maximum}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} + invalid_properties.push("invalid value for \"{{{name}}}\", must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.") + end + + {{/maximum}} + {{#minimum}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} + invalid_properties.push("invalid value for \"{{{name}}}\", must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.") + end + + {{/minimum}} + {{#pattern}} + pattern = Regexp.new({{{pattern}}}) + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} !~ pattern + invalid_properties.push("invalid value for \"{{{name}}}\", must conform to the pattern #{pattern}.") + end + + {{/pattern}} + {{#maxItems}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.length > {{{maxItems}}} + invalid_properties.push("invalid value for \"{{{name}}}\", number of items must be less than or equal to {{{maxItems}}}." + end + + {{/maxItems}} + {{#minItems}} + if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.length < {{{minItems}}} + invalid_properties.push("invalid value for \"{{{name}}}\", number of items must be greater than or equal to {{{minItems}}}." + end + + {{/minItems}} + {{/hasValidation}} + {{/vars}} + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + {{#vars}} + {{^isNullable}} + {{#required}} + return false if @{{{name}}}.nil? + {{/required}} + {{/isNullable}} + {{#isEnum}} + {{^isContainer}} + {{{name}}}_validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]) + return false unless {{{name}}}_validator.valid?(@{{{name}}}) + {{/isContainer}} + {{/isEnum}} + {{#hasValidation}} + {{#maxLength}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.length > {{{maxLength}}} + {{/maxLength}} + {{#minLength}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.to_s.length < {{{minLength}}} + {{/minLength}} + {{#maximum}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} + {{/maximum}} + {{#minimum}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} + {{/minimum}} + {{#pattern}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}} !~ Regexp.new({{{pattern}}}) + {{/pattern}} + {{#maxItems}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.length > {{{maxItems}}} + {{/maxItems}} + {{#minItems}} + return false if {{^required}}!@{{{name}}}.nil? && {{/required}}@{{{name}}}.length < {{{minItems}}} + {{/minItems}} + {{/hasValidation}} + {{/vars}} + {{#anyOf}} + {{#-first}} + _any_of_found = false + self.class.openapi_any_of.each do |_class| + _any_of = {{moduleName}}.const_get(_class).build_from_hash(self.to_hash) + if _any_of.valid? + _any_of_found = true + end + end + + if !_any_of_found + return false + end + + {{/-first}} + {{/anyOf}} + true{{#parent}} && super{{/parent}} + end + + {{#vars}} + {{#isEnum}} + {{^isContainer}} + # Custom attribute writer method checking allowed values (enum). + # @param [Object] {{{name}}} Object to be assigned + def {{{name}}}=({{{name}}}) + validator = EnumAttributeValidator.new("{{{dataType}}}", [{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]) + unless validator.valid?({{{name}}}) + raise ArgumentError.new("invalid value for \"{{{name}}}\", must be one of #{validator.allowable_values}.") + end + @{{{name}}} = {{{name}}} + end + + {{/isContainer}} + {{/isEnum}} + {{^isEnum}} + {{#hasValidation}} + # Custom attribute writer method with validation + # @param [Object] {{{name}}} Value to be assigned + def {{{name}}}=({{{name}}}) + {{^isNullable}} + {{#required}} + if {{{name}}}.nil? + raise ArgumentError.new("{{{name}}} cannot be nil") + end + + {{/required}} + {{/isNullable}} + {{#maxLength}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.to_s.length > {{{maxLength}}} + raise ArgumentError.new("invalid value for "{{{name}}}", the character length must be smaller than or equal to {{{maxLength}}}.") + end + + {{/maxLength}} + {{#minLength}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.to_s.length < {{{minLength}}} + raise ArgumentError.new("invalid value for \"{{{name}}}\", the character length must be great than or equal to {{{minLength}}}.") + end + + {{/minLength}} + {{#maximum}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}} >{{#exclusiveMaximum}}={{/exclusiveMaximum}} {{{maximum}}} + raise ArgumentError.new("invalid value for \"{{{name}}}\", must be smaller than {{^exclusiveMaximum}}or equal to {{/exclusiveMaximum}}{{{maximum}}}.") + end + + {{/maximum}} + {{#minimum}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}} <{{#exclusiveMinimum}}={{/exclusiveMinimum}} {{{minimum}}} + raise ArgumentError.new("invalid value for \"{{{name}}}\", must be greater than {{^exclusiveMinimum}}or equal to {{/exclusiveMinimum}}{{{minimum}}}.") + end + + {{/minimum}} + {{#pattern}} + pattern = Regexp.new({{{pattern}}}) + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}} !~ pattern + raise ArgumentError.new("invalid value for \"{{{name}}}\", must conform to the pattern #{pattern}.") + end + + {{/pattern}} + {{#maxItems}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.length > {{{maxItems}}} + raise ArgumentError.new("invalid value for \"{{{name}}}\", number of items must be less than or equal to {{{maxItems}}}.") + end + + {{/maxItems}} + {{#minItems}} + if {{^required}}!{{{name}}}.nil? && {{/required}}{{{name}}}.length < {{{minItems}}} + raise ArgumentError.new("invalid value for \"{{{name}}}\", number of items must be greater than or equal to {{{minItems}}}.") + end + + {{/minItems}} + @{{{name}}} = {{{name}}} + end + + {{/hasValidation}} + {{/isEnum}} + {{/vars}} + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class{{#vars}} && + {{name}} == o.{{name}}{{/vars}}{{#parent}} && super(o){{/parent}} + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [{{#vars}}{{name}}{{^-last}}, {{/-last}}{{/vars}}].hash + end + +{{> base_object}} + end diff --git a/modules/openapi-generator/src/main/resources/crystal/partial_model_generic_doc.mustache b/modules/openapi-generator/src/main/resources/crystal/partial_model_generic_doc.mustache new file mode 100644 index 000000000000..f188dd23e6ec --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/partial_model_generic_doc.mustache @@ -0,0 +1,28 @@ +# {{moduleName}}::{{classname}} + +## Properties + +| Name | Type | Description | Notes | +| ---- | ---- | ----------- | ----- | +{{#vars}} +| **{{name}}** | {{#isPrimitiveType}}**{{dataType}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{dataType}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} | {{^required}}[optional]{{/required}}{{#isReadOnly}}[readonly]{{/isReadOnly}}{{#defaultValue}}[default to {{defaultValue}}]{{/defaultValue}} | +{{/vars}} + +## Example + +```ruby +require '{{{gemName}}}' + +{{^vars}} +instance = {{moduleName}}::{{classname}}.new() +{{/vars}} +{{#vars}} +{{#-first}} +instance = {{moduleName}}::{{classname}}.new( +{{/-first}} + {{name}}: {{example}}{{^-last}},{{/-last}} +{{#-last}} +) +{{/-last}} +{{/vars}} +``` diff --git a/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module.mustache b/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module.mustache new file mode 100644 index 000000000000..14dc1635bb74 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module.mustache @@ -0,0 +1,137 @@ + {{#description}} + # {{{description}}} + {{/description}} + module {{classname}} + class << self + {{#oneOf}} + {{#-first}} + # List of class defined in oneOf (OpenAPI v3) + def openapi_one_of + [ + {{/-first}} + :'{{{.}}}'{{^-last}},{{/-last}} + {{#-last}} + ] + end + + {{/-last}} + {{/oneOf}} + {{#discriminator}} + {{#propertyName}} + # Discriminator's property name (OpenAPI v3) + def openapi_discriminator_name + :'{{{.}}}' + end + + {{/propertyName}} + {{#mappedModels}} + {{#-first}} + # Discriminator's mapping (OpenAPI v3) + def openapi_discriminator_mapping + { + {{/-first}} + :'{{{mappingName}}}' => :'{{{modelName}}}'{{^-last}},{{/-last}} + {{#-last}} + } + end + + {{/-last}} + {{/mappedModels}} + {{/discriminator}} + # Builds the object + # @param [Mixed] Data to be matched against the list of oneOf items + # @return [Object] Returns the model or the data itself + def build(data) + {{#discriminator}} + discriminator_value = data[openapi_discriminator_name] + return nil unless discriminator_value + {{#mappedModels}} + {{#-first}} + + klass = openapi_discriminator_mapping[discriminator_value.to_sym] + return nil unless klass + + {{moduleName}}.const_get(klass).build_from_hash(data) + {{/-first}} + {{/mappedModels}} + {{^mappedModels}} + {{moduleName}}.const_get(discriminator_value).build_from_hash(data) + {{/mappedModels}} + {{/discriminator}} + {{^discriminator}} + # Go through the list of oneOf items and attempt to identify the appropriate one. + # Note: + # - We do not attempt to check whether exactly one item matches. + # - No advanced validation of types in some cases (e.g. "x: { type: string }" will happily match { x: 123 }) + # due to the way the deserialization is made in the base_object template (it just casts without verifying). + # - TODO: scalar values are defacto behaving as if they were nullable. + # - TODO: logging when debugging is set. + openapi_one_of.each do |klass| + begin + next if klass == :AnyType # "nullable: true" + typed_data = find_and_cast_into_type(klass, data) + return typed_data if typed_data + rescue # rescue all errors so we keep iterating even if the current item lookup raises + end + end + + openapi_one_of.include?(:AnyType) ? data : nil + {{/discriminator}} + end + {{^discriminator}} + + private + + SchemaMismatchError = Class.new(StandardError) + + # Note: 'File' is missing here because in the regular case we get the data _after_ a call to JSON.parse. + def find_and_cast_into_type(klass, data) + return if data.nil? + + case klass.to_s + when 'Boolean' + return data if data.instance_of?(TrueClass) || data.instance_of?(FalseClass) + when 'Float' + return data if data.instance_of?(Float) + when 'Integer' + return data if data.instance_of?(Integer) + when 'Time' + return Time.parse(data) + when 'Date' + return Date.parse(data) + when 'String' + return data if data.instance_of?(String) + when 'Object' # "type: object" + return data if data.instance_of?(Hash) + when /\AArray<(?.+)>\z/ # "type: array" + if data.instance_of?(Array) + sub_type = Regexp.last_match[:sub_type] + return data.map { |item| find_and_cast_into_type(sub_type, item) } + end + when /\AHash.+)>\z/ # "type: object" with "additionalProperties: { ... }" + if data.instance_of?(Hash) && data.keys.all? { |k| k.instance_of?(Symbol) || k.instance_of?(String) } + sub_type = Regexp.last_match[:sub_type] + return data.each_with_object({}) { |(k, v), hsh| hsh[k] = find_and_cast_into_type(sub_type, v) } + end + else # model + const = {{moduleName}}.const_get(klass) + if const + if const.respond_to?(:openapi_one_of) # nested oneOf model + model = const.build(data) + return model if model + else + # raise if data contains keys that are not known to the model + raise unless (data.keys - const.acceptable_attributes).empty? + model = const.build_from_hash(data) + return model if model && model.valid? + end + end + end + + raise # if no match by now, raise + rescue + raise SchemaMismatchError, "#{data} doesn't match the #{klass} type" + end + {{/discriminator}} + end + end diff --git a/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module_doc.mustache b/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module_doc.mustache new file mode 100644 index 000000000000..64a6c32dc854 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/partial_oneof_module_doc.mustache @@ -0,0 +1,92 @@ +# {{moduleName}}::{{classname}} + +## Class instance methods + +### `openapi_one_of` + +Returns the list of classes defined in oneOf. + +#### Example + +```ruby +require '{{{gemName}}}' + +{{moduleName}}::{{classname}}.openapi_one_of +# => +{{#oneOf}} +{{#-first}} +# [ +{{/-first}} +# :'{{{.}}}'{{^-last}},{{/-last}} +{{#-last}} +# ] +{{/-last}} +{{/oneOf}} +``` +{{#discriminator}} +{{#propertyName}} + +### `openapi_discriminator_name` + +Returns the discriminator's property name. + +#### Example + +```ruby +require '{{{gemName}}}' + +{{moduleName}}::{{classname}}.openapi_discriminator_name +# => :'{{{.}}}' +``` +{{/propertyName}} +{{#mappedModels}} +{{#-first}} + +### `openapi_discriminator_name` + +Returns the discriminator's mapping. + +#### Example + +```ruby +require '{{{gemName}}}' + +{{moduleName}}::{{classname}}.openapi_discriminator_mapping +# => +# { +{{/-first}} +# :'{{{mappingName}}}' => :'{{{modelName}}}'{{^-last}},{{/-last}} +{{#-last}} +# } +{{/-last}} +{{/mappedModels}} +{{/discriminator}} + +### build + +Find the appropriate object from the `openapi_one_of` list and casts the data into it. + +#### Example + +```ruby +require '{{{gemName}}}' + +{{moduleName}}::{{classname}}.build(data) +# => {{#oneOf}}{{#-first}}#<{{{.}}}:0x00007fdd4aab02a0>{{/-first}}{{/oneOf}} + +{{moduleName}}::{{classname}}.build(data_that_doesnt_match) +# => nil +``` + +#### Parameters + +| Name | Type | Description | +| ---- | ---- | ----------- | +| **data** | **Mixed** | data to be matched against the list of oneOf items | + +#### Return type + +{{#oneOf}} +- `{{{.}}}` +{{/oneOf}} +- `nil` (if no type matches) diff --git a/modules/openapi-generator/src/main/resources/crystal/rspec.mustache b/modules/openapi-generator/src/main/resources/crystal/rspec.mustache new file mode 100644 index 000000000000..83e16f804474 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/rspec.mustache @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/modules/openapi-generator/src/main/resources/crystal/rubocop.mustache b/modules/openapi-generator/src/main/resources/crystal/rubocop.mustache new file mode 100644 index 000000000000..d32b2b1cdab5 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/rubocop.mustache @@ -0,0 +1,148 @@ +# This file is based on https://github.com/rails/rails/blob/master/.rubocop.yml (MIT license) +# Automatically generated by OpenAPI Generator (https://openapi-generator.tech) +AllCops: + TargetRubyVersion: 2.4 + # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop + # to ignore them, so only the ones explicitly set in this file are enabled. + DisabledByDefault: true + Exclude: + - '**/templates/**/*' + - '**/vendor/**/*' + - 'actionpack/lib/action_dispatch/journey/parser.rb' + +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true + +# Align `when` with `case`. +Layout/CaseIndentation: + Enabled: true + +# Align comments with method definitions. +Layout/CommentIndentation: + Enabled: true + +Layout/ElseAlignment: + Enabled: true + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +# In a regular class definition, no empty lines around the body. +Layout/EmptyLinesAroundClassBody: + Enabled: true + +# In a regular method definition, no empty lines around the body. +Layout/EmptyLinesAroundMethodBody: + Enabled: true + +# In a regular module definition, no empty lines around the body. +Layout/EmptyLinesAroundModuleBody: + Enabled: true + +Layout/FirstArgumentIndentation: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: false + +# Method definitions after `private` or `protected` isolated calls need one +# extra level of indentation. +Layout/IndentationConsistency: + Enabled: true + EnforcedStyle: indented_internal_methods + +# Two spaces, no tabs (for indentation). +Layout/IndentationWidth: + Enabled: true + +Layout/LeadingCommentSpace: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeComma: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +Style/DefWithParentheses: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: false + EnforcedStyle: always + +# Use `foo {}` not `foo{}`. +Layout/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Layout/SpaceInsideBlockBraces: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +# Check quotes usage according to lint rule below. +#Style/StringLiterals: +# Enabled: true +# EnforcedStyle: single_quotes + +# Detect hard tabs, no hard tabs. +Layout/IndentationStyle: + Enabled: true + +# Blank lines should not have any spaces. +Layout/TrailingEmptyLines: + Enabled: true + +# No trailing whitespace. +Layout/TrailingWhitespace: + Enabled: false + +# Use quotes for string literals when they are enough. +Style/RedundantPercentQ: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + AutoCorrect: true + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true + +Style/RedundantReturn: + Enabled: true + AllowMultipleReturnValues: true + +Style/Semicolon: + Enabled: true + AllowAsExpressionSeparator: true diff --git a/modules/openapi-generator/src/main/resources/crystal/shard.mustache b/modules/openapi-generator/src/main/resources/crystal/shard.mustache new file mode 100644 index 000000000000..89376ca4593b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/shard.mustache @@ -0,0 +1,20 @@ +name: {{{moduleName}}} +version: {{{shardVersion}}} +authors: + - {{{shardAuthors}}} +description: | + - {{{ shardDescription}}} +crystal: ">= 0.35.1" +dependencies: + crest: + github: mamantoha/crest + version: ~> 0.26.0 + +development_dependencies: + kemal: + github: kemalcr/kemal + version: ~>0.27.0 + ameba: + github: crystal-ameba/ameba + +license: {{{shardLicense}}} diff --git a/modules/openapi-generator/src/main/resources/crystal/shard_name.mustache b/modules/openapi-generator/src/main/resources/crystal/shard_name.mustache new file mode 100644 index 000000000000..7e3e9b162d4b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/shard_name.mustache @@ -0,0 +1,27 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +# Dependencies +require "crest" +require "log" + +module {{moduleName}} + Log = ::Log.for("{{moduleName}}") # => Log for {{moduleName}} source + + VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }} + + # Customize default settings for the SDK using block. + # {{moduleName}}.configure do |config| + # config.username = "xxx" + # config.password = "xxx" + # end + # If no block given, return the default Configuration object. + def configure + if block_given? + yield(Configuration.default) + else + Configuration.default + end + end +end + +require "./{{shardName}}/**" diff --git a/modules/openapi-generator/src/main/resources/crystal/spec_helper.mustache b/modules/openapi-generator/src/main/resources/crystal/spec_helper.mustache new file mode 100644 index 000000000000..9facafa4a931 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/spec_helper.mustache @@ -0,0 +1,6 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +# load modules +require "spec" +require "json" +require "../src/{{{shardName}}}" \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/crystal/travis.mustache b/modules/openapi-generator/src/main/resources/crystal/travis.mustache new file mode 100644 index 000000000000..21509cfe82ae --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/travis.mustache @@ -0,0 +1,8 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +language: crystal + +script: + - crystal spec +# uncomment below to check the code format +# - crystal tool format --check diff --git a/modules/openapi-generator/src/main/resources/crystal/version.mustache b/modules/openapi-generator/src/main/resources/crystal/version.mustache new file mode 100644 index 000000000000..9be1d633971f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/crystal/version.mustache @@ -0,0 +1,5 @@ +# {{#lambdaPrefixWithHash}}{{> api_info}}{{/lambdaPrefixWithHash}} + +module {{moduleName}} + VERSION = '{{shardVersion}}' +end diff --git a/pom.xml b/pom.xml index ff2d86119fa8..1213adf3b321 100644 --- a/pom.xml +++ b/pom.xml @@ -1181,6 +1181,7 @@ + samples/client/petstore/crystal samples/server/petstore/python-aiohttp samples/server/petstore/python-aiohttp-srclayout diff --git a/samples/client/petstore/crystal/.gitignore b/samples/client/petstore/crystal/.gitignore new file mode 100644 index 000000000000..05a17cb8f0a0 --- /dev/null +++ b/samples/client/petstore/crystal/.gitignore @@ -0,0 +1,39 @@ +# Generated by: https://openapi-generator.tech +# + +*.gem +*.rbc +/.config +/coverage/ +/InstalledFiles +/pkg/ +/spec/reports/ +/spec/examples.txt +/test/tmp/ +/test/version_tmp/ +/tmp/ + +## Specific to RubyMotion: +.dat* +.repl_history +build/ + +## Documentation cache and generated files: +/.yardoc/ +/_yardoc/ +/doc/ +/rdoc/ + +## Environment normalization: +/.bundle/ +/vendor/bundle +/lib/bundler/man/ + +# for a library or gem, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# Gemfile.lock +# .ruby-version +# .ruby-gemset + +# unless supporting rvm < 1.11.0 or doing something fancy, ignore this: +.rvmrc diff --git a/samples/client/petstore/crystal/.openapi-generator-ignore b/samples/client/petstore/crystal/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/crystal/.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/crystal/.openapi-generator/FILES b/samples/client/petstore/crystal/.openapi-generator/FILES new file mode 100644 index 000000000000..aa17a1e4b987 --- /dev/null +++ b/samples/client/petstore/crystal/.openapi-generator/FILES @@ -0,0 +1,19 @@ +.gitignore +.travis.yml +README.md +git_push.sh +shard.yml +spec/spec_helper.cr +src/petstore.cr +src/petstore/api/pet_api.cr +src/petstore/api/store_api.cr +src/petstore/api/user_api.cr +src/petstore/api_client.cr +src/petstore/api_error.cr +src/petstore/configuration.cr +src/petstore/models/api_response.cr +src/petstore/models/category.cr +src/petstore/models/order.cr +src/petstore/models/pet.cr +src/petstore/models/tag.cr +src/petstore/models/user.cr diff --git a/samples/client/petstore/crystal/.openapi-generator/VERSION b/samples/client/petstore/crystal/.openapi-generator/VERSION new file mode 100644 index 000000000000..3fa3b389a57d --- /dev/null +++ b/samples/client/petstore/crystal/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.0.1-SNAPSHOT \ No newline at end of file diff --git a/samples/client/petstore/crystal/.rspec b/samples/client/petstore/crystal/.rspec new file mode 100644 index 000000000000..83e16f804474 --- /dev/null +++ b/samples/client/petstore/crystal/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/samples/client/petstore/crystal/.rubocop.yml b/samples/client/petstore/crystal/.rubocop.yml new file mode 100644 index 000000000000..d32b2b1cdab5 --- /dev/null +++ b/samples/client/petstore/crystal/.rubocop.yml @@ -0,0 +1,148 @@ +# This file is based on https://github.com/rails/rails/blob/master/.rubocop.yml (MIT license) +# Automatically generated by OpenAPI Generator (https://openapi-generator.tech) +AllCops: + TargetRubyVersion: 2.4 + # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop + # to ignore them, so only the ones explicitly set in this file are enabled. + DisabledByDefault: true + Exclude: + - '**/templates/**/*' + - '**/vendor/**/*' + - 'actionpack/lib/action_dispatch/journey/parser.rb' + +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true + +# Align `when` with `case`. +Layout/CaseIndentation: + Enabled: true + +# Align comments with method definitions. +Layout/CommentIndentation: + Enabled: true + +Layout/ElseAlignment: + Enabled: true + +Layout/EmptyLineAfterMagicComment: + Enabled: true + +# In a regular class definition, no empty lines around the body. +Layout/EmptyLinesAroundClassBody: + Enabled: true + +# In a regular method definition, no empty lines around the body. +Layout/EmptyLinesAroundMethodBody: + Enabled: true + +# In a regular module definition, no empty lines around the body. +Layout/EmptyLinesAroundModuleBody: + Enabled: true + +Layout/FirstArgumentIndentation: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: false + +# Method definitions after `private` or `protected` isolated calls need one +# extra level of indentation. +Layout/IndentationConsistency: + Enabled: true + EnforcedStyle: indented_internal_methods + +# Two spaces, no tabs (for indentation). +Layout/IndentationWidth: + Enabled: true + +Layout/LeadingCommentSpace: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeComma: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +Style/DefWithParentheses: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +Style/FrozenStringLiteralComment: + Enabled: false + EnforcedStyle: always + +# Use `foo {}` not `foo{}`. +Layout/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Layout/SpaceInsideBlockBraces: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +# Check quotes usage according to lint rule below. +#Style/StringLiterals: +# Enabled: true +# EnforcedStyle: single_quotes + +# Detect hard tabs, no hard tabs. +Layout/IndentationStyle: + Enabled: true + +# Blank lines should not have any spaces. +Layout/TrailingEmptyLines: + Enabled: true + +# No trailing whitespace. +Layout/TrailingWhitespace: + Enabled: false + +# Use quotes for string literals when they are enough. +Style/RedundantPercentQ: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Layout/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + AutoCorrect: true + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true + +Style/RedundantReturn: + Enabled: true + AllowMultipleReturnValues: true + +Style/Semicolon: + Enabled: true + AllowAsExpressionSeparator: true diff --git a/samples/client/petstore/crystal/.travis.yml b/samples/client/petstore/crystal/.travis.yml new file mode 100644 index 000000000000..c81e9f2a6e3e --- /dev/null +++ b/samples/client/petstore/crystal/.travis.yml @@ -0,0 +1,16 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +language: crystal + +script: + - crystal spec +# uncomment below to check the code format +# - crystal tool format --check diff --git a/samples/client/petstore/crystal/README.md b/samples/client/petstore/crystal/README.md new file mode 100644 index 000000000000..fc7fa03548c5 --- /dev/null +++ b/samples/client/petstore/crystal/README.md @@ -0,0 +1,38 @@ +# petstore + +The Crystsal module for the OpenAPI Petstore + +This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + +This SDK is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: + +- API version: 1.0.0 +- Package version: 1.0.0 +- Build package: org.openapitools.codegen.languages.CrystalClientCodegen + +## Installation + +### Install from Git + +Add the following to shard.yaml + +```yaml +dependencies: + petstore: + github: GIT_USER_ID/GIT_REPO_ID + version: ~> 1.0.0 +``` + +## Development + +Install dependencies + +```shell +shards +``` + +Run the tests: + +```shell +crystal spec +``` diff --git a/samples/client/petstore/crystal/bin/ameba b/samples/client/petstore/crystal/bin/ameba new file mode 100755 index 000000000000..332b8c750a45 Binary files /dev/null and b/samples/client/petstore/crystal/bin/ameba differ diff --git a/samples/client/petstore/crystal/bin/ameba.cr b/samples/client/petstore/crystal/bin/ameba.cr new file mode 100644 index 000000000000..d61d0ff11a5c --- /dev/null +++ b/samples/client/petstore/crystal/bin/ameba.cr @@ -0,0 +1,7 @@ +# Require ameba cli which starts the inspection. +require "ameba/cli" + +# Require ameba extensions here which are added as project dependencies. +# Example: +# +# require "ameba-performance" diff --git a/samples/client/petstore/crystal/git_push.sh b/samples/client/petstore/crystal/git_push.sh new file mode 100644 index 000000000000..ced3be2b0c7b --- /dev/null +++ b/samples/client/petstore/crystal/git_push.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/samples/client/petstore/crystal/pom.xml b/samples/client/petstore/crystal/pom.xml new file mode 100644 index 000000000000..f4b890f7d802 --- /dev/null +++ b/samples/client/petstore/crystal/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + org.openapitools + CrystalPetstoreClientTests + pom + 1.0-SNAPSHOT + Crystal OpenAPI Petstore Client + + + + maven-dependency-plugin + + + package + + copy-dependencies + + + ${project.build.directory} + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.0.0 + + + shards-install + pre-integration-test + + exec + + + shards + + + + crystal-spec + integration-test + + exec + + + crystal + + spec + + + + + + + + diff --git a/samples/client/petstore/crystal/shard.lock b/samples/client/petstore/crystal/shard.lock new file mode 100644 index 000000000000..97567adcb9e5 --- /dev/null +++ b/samples/client/petstore/crystal/shard.lock @@ -0,0 +1,34 @@ +version: 2.0 +shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 0.13.3 + + crest: + git: https://github.com/mamantoha/crest.git + version: 0.26.1 + + exception_page: + git: https://github.com/crystal-loot/exception_page.git + version: 0.1.4 + + http-client-digest_auth: + git: https://github.com/mamantoha/http-client-digest_auth.git + version: 0.4.0 + + http_proxy: + git: https://github.com/mamantoha/http_proxy.git + version: 0.7.2 + + kemal: + git: https://github.com/kemalcr/kemal.git + version: 0.27.0 + + kilt: + git: https://github.com/jeromegn/kilt.git + version: 0.4.0 + + radix: + git: https://github.com/luislavena/radix.git + version: 0.3.9 + diff --git a/samples/client/petstore/crystal/shard.yml b/samples/client/petstore/crystal/shard.yml new file mode 100644 index 000000000000..06fc3300e053 --- /dev/null +++ b/samples/client/petstore/crystal/shard.yml @@ -0,0 +1,20 @@ +name: Petstore +version: 1.0.0 +authors: + - +description: | + - +crystal: ">= 0.35.1" +dependencies: + crest: + github: mamantoha/crest + version: ~> 0.26.0 + +development_dependencies: + kemal: + github: kemalcr/kemal + version: ~>0.27.0 + ameba: + github: crystal-ameba/ameba + +license: diff --git a/samples/client/petstore/crystal/spec/api/pet_api_spec.cr b/samples/client/petstore/crystal/spec/api/pet_api_spec.cr new file mode 100644 index 000000000000..23bdfa818477 --- /dev/null +++ b/samples/client/petstore/crystal/spec/api/pet_api_spec.cr @@ -0,0 +1,137 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::PetApi +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe "PetApi" do + describe "test an instance of PetApi" do + it "should create an instance of PetApi" do + api_instance = Petstore::PetApi.new + # TODO expect(api_instance).to be_instance_of(Petstore::PetApi) + end + end + + # unit tests for add_pet + # Add a new pet to the store + # @param pet Pet object that needs to be added to the store + # @param [Hash] opts the optional parameters + # @return [Pet] + describe "add_pet test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for delete_pet + # Deletes a pet + # @param pet_id Pet id to delete + # @param [Hash] opts the optional parameters + # @option opts [String] :api_key + # @return [nil] + describe "delete_pet test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for find_pets_by_status + # Finds Pets by status + # Multiple status values can be provided with comma separated strings + # @param status Status values that need to be considered for filter + # @param [Hash] opts the optional parameters + # @return [Array(Pet)] + describe "find_pets_by_status test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for find_pets_by_tags + # Finds Pets by tags + # Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + # @param tags Tags to filter by + # @param [Hash] opts the optional parameters + # @return [Array(Pet)] + describe "find_pets_by_tags test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for get_pet_by_id + # Find pet by ID + # Returns a single pet + # @param pet_id ID of pet to return + # @param [Hash] opts the optional parameters + # @return [Pet] + describe "get_pet_by_id test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + api_instance = Petstore::PetApi.new + # create a pet to start with + pet_id = Int64.new(91829) + pet = Petstore::Pet.new(id: pet_id, category: Petstore::Category.new(id: pet_id + 10, name: "crystal category"), name: "crystal", photo_urls: ["https://crystal-lang.org"], tags: [Petstore::Tag.new(id: pet_id + 100, name: "crystal tag")], status: "available") + + api_instance.add_pet(pet) + + result = api_instance.get_pet_by_id(pet_id: pet_id) + result.id.should eq pet_id + result.category.id.should eq pet_id + 10 + result.category.name.should eq "crystal category" + result.name.should eq "crystal" + result.photo_urls.should eq ["https://crystal-lang.org"] + result.status.should eq "available" + result.tags[0].id.should eq pet_id + 100 + end + end + + # unit tests for update_pet + # Update an existing pet + # @param pet Pet object that needs to be added to the store + # @param [Hash] opts the optional parameters + # @return [Pet] + describe "update_pet test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for update_pet_with_form + # Updates a pet in the store with form data + # @param pet_id ID of pet that needs to be updated + # @param [Hash] opts the optional parameters + # @option opts [String] :name Updated name of the pet + # @option opts [String] :status Updated status of the pet + # @return [nil] + describe "update_pet_with_form test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for upload_file + # uploads an image + # @param pet_id ID of pet to update + # @param [Hash] opts the optional parameters + # @option opts [String] :additional_metadata Additional data to pass to server + # @option opts [File] :file file to upload + # @return [ApiResponse] + describe "upload_file test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/api/store_api_spec.cr b/samples/client/petstore/crystal/spec/api/store_api_spec.cr new file mode 100644 index 000000000000..592747d6fe7b --- /dev/null +++ b/samples/client/petstore/crystal/spec/api/store_api_spec.cr @@ -0,0 +1,72 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::StoreApi +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe "StoreApi" do + describe "test an instance of StoreApi" do + it "should create an instance of StoreApi" do + api_instance = Petstore::StoreApi.new + # TODO expect(api_instance).to be_instance_of(Petstore::StoreApi) + end + end + + # unit tests for delete_order + # Delete purchase order by ID + # For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + # @param order_id ID of the order that needs to be deleted + # @param [Hash] opts the optional parameters + # @return [nil] + describe "delete_order test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for get_inventory + # Returns pet inventories by status + # Returns a map of status codes to quantities + # @param [Hash] opts the optional parameters + # @return [Hash(String, Int32)] + describe "get_inventory test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for get_order_by_id + # Find purchase order by ID + # For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions + # @param order_id ID of pet that needs to be fetched + # @param [Hash] opts the optional parameters + # @return [Order] + describe "get_order_by_id test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for place_order + # Place an order for a pet + # @param order order placed for purchasing the pet + # @param [Hash] opts the optional parameters + # @return [Order] + describe "place_order test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/api/user_api_spec.cr b/samples/client/petstore/crystal/spec/api/user_api_spec.cr new file mode 100644 index 000000000000..38a13b9d7c93 --- /dev/null +++ b/samples/client/petstore/crystal/spec/api/user_api_spec.cr @@ -0,0 +1,118 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::UserApi +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe "UserApi" do + describe "test an instance of UserApi" do + it "should create an instance of UserApi" do + api_instance = Petstore::UserApi.new + # TODO expect(api_instance).to be_instance_of(Petstore::UserApi) + end + end + + # unit tests for create_user + # Create user + # This can only be done by the logged in user. + # @param user Created user object + # @param [Hash] opts the optional parameters + # @return [nil] + describe "create_user test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for create_users_with_array_input + # Creates list of users with given input array + # @param user List of user object + # @param [Hash] opts the optional parameters + # @return [nil] + describe "create_users_with_array_input test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for create_users_with_list_input + # Creates list of users with given input array + # @param user List of user object + # @param [Hash] opts the optional parameters + # @return [nil] + describe "create_users_with_list_input test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for delete_user + # Delete user + # This can only be done by the logged in user. + # @param username The name that needs to be deleted + # @param [Hash] opts the optional parameters + # @return [nil] + describe "delete_user test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for get_user_by_name + # Get user by user name + # @param username The name that needs to be fetched. Use user1 for testing. + # @param [Hash] opts the optional parameters + # @return [User] + describe "get_user_by_name test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for login_user + # Logs user into the system + # @param username The user name for login + # @param password The password for login in clear text + # @param [Hash] opts the optional parameters + # @return [String] + describe "login_user test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for logout_user + # Logs out current logged in user session + # @param [Hash] opts the optional parameters + # @return [nil] + describe "logout_user test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + # unit tests for update_user + # Updated user + # This can only be done by the logged in user. + # @param username name that need to be deleted + # @param user Updated user object + # @param [Hash] opts the optional parameters + # @return [nil] + describe "update_user test" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/api_response_spec.cr b/samples/client/petstore/crystal/spec/models/api_response_spec.cr new file mode 100644 index 000000000000..3a4ff0b1269e --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/api_response_spec.cr @@ -0,0 +1,44 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::ApiResponse +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::ApiResponse do + + describe "test an instance of ApiResponse" do + it "should create an instance of ApiResponse" do + #instance = Petstore::ApiResponse.new + #expect(instance).to be_instance_of(Petstore::ApiResponse) + end + end + describe "test attribute 'code'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute '_type'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'message'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/category_spec.cr b/samples/client/petstore/crystal/spec/models/category_spec.cr new file mode 100644 index 000000000000..120fa0459f18 --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/category_spec.cr @@ -0,0 +1,38 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::Category +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::Category do + + describe "test an instance of Category" do + it "should create an instance of Category" do + #instance = Petstore::Category.new + #expect(instance).to be_instance_of(Petstore::Category) + end + end + describe "test attribute 'id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'name'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/order_spec.cr b/samples/client/petstore/crystal/spec/models/order_spec.cr new file mode 100644 index 000000000000..e64ada1fb51b --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/order_spec.cr @@ -0,0 +1,66 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::Order +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::Order do + + describe "test an instance of Order" do + it "should create an instance of Order" do + #instance = Petstore::Order.new + #expect(instance).to be_instance_of(Petstore::Order) + end + end + describe "test attribute 'id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'pet_id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'quantity'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'ship_date'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'status'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + # validator = Petstore::EnumTest::EnumAttributeValidator.new("String", ["placed", "approved", "delivered"]) + # validator.allowable_values.each do |value| + # expect { instance.status = value }.not_to raise_error + # end + end + end + + describe "test attribute 'complete'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/pet_spec.cr b/samples/client/petstore/crystal/spec/models/pet_spec.cr new file mode 100644 index 000000000000..d3eaf60dfc11 --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/pet_spec.cr @@ -0,0 +1,66 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::Pet +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::Pet do + + describe "test an instance of Pet" do + it "should create an instance of Pet" do + #instance = Petstore::Pet.new + #expect(instance).to be_instance_of(Petstore::Pet) + end + end + describe "test attribute 'id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'category'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'name'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'photo_urls'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'tags'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'status'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + # validator = Petstore::EnumTest::EnumAttributeValidator.new("String", ["available", "pending", "sold"]) + # validator.allowable_values.each do |value| + # expect { instance.status = value }.not_to raise_error + # end + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/tag_spec.cr b/samples/client/petstore/crystal/spec/models/tag_spec.cr new file mode 100644 index 000000000000..f79e12657fc5 --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/tag_spec.cr @@ -0,0 +1,38 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::Tag +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::Tag do + + describe "test an instance of Tag" do + it "should create an instance of Tag" do + #instance = Petstore::Tag.new + #expect(instance).to be_instance_of(Petstore::Tag) + end + end + describe "test attribute 'id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'name'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/models/user_spec.cr b/samples/client/petstore/crystal/spec/models/user_spec.cr new file mode 100644 index 000000000000..6e52fc2c5bdf --- /dev/null +++ b/samples/client/petstore/crystal/spec/models/user_spec.cr @@ -0,0 +1,74 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "../spec_helper" +require "json" +require "time" + +# Unit tests for Petstore::User +# Automatically generated by openapi-generator (https://openapi-generator.tech) +# Please update as you see appropriate +describe Petstore::User do + + describe "test an instance of User" do + it "should create an instance of User" do + #instance = Petstore::User.new + #expect(instance).to be_instance_of(Petstore::User) + end + end + describe "test attribute 'id'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'username'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'first_name'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'last_name'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'email'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'password'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'phone'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + + describe "test attribute 'user_status'" do + it "should work" do + # assertion here. ref: https://crystal-lang.org/reference/guides/testing.html + end + end + +end diff --git a/samples/client/petstore/crystal/spec/spec_helper.cr b/samples/client/petstore/crystal/spec/spec_helper.cr new file mode 100644 index 000000000000..1cafe029c9da --- /dev/null +++ b/samples/client/petstore/crystal/spec/spec_helper.cr @@ -0,0 +1,14 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +# load modules +require "spec" +require "json" +require "../src/petstore" \ No newline at end of file diff --git a/samples/client/petstore/crystal/src/petstore.cr b/samples/client/petstore/crystal/src/petstore.cr new file mode 100644 index 000000000000..551e69efe96d --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore.cr @@ -0,0 +1,35 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +# Dependencies +require "crest" +require "log" + +module Petstore + Log = ::Log.for("Petstore") # => Log for Petstore source + + VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }} + + # Customize default settings for the SDK using block. + # Petstore.configure do |config| + # config.username = "xxx" + # config.password = "xxx" + # end + # If no block given, return the default Configuration object. + def configure + if block_given? + yield(Configuration.default) + else + Configuration.default + end + end +end + +require "./petstore/**" diff --git a/samples/client/petstore/crystal/src/petstore/api/pet_api.cr b/samples/client/petstore/crystal/src/petstore/api/pet_api.cr new file mode 100644 index 000000000000..a263fc37db1f --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/api/pet_api.cr @@ -0,0 +1,493 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "uri" + +module Petstore + class PetApi + property api_client : ApiClient + + def initialize(api_client = ApiClient.default) + @api_client = api_client + end + # Add a new pet to the store + # @param pet [Pet] Pet object that needs to be added to the store + # @return [Pet] + def add_pet(pet : Pet) + data, _status_code, _headers = add_pet_with_http_info(pet) + data + end + + # Add a new pet to the store + # @param pet [Pet] Pet object that needs to be added to the store + # @return [Array<(Pet, Integer, Hash)>] Pet data, response status code and response headers + def add_pet_with_http_info(pet : Pet) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.add_pet ..."} + end + # verify the required parameter "pet" is set + if @api_client.config.client_side_validation && pet.nil? + raise ArgumentError.new("Missing the required parameter 'pet' when calling PetApi.add_pet") + end + # resource path + local_var_path = "/pet" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json", "application/xml"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = pet.to_json + + # return_type + return_type = "Pet" + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"PetApi.add_pet", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#add_pet\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Pet.from_json(data), status_code, headers + end + + # Deletes a pet + # @param pet_id [Int64] Pet id to delete + # @return [nil] + def delete_pet(pet_id : Int64, api_key : String?) + delete_pet_with_http_info(pet_id, api_key) + nil + end + + # Deletes a pet + # @param pet_id [Int64] Pet id to delete + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def delete_pet_with_http_info(pet_id : Int64, api_key : String?) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.delete_pet ..."} + end + # verify the required parameter "pet_id" is set + if @api_client.config.client_side_validation && pet_id.nil? + raise ArgumentError.new("Missing the required parameter 'pet_id' when calling PetApi.delete_pet") + end + # resource path + local_var_path = "/pet/{petId}".sub("{" + "petId" + "}", URI.encode(pet_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + header_params["api_key"] = api_key + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = nil + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:DELETE, + local_var_path, + :"PetApi.delete_pet", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#delete_pet\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Finds Pets by status + # Multiple status values can be provided with comma separated strings + # @param status [Array(String)] Status values that need to be considered for filter + # @return [Array(Pet)] + def find_pets_by_status(status : Array(String)) + data, _status_code, _headers = find_pets_by_status_with_http_info(status) + data + end + + # Finds Pets by status + # Multiple status values can be provided with comma separated strings + # @param status [Array(String)] Status values that need to be considered for filter + # @return [Array<(Array(Pet), Integer, Hash)>] Array(Pet) data, response status code and response headers + def find_pets_by_status_with_http_info(status : Array(String)) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.find_pets_by_status ..."} + end + # verify the required parameter "status" is set + if @api_client.config.client_side_validation && status.nil? + raise ArgumentError.new("Missing the required parameter 'status' when calling PetApi.find_pets_by_status") + end + # resource path + local_var_path = "/pet/findByStatus" + + # query parameters + query_params = Hash(Symbol, String).new + query_params[:"status"] = @api_client.build_collection_param(status, :csv) + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "Array(Pet)" + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"PetApi.find_pets_by_status", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#find_pets_by_status\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Array(Pet).from_json(data), status_code, headers + end + + # Finds Pets by tags + # Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + # @param tags [Array(String)] Tags to filter by + # @return [Array(Pet)] + def find_pets_by_tags(tags : Array(String)) + data, _status_code, _headers = find_pets_by_tags_with_http_info(tags) + data + end + + # Finds Pets by tags + # Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + # @param tags [Array(String)] Tags to filter by + # @return [Array<(Array(Pet), Integer, Hash)>] Array(Pet) data, response status code and response headers + def find_pets_by_tags_with_http_info(tags : Array(String)) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.find_pets_by_tags ..."} + end + # verify the required parameter "tags" is set + if @api_client.config.client_side_validation && tags.nil? + raise ArgumentError.new("Missing the required parameter 'tags' when calling PetApi.find_pets_by_tags") + end + # resource path + local_var_path = "/pet/findByTags" + + # query parameters + query_params = Hash(Symbol, String).new + query_params[:"tags"] = @api_client.build_collection_param(tags, :csv) + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "Array(Pet)" + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"PetApi.find_pets_by_tags", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#find_pets_by_tags\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Array(Pet).from_json(data), status_code, headers + end + + # Find pet by ID + # Returns a single pet + # @param pet_id [Int64] ID of pet to return + # @return [Pet] + def get_pet_by_id(pet_id : Int64) + data, _status_code, _headers = get_pet_by_id_with_http_info(pet_id) + data + end + + # Find pet by ID + # Returns a single pet + # @param pet_id [Int64] ID of pet to return + # @return [Array<(Pet, Integer, Hash)>] Pet data, response status code and response headers + def get_pet_by_id_with_http_info(pet_id : Int64) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.get_pet_by_id ..."} + end + # verify the required parameter "pet_id" is set + if @api_client.config.client_side_validation && pet_id.nil? + raise ArgumentError.new("Missing the required parameter 'pet_id' when calling PetApi.get_pet_by_id") + end + # resource path + local_var_path = "/pet/{petId}".sub("{" + "petId" + "}", URI.encode(pet_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "Pet" + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"PetApi.get_pet_by_id", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#get_pet_by_id\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Pet.from_json(data), status_code, headers + end + + # Update an existing pet + # @param pet [Pet] Pet object that needs to be added to the store + # @return [Pet] + def update_pet(pet : Pet) + data, _status_code, _headers = update_pet_with_http_info(pet) + data + end + + # Update an existing pet + # @param pet [Pet] Pet object that needs to be added to the store + # @return [Array<(Pet, Integer, Hash)>] Pet data, response status code and response headers + def update_pet_with_http_info(pet : Pet) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.update_pet ..."} + end + # verify the required parameter "pet" is set + if @api_client.config.client_side_validation && pet.nil? + raise ArgumentError.new("Missing the required parameter 'pet' when calling PetApi.update_pet") + end + # resource path + local_var_path = "/pet" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json", "application/xml"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = pet.to_json + + # return_type + return_type = "Pet" + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:PUT, + local_var_path, + :"PetApi.update_pet", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#update_pet\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Pet.from_json(data), status_code, headers + end + + # Updates a pet in the store with form data + # @param pet_id [Int64] ID of pet that needs to be updated + # @return [nil] + def update_pet_with_form(pet_id : Int64, name : String?, status : String?) + update_pet_with_form_with_http_info(pet_id, name, status) + nil + end + + # Updates a pet in the store with form data + # @param pet_id [Int64] ID of pet that needs to be updated + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def update_pet_with_form_with_http_info(pet_id : Int64, name : String?, status : String?) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.update_pet_with_form ..."} + end + # verify the required parameter "pet_id" is set + if @api_client.config.client_side_validation && pet_id.nil? + raise ArgumentError.new("Missing the required parameter 'pet_id' when calling PetApi.update_pet_with_form") + end + # resource path + local_var_path = "/pet/{petId}".sub("{" + "petId" + "}", URI.encode(pet_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/x-www-form-urlencoded"]) + + # form parameters + form_params = Hash(Symbol, String).new + form_params[:"name"] = name + form_params[:"status"] = status + + # http body (model) + post_body = nil + + # return_type + return_type = nil + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"PetApi.update_pet_with_form", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#update_pet_with_form\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # uploads an image + # @param pet_id [Int64] ID of pet to update + # @return [ApiResponse] + def upload_file(pet_id : Int64, additional_metadata : String?, file : File?) + data, _status_code, _headers = upload_file_with_http_info(pet_id, additional_metadata, file) + data + end + + # uploads an image + # @param pet_id [Int64] ID of pet to update + # @return [Array<(ApiResponse, Integer, Hash)>] ApiResponse data, response status code and response headers + def upload_file_with_http_info(pet_id : Int64, additional_metadata : String?, file : File?) + if @api_client.config.debugging + Log.debug {"Calling API: PetApi.upload_file ..."} + end + # verify the required parameter "pet_id" is set + if @api_client.config.client_side_validation && pet_id.nil? + raise ArgumentError.new("Missing the required parameter 'pet_id' when calling PetApi.upload_file") + end + # resource path + local_var_path = "/pet/{petId}/uploadImage".sub("{" + "petId" + "}", URI.encode(pet_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/json"]) + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["multipart/form-data"]) + + # form parameters + form_params = Hash(Symbol, String).new + form_params[:"additionalMetadata"] = additional_metadata + form_params[:"file"] = file + + # http body (model) + post_body = nil + + # return_type + return_type = "ApiResponse" + + # auth_names + auth_names = ["petstore_auth"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"PetApi.upload_file", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: PetApi#upload_file\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return ApiResponse.from_json(data), status_code, headers + end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/api/store_api.cr b/samples/client/petstore/crystal/src/petstore/api/store_api.cr new file mode 100644 index 000000000000..adfb59de419c --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/api/store_api.cr @@ -0,0 +1,256 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "uri" + +module Petstore + class StoreApi + property api_client : ApiClient + + def initialize(api_client = ApiClient.default) + @api_client = api_client + end + # Delete purchase order by ID + # For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + # @param order_id [String] ID of the order that needs to be deleted + # @return [nil] + def delete_order(order_id : String) + delete_order_with_http_info(order_id) + nil + end + + # Delete purchase order by ID + # For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + # @param order_id [String] ID of the order that needs to be deleted + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def delete_order_with_http_info(order_id : String) + if @api_client.config.debugging + Log.debug {"Calling API: StoreApi.delete_order ..."} + end + # verify the required parameter "order_id" is set + if @api_client.config.client_side_validation && order_id.nil? + raise ArgumentError.new("Missing the required parameter 'order_id' when calling StoreApi.delete_order") + end + # resource path + local_var_path = "/store/order/{orderId}".sub("{" + "orderId" + "}", URI.encode(order_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = nil + + # auth_names + auth_names = [] of String + + data, status_code, headers = @api_client.call_api(:DELETE, + local_var_path, + :"StoreApi.delete_order", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: StoreApi#delete_order\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Returns pet inventories by status + # Returns a map of status codes to quantities + # @return [Hash(String, Int32)] + def get_inventory() + data, _status_code, _headers = get_inventory_with_http_info() + data + end + + # Returns pet inventories by status + # Returns a map of status codes to quantities + # @return [Array<(Hash(String, Int32), Integer, Hash)>] Hash(String, Int32) data, response status code and response headers + def get_inventory_with_http_info() + if @api_client.config.debugging + Log.debug {"Calling API: StoreApi.get_inventory ..."} + end + # resource path + local_var_path = "/store/inventory" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "Hash(String, Int32)" + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"StoreApi.get_inventory", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: StoreApi#get_inventory\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Hash(String, Int32).from_json(data), status_code, headers + end + + # Find purchase order by ID + # For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions + # @param order_id [Int64] ID of pet that needs to be fetched + # @return [Order] + def get_order_by_id(order_id : Int64) + data, _status_code, _headers = get_order_by_id_with_http_info(order_id) + data + end + + # Find purchase order by ID + # For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions + # @param order_id [Int64] ID of pet that needs to be fetched + # @return [Array<(Order, Integer, Hash)>] Order data, response status code and response headers + def get_order_by_id_with_http_info(order_id : Int64) + if @api_client.config.debugging + Log.debug {"Calling API: StoreApi.get_order_by_id ..."} + end + # verify the required parameter "order_id" is set + if @api_client.config.client_side_validation && order_id.nil? + raise ArgumentError.new("Missing the required parameter 'order_id' when calling StoreApi.get_order_by_id") + end + if @api_client.config.client_side_validation && order_id > 5 + raise ArgumentError.new("invalid value for \"order_id\" when calling StoreApi.get_order_by_id, must be smaller than or equal to 5.") + end + + if @api_client.config.client_side_validation && order_id < 1 + raise ArgumentError.new("invalid value for \"order_id\" when calling StoreApi.get_order_by_id, must be greater than or equal to 1.") + end + + # resource path + local_var_path = "/store/order/{orderId}".sub("{" + "orderId" + "}", URI.encode(order_id.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "Order" + + # auth_names + auth_names = [] of String + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"StoreApi.get_order_by_id", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: StoreApi#get_order_by_id\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Order.from_json(data), status_code, headers + end + + # Place an order for a pet + # @param order [Order] order placed for purchasing the pet + # @return [Order] + def place_order(order : Order) + data, _status_code, _headers = place_order_with_http_info(order) + data + end + + # Place an order for a pet + # @param order [Order] order placed for purchasing the pet + # @return [Array<(Order, Integer, Hash)>] Order data, response status code and response headers + def place_order_with_http_info(order : Order) + if @api_client.config.debugging + Log.debug {"Calling API: StoreApi.place_order ..."} + end + # verify the required parameter "order" is set + if @api_client.config.client_side_validation && order.nil? + raise ArgumentError.new("Missing the required parameter 'order' when calling StoreApi.place_order") + end + # resource path + local_var_path = "/store/order" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = order.to_json + + # return_type + return_type = "Order" + + # auth_names + auth_names = [] of String + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"StoreApi.place_order", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: StoreApi#place_order\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return Order.from_json(data), status_code, headers + end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/api/user_api.cr b/samples/client/petstore/crystal/src/petstore/api/user_api.cr new file mode 100644 index 000000000000..db3832b3c1e1 --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/api/user_api.cr @@ -0,0 +1,491 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "uri" + +module Petstore + class UserApi + property api_client : ApiClient + + def initialize(api_client = ApiClient.default) + @api_client = api_client + end + # Create user + # This can only be done by the logged in user. + # @param user [User] Created user object + # @return [nil] + def create_user(user : User) + create_user_with_http_info(user) + nil + end + + # Create user + # This can only be done by the logged in user. + # @param user [User] Created user object + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def create_user_with_http_info(user : User) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.create_user ..."} + end + # verify the required parameter "user" is set + if @api_client.config.client_side_validation && user.nil? + raise ArgumentError.new("Missing the required parameter 'user' when calling UserApi.create_user") + end + # resource path + local_var_path = "/user" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = user.to_json + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"UserApi.create_user", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#create_user\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Creates list of users with given input array + # @param user [Array(User)] List of user object + # @return [nil] + def create_users_with_array_input(user : Array(User)) + create_users_with_array_input_with_http_info(user) + nil + end + + # Creates list of users with given input array + # @param user [Array(User)] List of user object + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def create_users_with_array_input_with_http_info(user : Array(User)) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.create_users_with_array_input ..."} + end + # verify the required parameter "user" is set + if @api_client.config.client_side_validation && user.nil? + raise ArgumentError.new("Missing the required parameter 'user' when calling UserApi.create_users_with_array_input") + end + # resource path + local_var_path = "/user/createWithArray" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = user.to_json + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"UserApi.create_users_with_array_input", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#create_users_with_array_input\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Creates list of users with given input array + # @param user [Array(User)] List of user object + # @return [nil] + def create_users_with_list_input(user : Array(User)) + create_users_with_list_input_with_http_info(user) + nil + end + + # Creates list of users with given input array + # @param user [Array(User)] List of user object + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def create_users_with_list_input_with_http_info(user : Array(User)) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.create_users_with_list_input ..."} + end + # verify the required parameter "user" is set + if @api_client.config.client_side_validation && user.nil? + raise ArgumentError.new("Missing the required parameter 'user' when calling UserApi.create_users_with_list_input") + end + # resource path + local_var_path = "/user/createWithList" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = user.to_json + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:POST, + local_var_path, + :"UserApi.create_users_with_list_input", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#create_users_with_list_input\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Delete user + # This can only be done by the logged in user. + # @param username [String] The name that needs to be deleted + # @return [nil] + def delete_user(username : String) + delete_user_with_http_info(username) + nil + end + + # Delete user + # This can only be done by the logged in user. + # @param username [String] The name that needs to be deleted + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def delete_user_with_http_info(username : String) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.delete_user ..."} + end + # verify the required parameter "username" is set + if @api_client.config.client_side_validation && username.nil? + raise ArgumentError.new("Missing the required parameter 'username' when calling UserApi.delete_user") + end + # resource path + local_var_path = "/user/{username}".sub("{" + "username" + "}", URI.encode(username.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:DELETE, + local_var_path, + :"UserApi.delete_user", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#delete_user\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Get user by user name + # @param username [String] The name that needs to be fetched. Use user1 for testing. + # @return [User] + def get_user_by_name(username : String) + data, _status_code, _headers = get_user_by_name_with_http_info(username) + data + end + + # Get user by user name + # @param username [String] The name that needs to be fetched. Use user1 for testing. + # @return [Array<(User, Integer, Hash)>] User data, response status code and response headers + def get_user_by_name_with_http_info(username : String) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.get_user_by_name ..."} + end + # verify the required parameter "username" is set + if @api_client.config.client_side_validation && username.nil? + raise ArgumentError.new("Missing the required parameter 'username' when calling UserApi.get_user_by_name") + end + # resource path + local_var_path = "/user/{username}".sub("{" + "username" + "}", URI.encode(username.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "User" + + # auth_names + auth_names = [] of String + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"UserApi.get_user_by_name", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#get_user_by_name\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return User.from_json(data), status_code, headers + end + + # Logs user into the system + # @param username [String] The user name for login + # @param password [String] The password for login in clear text + # @return [String] + def login_user(username : String, password : String) + data, _status_code, _headers = login_user_with_http_info(username, password) + data + end + + # Logs user into the system + # @param username [String] The user name for login + # @param password [String] The password for login in clear text + # @return [Array<(String, Integer, Hash)>] String data, response status code and response headers + def login_user_with_http_info(username : String, password : String) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.login_user ..."} + end + # verify the required parameter "username" is set + if @api_client.config.client_side_validation && username.nil? + raise ArgumentError.new("Missing the required parameter 'username' when calling UserApi.login_user") + end + pattern = Regexp.new(/^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$/) + if @api_client.config.client_side_validation && username !~ pattern + raise ArgumentError.new("invalid value for \"username\" when calling UserApi.login_user, must conform to the pattern #{pattern}.") + end + + # verify the required parameter "password" is set + if @api_client.config.client_side_validation && password.nil? + raise ArgumentError.new("Missing the required parameter 'password' when calling UserApi.login_user") + end + # resource path + local_var_path = "/user/login" + + # query parameters + query_params = Hash(Symbol, String).new + query_params[:"username"] = username + query_params[:"password"] = password + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Accept" (if needed) + header_params["Accept"] = @api_client.select_header_accept(["application/xml", "application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = "String" + + # auth_names + auth_names = [] of String + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"UserApi.login_user", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#login_user\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return String.from_json(data), status_code, headers + end + + # Logs out current logged in user session + # @return [nil] + def logout_user() + logout_user_with_http_info() + nil + end + + # Logs out current logged in user session + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def logout_user_with_http_info() + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.logout_user ..."} + end + # resource path + local_var_path = "/user/logout" + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = nil + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:GET, + local_var_path, + :"UserApi.logout_user", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#logout_user\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + + # Updated user + # This can only be done by the logged in user. + # @param username [String] name that need to be deleted + # @param user [User] Updated user object + # @return [nil] + def update_user(username : String, user : User) + update_user_with_http_info(username, user) + nil + end + + # Updated user + # This can only be done by the logged in user. + # @param username [String] name that need to be deleted + # @param user [User] Updated user object + # @return [Array<(nil, Integer, Hash)>] nil, response status code and response headers + def update_user_with_http_info(username : String, user : User) + if @api_client.config.debugging + Log.debug {"Calling API: UserApi.update_user ..."} + end + # verify the required parameter "username" is set + if @api_client.config.client_side_validation && username.nil? + raise ArgumentError.new("Missing the required parameter 'username' when calling UserApi.update_user") + end + # verify the required parameter "user" is set + if @api_client.config.client_side_validation && user.nil? + raise ArgumentError.new("Missing the required parameter 'user' when calling UserApi.update_user") + end + # resource path + local_var_path = "/user/{username}".sub("{" + "username" + "}", URI.encode(username.to_s).gsub("%2F", "/")) + + # query parameters + query_params = Hash(Symbol, String).new + + # header parameters + header_params = Hash(String, String).new + # HTTP header "Content-Type" + header_params["Content-Type"] = @api_client.select_header_content_type(["application/json"]) + + # form parameters + form_params = Hash(Symbol, String).new + + # http body (model) + post_body = user.to_json + + # return_type + return_type = nil + + # auth_names + auth_names = ["api_key"] + + data, status_code, headers = @api_client.call_api(:PUT, + local_var_path, + :"UserApi.update_user", + return_type, + post_body, + auth_names, + header_params, + query_params, + form_params) + if @api_client.config.debugging + Log.debug {"API called: UserApi#update_user\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"} + end + return nil, status_code, headers + end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/api_client.cr b/samples/client/petstore/crystal/src/petstore/api_client.cr new file mode 100644 index 000000000000..d78cb31fc637 --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/api_client.cr @@ -0,0 +1,410 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "json" +require "time" + +module Petstore + class ApiClient + # The Configuration object holding settings to be used in the API client. + property config : Configuration + + # Defines the headers to be used in HTTP requests of all API calls by default. + # + # @return [Hash] + property default_headers : Hash(String, String) + + # Initializes the ApiClient + # @option config [Configuration] Configuration for initializing the object, default to Configuration.default + def initialize(@config = Configuration.default) + @user_agent = "OpenAPI-Generator/#{VERSION}/crystal" + @default_headers = { + "User-Agent" => @user_agent + } + end + + def self.default + @@default ||= ApiClient.new + end + + # Check if the given MIME is a JSON MIME. + # JSON MIME examples: + # application/json + # application/json; charset=UTF8 + # APPLICATION/JSON + # */* + # @param [String] mime MIME + # @return [Boolean] True if the MIME is application/json + def json_mime?(mime) + (mime == "*/*") || !(mime =~ /Application\/.*json(?!p)(;.*)?/i).nil? + end + + # Deserialize the response to the given return type. + # + # @param [Response] response HTTP response + # @param [String] return_type some examples: "User", "Array", "Hash" + def deserialize(response, return_type) + body = response.body + + # handle file downloading - return the File instance processed in request callbacks + # note that response body is empty when the file is written in chunks in request on_body callback + if return_type == "File" + content_disposition = response.headers["Content-Disposition"].to_s + if content_disposition && content_disposition =~ /filename=/i + filename = content_disposition.match(/filename=[""]?([^""\s]+)[""]?/i).try &.[0] + prefix = sanitize_filename(filename) + else + prefix = "download-" + end + if !prefix.nil? && prefix.ends_with?("-") + prefix = prefix + "-" + end + encoding = response.headers["Content-Encoding"].to_s + + # TODO add file support + raise ApiError.new(code: 0, message: "File response not yet supported in the client.") if return_type + return nil + + #@tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding) + #@tempfile.write(@stream.join.force_encoding(encoding)) + #@tempfile.close + #Log.info { "Temp file written to #{@tempfile.path}, please copy the file to a proper folder "\ + # "with e.g. `FileUtils.cp(tempfile.path, \"/new/file/path\")` otherwise the temp file "\ + # "will be deleted automatically with GC. It's also recommended to delete the temp file "\ + # "explicitly with `tempfile.delete`" } + #return @tempfile + end + + return nil if body.nil? || body.empty? + + # return response body directly for String return type + return body if return_type == "String" + + # ensuring a default content type + content_type = response.headers["Content-Type"] || "application/json" + + raise ApiError.new(code: 0, message: "Content-Type is not supported: #{content_type}") unless json_mime?(content_type) + + begin + data = JSON.parse("[#{body}]")[0] + rescue e : Exception + if %w(String Date Time).includes?(return_type) + data = body + else + raise e + end + end + + convert_to_type data, return_type + end + + # Convert data to the given return type. + # @param [Object] data Data to be converted + # @param [String] return_type Return type + # @return [Mixed] Data in a particular type + def convert_to_type(data, return_type) + return nil if data.nil? + case return_type + when "String" + data.to_s + when "Integer" + data.to_s.to_i + when "Float" + data.to_s.to_f + when "Boolean" + data == true + when "Time" + # parse date time (expecting ISO 8601 format) + Time.parse! data.to_s, "%Y-%m-%dT%H:%M:%S%Z" + when "Date" + # parse date (expecting ISO 8601 format) + Time.parse! data.to_s, "%Y-%m-%d" + when "Object" + # generic object (usually a Hash), return directly + data + when /\AArray<(.+)>\z/ + # e.g. Array + sub_type = $1 + data.map { |item| convert_to_type(item, sub_type) } + when /\AHash\\z/ + # e.g. Hash + sub_type = $1 + ({} of Symbol => String).tap do |hash| + data.each { |k, v| hash[k] = convert_to_type(v, sub_type) } + end + else + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(return_type) + klass.respond_to?(:openapi_one_of) ? klass.build(data) : klass.build_from_hash(data) + end + end + + # Sanitize filename by removing path. + # e.g. ../../sun.gif becomes sun.gif + # + # @param [String] filename the filename to be sanitized + # @return [String] the sanitized filename + def sanitize_filename(filename) + if filename.nil? + return nil + else + filename.gsub(/.*[\/\\]/, "") + end + end + + def build_request_url(path : String, operation : Symbol) + # Add leading and trailing slashes to path + path = "/#{path}".gsub(/\/+/, "/") + @config.base_url(operation) + path + end + + # Update hearder and query params based on authentication settings. + # + # @param [Hash] header_params Header parameters + # @param [Hash] query_params Query parameters + # @param [String] auth_names Authentication scheme name + def update_params_for_auth!(header_params, query_params, auth_names) + Array{auth_names}.each do |auth_name| + auth_setting = @config.auth_settings[auth_name] + next unless auth_setting + case auth_setting[:in] + when "header" then header_params[auth_setting[:key]] = auth_setting[:value] + when "query" then query_params[auth_setting[:key]] = auth_setting[:value] + else raise ArgumentError.new("Authentication token must be in `query` of `header`") + end + end + end + + # Sets user agent in HTTP header + # + # @param [String] user_agent User agent (e.g. openapi-generator/ruby/1.0.0) + def user_agent=(user_agent) + @user_agent = user_agent + @default_headers["User-Agent"] = @user_agent + end + + # Return Accept header based on an array of accepts provided. + # @param [Array] accepts array for Accept + # @return [String] the Accept header (e.g. application/json) + def select_header_accept(accepts) : String + #return nil if accepts.nil? || accepts.empty? + # use JSON when present, otherwise use all of the provided + json_accept = accepts.find { |s| json_mime?(s) } + if json_accept.nil? + accepts.join(",") + else + json_accept + end + end + + # Return Content-Type header based on an array of content types provided. + # @param [Array] content_types array for Content-Type + # @return [String] the Content-Type header (e.g. application/json) + def select_header_content_type(content_types) + # use application/json by default + return "application/json" if content_types.nil? || content_types.empty? + # use JSON when present, otherwise use the first one + json_content_type = content_types.find { |s| json_mime?(s) } + json_content_type || content_types.first + end + + # Convert object (array, hash, object, etc) to JSON string. + # @param [Object] model object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_http_body(model) + return model if model.nil? || model.is_a?(String) + local_body = nil + if model.is_a?(Array) + local_body = model.map { |m| object_to_hash(m) } + else + local_body = object_to_hash(model) + end + local_body.to_json + end + + # Convert object(non-array) to hash. + # @param [Object] obj object to be converted into JSON string + # @return [String] JSON string representation of the object + def object_to_hash(obj) + if obj.respond_to?(:to_hash) + obj.to_hash + else + obj + end + end + + # Build parameter value according to the given collection format. + # @param [String] collection_format one of :csv, :ssv, :tsv, :pipes and :multi + def build_collection_param(param, collection_format) + case collection_format + when :csv + param.join(",") + when :ssv + param.join(" ") + when :tsv + param.join("\t") + when :pipes + param.join("|") + when :multi + # return the array directly as typhoeus will handle it as expected + param + else + fail "unknown collection format: #{collection_format.inspect}" + end + end + + # Call an API with given options. + # + # @return [Array<(Object, Integer, Hash)>] an array of 3 elements: + # the data deserialized from response body (could be nil), response status code and response headers. + def call_api(http_method : Symbol, path : String, operation : Symbol, return_type : String, post_body : String?, auth_names = [] of String, header_params = {} of String => String, query_params = {} of Symbol => String, form_params = {} of Symbol => String) + #ssl_options = { + # :ca_file => @config.ssl_ca_file, + # :verify => @config.ssl_verify, + # :verify_mode => @config.ssl_verify_mode, + # :client_cert => @config.ssl_client_cert, + # :client_key => @config.ssl_client_key + #} + + #connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn| + # conn.basic_auth(config.username, config.password) + # if opts[:header_params]["Content-Type"] == "multipart/form-data" + # conn.request :multipart + # conn.request :url_encoded + # end + # conn.adapter(Faraday.default_adapter) + #end + + if !post_body.nil? && !post_body.empty? + # use JSON string in the payload + form_or_body = post_body + else + # use HTTP forms in the payload + # TDOD use HTTP form encoding + form_or_body = form_params + end + + request = Crest::Request.new(http_method, + build_request_url(path, operation), + params: query_params, + headers: header_params, + #cookies: cookie_params, # TODO add cookies support + form: form_or_body, + logging: @config.debugging, + handle_errors: false + ) + + response = request.execute + + if @config.debugging + Log.debug {"HTTP response body ~BEGIN~\n#{response.body}\n~END~\n"} + end + + if !response.success? + if response.status == 0 + # Errors from libcurl will be made visible here + raise ApiError.new(code: 0, + message: response.body) + else + raise ApiError.new(code: response.status_code, + response_headers: response.headers, + message: response.body) + end + end + + return response.body, response.status_code, response.headers + end + + # Builds the HTTP request + # + # @param [String] http_method HTTP method/verb (e.g. POST) + # @param [String] path URL path (e.g. /account/new) + # @option opts [Hash] :header_params Header parameters + # @option opts [Hash] :query_params Query parameters + # @option opts [Hash] :form_params Query parameters + # @option opts [Object] :body HTTP body (JSON/XML) + # @return [Typhoeus::Request] A Typhoeus Request + def build_request(http_method, path, request, opts = {} of Symbol => String) + url = build_request_url(path, opts) + http_method = http_method.to_sym.downcase + + header_params = @default_headers.merge(opts[:header_params] || {} of Symbole => String) + query_params = opts[:query_params] || {} of Symbol => String + form_params = opts[:form_params] || {} of Symbol => String + + update_params_for_auth! header_params, query_params, opts[:auth_names] + + req_opts = { + :method => http_method, + :headers => header_params, + :params => query_params, + :params_encoding => @config.params_encoding, + :timeout => @config.timeout, + :verbose => @config.debugging + } + + if [:post, :patch, :put, :delete].include?(http_method) + req_body = build_request_body(header_params, form_params, opts[:body]) + req_opts.update body: req_body + if @config.debugging + Log.debug {"HTTP request body param ~BEGIN~\n#{req_body}\n~END~\n"} + end + end + request.headers = header_params + request.body = req_body + request.url url + request.params = query_params + download_file(request) if opts[:return_type] == "File" + request + end + + # Builds the HTTP request body + # + # @param [Hash] header_params Header parameters + # @param [Hash] form_params Query parameters + # @param [Object] body HTTP body (JSON/XML) + # @return [String] HTTP body data in the form of string + def build_request_body(header_params, form_params, body) + # http form + if header_params["Content-Type"] == "application/x-www-form-urlencoded" + data = URI.encode_www_form(form_params) + elsif header_params["Content-Type"] == "multipart/form-data" + data = {} of Symbol => String + form_params.each do |key, value| + case value + when ::File, ::Tempfile + # TODO hardcode to application/octet-stream, need better way to detect content type + data[key] = Faraday::UploadIO.new(value.path, "application/octet-stream", value.path) + when ::Array, nil + # let Faraday handle Array and nil parameters + data[key] = value + else + data[key] = value.to_s + end + end + elsif body + data = body.is_a?(String) ? body : body.to_json + else + data = nil + end + data + end + + # TODO fix streaming response + #def download_file(request) + # @stream = [] + + # # handle streaming Responses + # request.options.on_data = Proc.new do |chunk, overall_received_bytes| + # @stream << chunk + # end + #end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/api_error.cr b/samples/client/petstore/crystal/src/petstore/api_error.cr new file mode 100644 index 000000000000..4ae71b2b1086 --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/api_error.cr @@ -0,0 +1,41 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +module Petstore + class ApiError < Exception + getter code : Int32? + getter response_headers : Hash(String, Array(String) | String)? + + # Usage examples: + # ApiError.new + # ApiError.new(message: "message") + # ApiError.new(code: 500, response_headers: {}, message: "") + # ApiError.new(code: 404, message: "Not Found") + def initialize(@code , @message, @response_headers) + end + + def initialize(@code , @message) + end + + # Override to_s to display a friendly error message + def to_s + msg = "" + msg = msg + "\nHTTP status code: #{code}" if @code + msg = msg + "\nResponse headers: #{response_headers}" if @response_headers + if @message.nil? || @message.empty? + msg = msg + "\nError message: the server returns an error but the HTTP respone body is empty." + else + msg = msg + "\nResponse body: #{@message}" + end + + msg + end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/configuration.cr b/samples/client/petstore/crystal/src/petstore/configuration.cr new file mode 100644 index 000000000000..cbd2c642c0e9 --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/configuration.cr @@ -0,0 +1,271 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "log" + +module Petstore + class Configuration + # Defines url scheme + property scheme : String + + # Defines url host + property host : String + + # Defines url base path + property base_path : String + + # Define server configuration index + property server_index : Int32 + + # Define server operation configuration index + property server_operation_index : Hash(Symbol, String) + + # Default server variables + property server_variables : Hash(Symbol, String) + + # Default server operation variables + property server_operation_variables : Hash(Symbol, String) + + # Defines API keys used with API Key authentications. + # + # @return [Hash] key: parameter name, value: parameter value (API key) + # + # @example parameter name is "api_key", API key is "xxx" (e.g. "api_key=xxx" in query string) + # config.api_key[:"api_key"] = "xxx" + property api_key : Hash(Symbol, String) + + # Defines API key prefixes used with API Key authentications. + # + # @return [Hash] key: parameter name, value: API key prefix + # + # @example parameter name is "Authorization", API key prefix is "Token" (e.g. "Authorization: Token xxx" in headers) + # config.api_key_prefix[:"api_key"] = "Token" + property api_key_prefix : Hash(Symbol, String) + + # Defines the username used with HTTP basic authentication. + # + # @return [String] + property username : String? + + # Defines the password used with HTTP basic authentication. + # + # @return [String] + property password : String? + + # Defines the access token (Bearer) used with OAuth2. + property access_token : String? + + # Set this to enable/disable debugging. When enabled (set to true), HTTP request/response + # details will be logged with `logger.debug` (see the `logger` attribute). + # Default to false. + # + # @return [true, false] + property debugging : Bool + + # Defines the temporary folder to store downloaded files + # (for API endpoints that have file response). + # Default to use `Tempfile`. + # + # @return [String] + property temp_folder_path : String? + + # The time limit for HTTP request in seconds. + # Default to 0 (never times out). + property timeout : Int32 + + # Set this to false to skip client side validation in the operation. + # Default to true. + # @return [true, false] + property client_side_validation : Bool + + ### TLS/SSL setting + # Set this to false to skip verifying SSL certificate when calling API from https server. + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + #TODO attr_accessor :verify_ssl + + ### TLS/SSL setting + # Set this to false to skip verifying SSL host name + # Default to true. + # + # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks. + # + # @return [true, false] + # TODO attr_accessor :verify_ssl_host + + ### TLS/SSL setting + # Set this to customize the certificate file to verify the peer. + # + # @return [String] the path to the certificate file + # + # @see The `cainfo` option of Typhoeus, `--cert` option of libcurl. Related source code: + # https://github.com/typhoeus/typhoeus/blob/master/lib/typhoeus/easy_factory.rb#L145 + # TODO attr_accessor :ssl_ca_cert + + ### TLS/SSL setting + # Client certificate file (for client certificate) + # TODO attr_accessor :cert_file + + ### TLS/SSL setting + # Client private key file (for client certificate) + # TODO attr_accessor :key_file + + # Set this to customize parameters encoding of array parameter with multi collectionFormat. + # Default to Nil. + # + # @see The params_encoding option of Ethon. Related source code: + # https://github.com/typhoeus/ethon/blob/master/lib/ethon/easy/queryable.rb#L96 + #property params_encoding : String? + + def initialize + @scheme = "http" + @host = "petstore.swagger.io" + @base_path = "/v2" + @server_index = 0 + @server_operation_index = {} of Symbol => String + @server_variables = {} of Symbol => String + @server_operation_variables = {} of Symbol => String + @api_key = {} of Symbol => String + @api_key_prefix = {} of Symbol => String + @timeout = 0 + @client_side_validation = true + @verify_ssl = true + @verify_ssl_host = true + #@params_encoding = nil + #@cert_file = nil + #@key_file = nil + @debugging = false + @username = nil + @password = nil + @access_token = nil + @temp_folder_path = nil + + # TODO revise below to support block + #yield(self) if block_given? + end + + # The default Configuration object. + def self.default + @@default ||= Configuration.new + end + + def configure + yield(self) if block_given? + end + + def scheme=(scheme) + # remove :// from scheme + @scheme = scheme.sub(/:\/\//, "") + end + + def host=(host) + # remove http(s):// and anything after a slash + @host = host.sub(/https?:\/\//, "").split("/").first + end + + def base_path=(base_path) + # Add leading and trailing slashes to base_path + @base_path = "/#{base_path}".gsub(/\/+/, "/") + @base_path = "" if @base_path == "/" + end + + # Returns base URL for specified operation based on server settings + def base_url(operation = Nil) + # TODO revise below to support operation-level server setting + #index = server_operation_index.fetch(operation, server_index) + return "#{scheme}://#{[host, base_path].join("/").gsub(/\/+/, "/")}".sub(/\/+\z/, "") #if index == Nil + + #server_url(index, server_operation_variables.fetch(operation, server_variables), operation_server_settings[operation]) + end + + # Gets API key (with prefix if set). + # @param [String] param_name the parameter name of API key auth + def api_key_with_prefix(param_name) + if @api_key_prefix[param_name] + "#{@api_key_prefix[param_name]} #{@api_key[param_name]}" + else + @api_key[param_name] + end + end + + # Gets Basic Auth token string + def basic_auth_token + "Basic " + ["#{username}:#{password}"].pack("m").delete("\r\n") + end + + # Returns Auth Settings hash for api client. + def auth_settings + Hash{ "api_key" => { + type: "api_key", + in: "header", + key: "api_key", + value: api_key_with_prefix("api_key") + }, + "petstore_auth" => + { + type: "oauth2", + in: "header", + key: "Authorization", + value: "Bearer #{access_token}" + }, + } + end + + # Returns an array of Server setting + def server_settings + [ + { + url: "http://petstore.swagger.io/v2", + description: "No description provided", + } + ] + end + + def operation_server_settings + end + + # Returns URL based on server settings + # + # @param index array index of the server settings + # @param variables hash of variable and the corresponding value + def server_url(index, variables = {} of Symbol => String, servers = Nil) + servers = server_settings if servers == Nil + + # check array index out of bound + if (index < 0 || index >= servers.size) + raise ArgumentError.new("Invalid index #{index} when selecting the server. Must be less than #{servers.size}") + end + + server = servers[index] + url = server[:url] + + return url unless server.key? :variables + + # go through variable and assign a value + server[:variables].each do |name, variable| + if variables.key?(name) + if (!server[:variables][name].key?(:enum_values) || server[:variables][name][:enum_values].include?(variables[name])) + url.gsub! "{" + name.to_s + "}", variables[name] + else + raise ArgumentError.new("The variable `#{name}` in the server URL has invalid value #{variables[name]}. Must be #{server[:variables][name][:enum_values]}.") + end + else + # use default value + url.gsub! "{" + name.to_s + "}", server[:variables][name][:default_value] + end + end + + url + end + end +end diff --git a/samples/client/petstore/crystal/src/petstore/models/api_response.cr b/samples/client/petstore/crystal/src/petstore/models/api_response.cr new file mode 100644 index 000000000000..f177d75d9d2f --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/api_response.cr @@ -0,0 +1,188 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # Describes the result of uploading an image resource + class ApiResponse include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: code, type: Int32)] + property code : Int32 + + + @[JSON::Field(key: type, type: String)] + property _type : String + + + @[JSON::Field(key: message, type: String)] + property message : String + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@code : Int32 | Nil, @_type : String | Nil, @message : String | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + true + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + code == o.code && + _type == o._type && + message == o.message + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [code, _type, message].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end diff --git a/samples/client/petstore/crystal/src/petstore/models/category.cr b/samples/client/petstore/crystal/src/petstore/models/category.cr new file mode 100644 index 000000000000..9bd40aadf7eb --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/category.cr @@ -0,0 +1,200 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # A category for a pet + class Category include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: id, type: Int64)] + property id : Int64 + + + @[JSON::Field(key: name, type: String)] + property name : String + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@id : Int64 | Nil, @name : String | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + pattern = Regexp.new(/^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$/) + if !@name.nil? && @name !~ pattern + invalid_properties.push("invalid value for \"name\", must conform to the pattern #{pattern}.") + end + + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + return false if !@name.nil? && @name !~ Regexp.new(/^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$/) + true + end + + # Custom attribute writer method with validation + # @param [Object] name Value to be assigned + def name=(name) + pattern = Regexp.new(/^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$/) + if !name.nil? && name !~ pattern + raise ArgumentError.new("invalid value for \"name\", must conform to the pattern #{pattern}.") + end + + @name = name + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + id == o.id && + name == o.name + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [id, name].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end diff --git a/samples/client/petstore/crystal/src/petstore/models/order.cr b/samples/client/petstore/crystal/src/petstore/models/order.cr new file mode 100644 index 000000000000..e88c5428a6bd --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/order.cr @@ -0,0 +1,239 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # An order for a pets from the pet store + class Order include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: id, type: Int64)] + property id : Int64 + + + @[JSON::Field(key: petId, type: Int64)] + property pet_id : Int64 + + + @[JSON::Field(key: quantity, type: Int32)] + property quantity : Int32 + + + @[JSON::Field(key: shipDate, type: Time)] + property ship_date : Time + + + # Order Status + @[JSON::Field(key: status, type: String)] + property status : String + + + @[JSON::Field(key: complete, type: Bool)] + property complete : Bool + + class EnumAttributeValidator + getter datatype : String + getter allowable_values : Array(String) + + def initialize(datatype, allowable_values) + @datatype = datatype + @allowable_values = allowable_values.map do |value| + case datatype.to_s + when /Integer/i + value.to_i + when /Float/i + value.to_f + else + value + end + end + end + + def valid?(value) + !value || allowable_values.include?(value) + end + end + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@id : Int64 | Nil, @pet_id : Int64 | Nil, @quantity : Int32 | Nil, @ship_date : Time | Nil, @status : String | Nil, @complete : Bool | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + status_validator = EnumAttributeValidator.new("String", ["placed", "approved", "delivered"]) + return false unless status_validator.valid?(@status) + true + end + + # Custom attribute writer method checking allowed values (enum). + # @param [Object] status Object to be assigned + def status=(status) + validator = EnumAttributeValidator.new("String", ["placed", "approved", "delivered"]) + unless validator.valid?(status) + raise ArgumentError.new("invalid value for \"status\", must be one of #{validator.allowable_values}.") + end + @status = status + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + id == o.id && + pet_id == o.pet_id && + quantity == o.quantity && + ship_date == o.ship_date && + status == o.status && + complete == o.complete + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [id, pet_id, quantity, ship_date, status, complete].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end diff --git a/samples/client/petstore/crystal/src/petstore/models/pet.cr b/samples/client/petstore/crystal/src/petstore/models/pet.cr new file mode 100644 index 000000000000..6b6a19ef363d --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/pet.cr @@ -0,0 +1,249 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # A pet for sale in the pet store + class Pet include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: id, type: Int64)] + property id : Int64 + + + @[JSON::Field(key: category, type: Category)] + property category : Category + + + @[JSON::Field(key: name, type: String)] + property name : String + + + @[JSON::Field(key: photoUrls, type: Array(String))] + property photo_urls : Array(String) + + + @[JSON::Field(key: tags, type: Array(Tag))] + property tags : Array(Tag) + + + # pet status in the store + @[JSON::Field(key: status, type: String)] + property status : String + + class EnumAttributeValidator + getter datatype : String + getter allowable_values : Array(String) + + def initialize(datatype, allowable_values) + @datatype = datatype + @allowable_values = allowable_values.map do |value| + case datatype.to_s + when /Integer/i + value.to_i + when /Float/i + value.to_f + else + value + end + end + end + + def valid?(value) + !value || allowable_values.include?(value) + end + end + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@id : Int64 | Nil, @category : Category | Nil, @name : String, @photo_urls : Array(String), @tags : Array(Tag) | Nil, @status : String | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + if @name.nil? + invalid_properties.push("invalid value for \"name\", name cannot be nil.") + end + + if @photo_urls.nil? + invalid_properties.push("invalid value for \"photo_urls\", photo_urls cannot be nil.") + end + + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + return false if @name.nil? + return false if @photo_urls.nil? + status_validator = EnumAttributeValidator.new("String", ["available", "pending", "sold"]) + return false unless status_validator.valid?(@status) + true + end + + # Custom attribute writer method checking allowed values (enum). + # @param [Object] status Object to be assigned + def status=(status) + validator = EnumAttributeValidator.new("String", ["available", "pending", "sold"]) + unless validator.valid?(status) + raise ArgumentError.new("invalid value for \"status\", must be one of #{validator.allowable_values}.") + end + @status = status + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + id == o.id && + category == o.category && + name == o.name && + photo_urls == o.photo_urls && + tags == o.tags && + status == o.status + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [id, category, name, photo_urls, tags, status].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end diff --git a/samples/client/petstore/crystal/src/petstore/models/tag.cr b/samples/client/petstore/crystal/src/petstore/models/tag.cr new file mode 100644 index 000000000000..8c85fbcd3c22 --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/tag.cr @@ -0,0 +1,183 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # A tag for a pet + class Tag include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: id, type: Int64)] + property id : Int64 + + + @[JSON::Field(key: name, type: String)] + property name : String + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@id : Int64 | Nil, @name : String | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + true + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + id == o.id && + name == o.name + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [id, name].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end diff --git a/samples/client/petstore/crystal/src/petstore/models/user.cr b/samples/client/petstore/crystal/src/petstore/models/user.cr new file mode 100644 index 000000000000..75b75c3f800e --- /dev/null +++ b/samples/client/petstore/crystal/src/petstore/models/user.cr @@ -0,0 +1,214 @@ +# #OpenAPI Petstore +# +##This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +# +#The version of the OpenAPI document: 1.0.0 +# +#Generated by: https://openapi-generator.tech +#OpenAPI Generator version: 5.0.1-SNAPSHOT +# + +require "time" + +module Petstore + # A User who is purchasing from the pet store + class User include JSON::Serializable + include JSON::Serializable + @[JSON::Field(key: id, type: Int64)] + property id : Int64 + + + @[JSON::Field(key: username, type: String)] + property username : String + + + @[JSON::Field(key: firstName, type: String)] + property first_name : String + + + @[JSON::Field(key: lastName, type: String)] + property last_name : String + + + @[JSON::Field(key: email, type: String)] + property email : String + + + @[JSON::Field(key: password, type: String)] + property password : String + + + @[JSON::Field(key: phone, type: String)] + property phone : String + + + # User Status + @[JSON::Field(key: userStatus, type: Int32)] + property user_status : Int32 + + # Initializes the object + # @param [Hash] attributes Model attributes in the form of hash + def initialize(@id : Int64 | Nil, @username : String | Nil, @first_name : String | Nil, @last_name : String | Nil, @email : String | Nil, @password : String | Nil, @phone : String | Nil, @user_status : Int32 | Nil) + end + + # Show invalid properties with the reasons. Usually used together with valid? + # @return Array for valid properties with the reasons + def list_invalid_properties + invalid_properties = Array.new + invalid_properties + end + + # Check to see if the all the properties in the model are valid + # @return true if the model is valid + def valid? + true + end + + # Checks equality by comparing each attribute. + # @param [Object] Object to be compared + def ==(o) + return true if self.equal?(o) + self.class == o.class && + id == o.id && + username == o.username && + first_name == o.first_name && + last_name == o.last_name && + email == o.email && + password == o.password && + phone == o.phone && + user_status == o.user_status + end + + # @see the `==` method + # @param [Object] Object to be compared + def eql?(o) + self == o + end + + # Calculates hash code according to all attributes. + # @return [Integer] Hash code + def hash + [id, username, first_name, last_name, email, password, phone, user_status].hash + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def self.build_from_hash(attributes) + new.build_from_hash(attributes) + end + + # Builds the object from hash + # @param [Hash] attributes Model attributes in the form of hash + # @return [Object] Returns the model itself + def build_from_hash(attributes) + return nil unless attributes.is_a?(Hash) + self.class.openapi_types.each_pair do |key, type| + if attributes[self.class.attribute_map[key]].nil? && self.class.openapi_nullable.include?(key) + self.send("#{key}=", nil) + elsif type =~ /\AArray<(.*)>/i + # check to ensure the input is an array given that the attribute + # is documented as an array but the input is not + if attributes[self.class.attribute_map[key]].is_a?(Array) + self.send("#{key}=", attributes[self.class.attribute_map[key]].map { |v| _deserialize($1, v) }) + end + elsif !attributes[self.class.attribute_map[key]].nil? + self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]])) + end + end + + self + end + + # Deserializes the data based on type + # @param string type Data type + # @param string value Value to be deserialized + # @return [Object] Deserialized data + def _deserialize(type, value) + case type.to_sym + when :Time + Time.parse(value) + when :Date + Date.parse(value) + when :String + value.to_s + when :Integer + value.to_i + when :Float + value.to_f + when :Boolean + if value.to_s =~ /\A(true|t|yes|y|1)\z/i + true + else + false + end + when :Object + # generic object (usually a Hash), return directly + value + when /\AArray<(?.+)>\z/ + inner_type = Regexp.last_match[:inner_type] + value.map { |v| _deserialize(inner_type, v) } + when /\AHash<(?.+?), (?.+)>\z/ + k_type = Regexp.last_match[:k_type] + v_type = Regexp.last_match[:v_type] + ({} of Symbol => String).tap do |hash| + value.each do |k, v| + hash[_deserialize(k_type, k)] = _deserialize(v_type, v) + end + end + else # model + # models (e.g. Pet) or oneOf + klass = Petstore.const_get(type) + klass.respond_to?(:openapi_one_of) ? klass.build(value) : klass.build_from_hash(value) + end + end + + # Returns the string representation of the object + # @return [String] String presentation of the object + def to_s + to_hash.to_s + end + + # to_body is an alias to to_hash (backward compatibility) + # @return [Hash] Returns the object in the form of hash + def to_body + to_hash + end + + # Returns the object in the form of hash + # @return [Hash] Returns the object in the form of hash + def to_hash + hash = {} of Symbol => String + self.class.attribute_map.each_pair do |attr, param| + value = self.send(attr) + if value.nil? + is_nullable = self.class.openapi_nullable.include?(attr) + next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}")) + end + + hash[param] = _to_hash(value) + end + hash + end + + # Outputs non-array value in the form of hash + # For object, use to_hash. Otherwise, just return the value + # @param [Object] value Any valid value + # @return [Hash] Returns the value in the form of hash + def _to_hash(value) + if value.is_a?(Array) + value.compact.map { |v| _to_hash(v) } + elsif value.is_a?(Hash) + ({} of Symbol => String).tap do |hash| + value.each { |k, v| hash[k] = _to_hash(v) } + end + elsif value.respond_to? :to_hash + value.to_hash + else + value + end + end + + end + +end