mirror of
https://github.com/OpenAPITools/openapi-generator.git
synced 2025-05-12 12:40:53 +00:00
[Scala] added new scala-cask generator for the cask framework (#18344)
* Ran `./new.sh -n scala-cask -s` to generate a new Scala Cask impl * removed scala-cask-petstore * Added Scala-cask param parser for BigDecimals * added scala cask to samples * splitting out validation and json * Added GitHub workflow support * regenerated cask samples * cask build fix for local builds * regenerated samples * trying to reproduce failed cask build. checking in compiles sources, which have been reformatted * reverted whaitespace change * cask fix - adding gitignored files * scala cask toLowreCase fix * scala cask toUpperCase fix * cask regenerated samples * improved exception handling for scala cask * File separator fix for windows * Noob fix for cask * regenerated api package * Removed environment variable settings, debug code * Updated samples * moved scala-cask output * Regenerated samples * scala cask fix * Updated scala cask settings for more typical package structure Removed cask client samples * cask - reran generate samples * Removed duplicate ScalaCaskServer entry * minor enhancements * update samples * update templates --------- Co-authored-by: aaron.pritzlaff <aaron@kindservices.co.uk>
This commit is contained in:
parent
ef36ea410e
commit
2bfc5a3958
1
.github/workflows/samples-scala.yaml
vendored
1
.github/workflows/samples-scala.yaml
vendored
@ -32,6 +32,7 @@ jobs:
|
||||
- samples/server/petstore/scalatra
|
||||
- samples/server/petstore/scala-finch # cannot be tested with jdk11
|
||||
- samples/server/petstore/scala-http4s-server
|
||||
- samples/server/petstore/scala-cask
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v4
|
||||
|
@ -85,7 +85,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.1, .NET Core 3.1, .NET 5.0. Libraries: RestSharp, GenericHost, HttpClient), **C++** (Arduino, cpp-restsdk, Qt5, Tizen, Unreal Engine 4), **Clojure**, **Crystal**, **Dart**, **Elixir**, **Elm**, **Eiffel**, **Erlang**, **Go**, **Groovy**, **Haskell** (http-client, Servant), **Java** (Apache HttpClient 4.x, Apache HttpClient 5.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, Helidon), **Jetbrains HTTP Client**, **Julia**, **k6**, **Kotlin**, **Lua**, **N4JS**, **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** (hyper, reqwest, rust-server), **Scala** (akka, http4s, scalaz, sttp, swagger-async-httpclient, pekko), **Swift** (2.x, 3.x, 4.x, 5.x), **Typescript** (AngularJS, Angular (9.x - 17.x), Aurelia, Axios, Fetch, Inversify, jQuery, Nestjs, Node, redux-query, Rxjs), **XoJo**, **Zapier** |
|
||||
| **Server stubs** | **Ada**, **C#** (ASP.NET Core, Azure Functions), **C++** (Pistache, Restbed, Qt5 QHTTPEngine), **Erlang**, **F#** (Giraffe), **Go** (net/http, Gin, Echo), **Haskell** (Servant, Yesod), **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/), [Apache Camel](https://camel.apache.org/), [Helidon](https://helidon.io/)), **Julia**, **Kotlin** (Spring Boot, [Ktor](https://github.com/ktorio/ktor), [Vert.x](https://vertx.io/)), **PHP** (Laravel, Lumen, [Mezzio (fka Zend Expressive)](https://github.com/mezzio/mezzio), Slim, Silex, [Symfony](https://symfony.com/)), **Python** (FastAPI, Flask), **NodeJS**, **Ruby** (Sinatra, Rails5), **Rust** ([rust-server](https://openapi-generator.tech/docs/generators/rust-server/)), **Scala** (Akka, [Finch](https://github.com/finagle/finch), [Lagom](https://github.com/lagom/lagom), [Play](https://www.playframework.com/), Scalatra) |
|
||||
| **Server stubs** | **Ada**, **C#** (ASP.NET Core, Azure Functions), **C++** (Pistache, Restbed, Qt5 QHTTPEngine), **Erlang**, **F#** (Giraffe), **Go** (net/http, Gin, Echo), **Haskell** (Servant, Yesod), **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/), [Apache Camel](https://camel.apache.org/), [Helidon](https://helidon.io/)), **Julia**, **Kotlin** (Spring Boot, [Ktor](https://github.com/ktorio/ktor), [Vert.x](https://vertx.io/)), **PHP** (Laravel, Lumen, [Mezzio (fka Zend Expressive)](https://github.com/mezzio/mezzio), Slim, Silex, [Symfony](https://symfony.com/)), **Python** (FastAPI, Flask), **NodeJS**, **Ruby** (Sinatra, Rails5), **Rust** ([rust-server](https://openapi-generator.tech/docs/generators/rust-server/)), **Scala** (Akka, [Finch](https://github.com/finagle/finch), [Lagom](https://github.com/lagom/lagom), [Play](https://www.playframework.com/), [Cask](https://github.com/com-lihaoyi/cask), Scalatra) |
|
||||
| **API documentation generators** | **HTML**, **Confluence Wiki**, **Asciidoc**, **Markdown**, **PlantUML** |
|
||||
| **Configuration files** | [**Apache2**](https://httpd.apache.org/) |
|
||||
| **Others** | **GraphQL**, **JMeter**, **Ktorm**, **MySQL Schema**, **Postman Collection**, **Protocol Buffer**, **WSDL** |
|
||||
@ -1102,6 +1102,7 @@ Here is a list of template creators:
|
||||
* Ruby on Rails 5: @zlx
|
||||
* Rust (rust-server): @metaswitch
|
||||
* Scala Akka: @Bouillie
|
||||
* Scala Cask: @aaronp
|
||||
* Scala Finch: @jimschubert [:heart:](https://www.patreon.com/jimschubert)
|
||||
* Scala Lagom: @gmkumar2005
|
||||
* Scala Play: @adigerber
|
||||
|
13
bin/configs/scala-cask-petstore-new.yaml
Normal file
13
bin/configs/scala-cask-petstore-new.yaml
Normal file
@ -0,0 +1,13 @@
|
||||
generatorName: scala-cask
|
||||
outputDir: samples/server/petstore/scala-cask
|
||||
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
|
||||
templateDir: modules/openapi-generator/src/main/resources/scala-cask
|
||||
additionalProperties:
|
||||
hideGenerationTimestamp: "true"
|
||||
artifactId: scala-cask-petstore
|
||||
groupId: "cask.groupId"
|
||||
package: "sample.cask"
|
||||
modelPackage: "sample.cask.model"
|
||||
apiPackage: "sample.cask.api"
|
||||
gitRepoId: "sample-cask-repo"
|
||||
gitUserId: "sample-cask-user"
|
@ -134,6 +134,7 @@ The following generators are available:
|
||||
* [rust-axum (beta)](generators/rust-axum.md)
|
||||
* [rust-server](generators/rust-server.md)
|
||||
* [scala-akka-http-server (beta)](generators/scala-akka-http-server.md)
|
||||
* [scala-cask](generators/scala-cask.md)
|
||||
* [scala-finch](generators/scala-finch.md)
|
||||
* [scala-http4s-server](generators/scala-http4s-server.md)
|
||||
* [scala-lagom-server](generators/scala-lagom-server.md)
|
||||
|
261
docs/generators/scala-cask.md
Normal file
261
docs/generators/scala-cask.md
Normal file
@ -0,0 +1,261 @@
|
||||
---
|
||||
title: Documentation for the scala-cask Generator
|
||||
---
|
||||
|
||||
## METADATA
|
||||
|
||||
| Property | Value | Notes |
|
||||
| -------- | ----- | ----- |
|
||||
| generator name | scala-cask | pass this to the generate command after -g |
|
||||
| generator stability | STABLE | |
|
||||
| generator type | SERVER | |
|
||||
| generator language | Scala | |
|
||||
| generator default templating engine | mustache | |
|
||||
| helpTxt | Generates a scala-cask server. | |
|
||||
|
||||
## CONFIG OPTIONS
|
||||
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|
|
||||
|apiPackage|package for generated api classes| |null|
|
||||
|artifactId|artifactId in generated pom.xml. This also becomes part of the generated library's filename| |null|
|
||||
|dateLibrary|Option. Date library to use|<dl><dt>**joda**</dt><dd>Joda (for legacy app)</dd><dt>**java8**</dt><dd>Java 8 native JSR310 (preferred for JDK 1.8+)</dd></dl>|java8|
|
||||
|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.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
|
||||
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|
||||
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enum's are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|
||||
|gitRepoId|Git repo ID, e.g. openapi-generator.| |null|
|
||||
|gitUserId|Git user ID, e.g. openapitools.| |null|
|
||||
|groupId|groupId in generated pom.xml| |null|
|
||||
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>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.</dd></dl>|true|
|
||||
|modelPackage|package for generated models| |null|
|
||||
|modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase|
|
||||
|packageName|packageDescription| |null|
|
||||
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|
||||
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|
||||
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|
||||
|sourceFolder|source folder for generated code| |null|
|
||||
|
||||
## IMPORT MAPPING
|
||||
|
||||
| Type/Alias | Imports |
|
||||
| ---------- | ------- |
|
||||
|Array|java.util.List|
|
||||
|ArrayList|java.util.ArrayList|
|
||||
|Date|java.util.Date|
|
||||
|DateTime|org.joda.time.*|
|
||||
|File|java.io.File|
|
||||
|HashMap|java.util.HashMap|
|
||||
|ListBuffer|scala.collection.mutable.ListBuffer|
|
||||
|ListSet|scala.collection.immutable.ListSet|
|
||||
|LocalDate|org.joda.time.*|
|
||||
|LocalDateTime|org.joda.time.*|
|
||||
|LocalTime|org.joda.time.*|
|
||||
|Seq|scala.collection.immutable.Seq|
|
||||
|Set|scala.collection.immutable.Set|
|
||||
|Timestamp|java.sql.Timestamp|
|
||||
|URI|java.net.URI|
|
||||
|UUID|java.util.UUID|
|
||||
|
||||
|
||||
## INSTANTIATION TYPES
|
||||
|
||||
| Type/Alias | Instantiated By |
|
||||
| ---------- | --------------- |
|
||||
|set|Set|
|
||||
|
||||
|
||||
## LANGUAGE PRIMITIVES
|
||||
|
||||
<ul class="column-ul">
|
||||
<li>Any</li>
|
||||
<li>Array</li>
|
||||
<li>Boolean</li>
|
||||
<li>Byte</li>
|
||||
<li>Double</li>
|
||||
<li>Float</li>
|
||||
<li>Int</li>
|
||||
<li>List</li>
|
||||
<li>Long</li>
|
||||
<li>Map</li>
|
||||
<li>Object</li>
|
||||
<li>Seq</li>
|
||||
<li>String</li>
|
||||
<li>boolean</li>
|
||||
</ul>
|
||||
|
||||
## RESERVED WORDS
|
||||
|
||||
<ul class="column-ul">
|
||||
<li>abstract</li>
|
||||
<li>assert</li>
|
||||
<li>boolean</li>
|
||||
<li>break</li>
|
||||
<li>byte</li>
|
||||
<li>case</li>
|
||||
<li>catch</li>
|
||||
<li>char</li>
|
||||
<li>class</li>
|
||||
<li>const</li>
|
||||
<li>continue</li>
|
||||
<li>default</li>
|
||||
<li>do</li>
|
||||
<li>double</li>
|
||||
<li>else</li>
|
||||
<li>enum</li>
|
||||
<li>extends</li>
|
||||
<li>final</li>
|
||||
<li>finally</li>
|
||||
<li>float</li>
|
||||
<li>for</li>
|
||||
<li>goto</li>
|
||||
<li>if</li>
|
||||
<li>implements</li>
|
||||
<li>import</li>
|
||||
<li>instanceof</li>
|
||||
<li>int</li>
|
||||
<li>interface</li>
|
||||
<li>long</li>
|
||||
<li>native</li>
|
||||
<li>new</li>
|
||||
<li>package</li>
|
||||
<li>private</li>
|
||||
<li>protected</li>
|
||||
<li>public</li>
|
||||
<li>return</li>
|
||||
<li>short</li>
|
||||
<li>static</li>
|
||||
<li>strictfp</li>
|
||||
<li>super</li>
|
||||
<li>switch</li>
|
||||
<li>synchronized</li>
|
||||
<li>this</li>
|
||||
<li>throw</li>
|
||||
<li>throws</li>
|
||||
<li>transient</li>
|
||||
<li>try</li>
|
||||
<li>type</li>
|
||||
<li>void</li>
|
||||
<li>volatile</li>
|
||||
<li>while</li>
|
||||
</ul>
|
||||
|
||||
## 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
|
||||
|Uuid|✗|
|
||||
|Array|✓|OAS2,OAS3
|
||||
|Null|✗|OAS3
|
||||
|AnyType|✗|OAS2,OAS3
|
||||
|Object|✓|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
|
||||
|allOf|✗|OAS2,OAS3
|
||||
|anyOf|✗|OAS3
|
||||
|oneOf|✗|OAS3
|
||||
|not|✗|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
|
||||
|SignatureAuth|✗|OAS3
|
||||
|AWSV4Signature|✗|ToolingExtension
|
||||
|
||||
### Wire Format Feature
|
||||
| Name | Supported | Defined By |
|
||||
| ---- | --------- | ---------- |
|
||||
|JSON|✓|OAS2,OAS3
|
||||
|XML|✓|OAS2,OAS3
|
||||
|PROTOBUF|✗|ToolingExtension
|
||||
|Custom|✗|OAS2,OAS3
|
@ -0,0 +1,845 @@
|
||||
/*
|
||||
* 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.OpenAPI;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.tags.Tag;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.openapitools.codegen.*;
|
||||
import org.openapitools.codegen.model.ModelMap;
|
||||
import org.openapitools.codegen.model.ModelsMap;
|
||||
import org.openapitools.codegen.model.OperationsMap;
|
||||
import org.openapitools.codegen.serializer.SerializerUtils;
|
||||
import org.openapitools.codegen.utils.ModelUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.openapitools.codegen.utils.StringUtils.camelize;
|
||||
|
||||
public class ScalaCaskServerCodegen extends AbstractScalaCodegen implements CodegenConfig {
|
||||
public static final String PROJECT_NAME = "projectName";
|
||||
|
||||
private final Logger LOGGER = LoggerFactory.getLogger(ScalaCaskServerCodegen.class);
|
||||
|
||||
public CodegenType getTag() {
|
||||
return CodegenType.SERVER;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "scala-cask";
|
||||
}
|
||||
|
||||
public String getHelp() {
|
||||
return "Generates a scala-cask server.";
|
||||
}
|
||||
|
||||
protected String artifactVersion = "0.0.1";
|
||||
|
||||
static String ApiServiceTemplate = "apiService.mustache";
|
||||
|
||||
public ScalaCaskServerCodegen() {
|
||||
super();
|
||||
|
||||
outputFolder = "generated-code/scala-cask";
|
||||
|
||||
embeddedTemplateDir = templateDir = "scala-cask";
|
||||
apiPackage = "Apis";
|
||||
modelPackage = "Models";
|
||||
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
|
||||
|
||||
outputFolder = "generated-code/cask";
|
||||
|
||||
modelTestTemplateFiles.put("modelTest.mustache", ".scala");
|
||||
modelTemplateFiles.put("model.mustache", ".scala");
|
||||
modelTemplateFiles.put("modelData.mustache", "Data.scala");
|
||||
|
||||
apiTemplateFiles.put("api.mustache", ".scala");
|
||||
apiTemplateFiles.put("apiRoutes.mustache", ".scala");
|
||||
apiTemplateFiles.put(ApiServiceTemplate, "Service.scala");
|
||||
|
||||
embeddedTemplateDir = templateDir = "scala-cask";
|
||||
|
||||
setReservedWordsLowerCase(
|
||||
Arrays.asList(
|
||||
"abstract", "continue", "for", "new", "switch", "assert",
|
||||
"default", "if", "package", "synchronized", "boolean", "do", "goto", "private",
|
||||
"this", "break", "double", "implements", "protected", "throw", "byte", "else",
|
||||
"import", "public", "throws", "case", "enum", "instanceof", "return", "transient",
|
||||
"catch", "extends", "int", "short", "try", "char", "final", "interface", "static",
|
||||
"void", "class", "finally", "long", "strictfp", "volatile", "const", "float",
|
||||
"native", "super", "while", "type")
|
||||
);
|
||||
|
||||
defaultIncludes = new HashSet<String>(
|
||||
Arrays.asList("double",
|
||||
"Int",
|
||||
"Long",
|
||||
"Float",
|
||||
"Double",
|
||||
"char",
|
||||
"float",
|
||||
"String",
|
||||
"boolean",
|
||||
"Boolean",
|
||||
"Double",
|
||||
"Integer",
|
||||
"Long",
|
||||
"Float",
|
||||
"List",
|
||||
"Set",
|
||||
"Map")
|
||||
);
|
||||
|
||||
typeMapping.put("integer", "Int");
|
||||
typeMapping.put("long", "Long");
|
||||
//TODO binary should be mapped to byte array
|
||||
// mapped to String as a workaround
|
||||
typeMapping.put("binary", "String");
|
||||
|
||||
cliOptions.add(new CliOption(CodegenConstants.GROUP_ID, CodegenConstants.GROUP_ID_DESC));
|
||||
cliOptions.add(new CliOption(CodegenConstants.ARTIFACT_ID, CodegenConstants.ARTIFACT_ID_DESC));
|
||||
cliOptions.add(new CliOption(CodegenConstants.GIT_REPO_ID, CodegenConstants.GIT_REPO_ID_DESC));
|
||||
cliOptions.add(new CliOption(CodegenConstants.GIT_USER_ID, CodegenConstants.GIT_USER_ID_DESC));
|
||||
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, CodegenConstants.PACKAGE_DESCRIPTION));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toDefaultValue(Schema p) {
|
||||
if (ModelUtils.isMapSchema(p)) {
|
||||
String inner = getSchemaType(ModelUtils.getAdditionalProperties(p));
|
||||
return "Map[String, " + inner + "]() ";
|
||||
}
|
||||
return super.toDefaultValue(p);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String testPackage() {
|
||||
return "src/test/scala";
|
||||
}
|
||||
|
||||
public String toModelTestFilename(String name) {
|
||||
String n = super.toModelTestFilename(name);
|
||||
return (modelPackage + "." + n).replace('.', '/');
|
||||
}
|
||||
|
||||
private String ensureProp(String key, String defaultValue) {
|
||||
if (additionalProperties.containsKey(key) && !additionalProperties.get(key).toString().trim().isEmpty()) {
|
||||
return (String) additionalProperties.get(key);
|
||||
} else {
|
||||
additionalProperties.put(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void processOpts() {
|
||||
super.processOpts();
|
||||
|
||||
final String groupId = ensureProp(CodegenConstants.GROUP_ID, "org.openapitools");
|
||||
ensureProp(CodegenConstants.ARTIFACT_ID, "caskgen");
|
||||
artifactVersion = ensureProp(CodegenConstants.ARTIFACT_VERSION, "0.0.1");
|
||||
gitRepoId = ensureProp(CodegenConstants.GIT_REPO_ID, "<your git repo -- set 'gitRepoId'>");
|
||||
gitUserId = ensureProp(CodegenConstants.GIT_USER_ID, "<your git user -- set 'gitUserId'>");
|
||||
|
||||
String basePackage = ensureProp(CodegenConstants.PACKAGE_NAME, groupId + ".server");
|
||||
apiPackage = ensureProp(CodegenConstants.API_PACKAGE, basePackage + ".api");
|
||||
modelPackage = ensureProp(CodegenConstants.MODEL_PACKAGE, basePackage + ".model");
|
||||
|
||||
|
||||
final String apiPath = "src/main/scala/" + apiPackage.replace('.', '/');
|
||||
final String modelPath = "src/main/scala/" + modelPackage.replace('.', '/');
|
||||
|
||||
final List<String> appFullPath = Arrays.stream(apiPath.split("/")).collect(Collectors.toList());
|
||||
final String appFolder = String.join("/", appFullPath.subList(0, appFullPath.size() - 1));
|
||||
|
||||
additionalProperties.put("appName", "Cask App");
|
||||
additionalProperties.put("appDescription", "A cask service");
|
||||
additionalProperties.put("infoUrl", "https://openapi-generator.tech");
|
||||
additionalProperties.put("infoEmail", infoEmail);
|
||||
additionalProperties.put("licenseInfo", "All rights reserved");
|
||||
additionalProperties.put("licenseUrl", "http://apache.org/licenses/LICENSE-2.0.html");
|
||||
additionalProperties.put(CodegenConstants.INVOKER_PACKAGE, invokerPackage);
|
||||
additionalProperties.put("openbrackets", "{{");
|
||||
additionalProperties.put("closebrackets", "}}");
|
||||
|
||||
supportingFiles.add(new SupportingFile("example.mustache", "example", "Server.scala"));
|
||||
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "example", "Dockerfile"));
|
||||
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
|
||||
supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt"));
|
||||
supportingFiles.add(new SupportingFile("bulidAndPublish.yml.mustache", "", ".github/workflows/bulidAndPublish.yml"));
|
||||
supportingFiles.add(new SupportingFile("build.sc.mustache", "", "build.sc"));
|
||||
supportingFiles.add(new SupportingFile(".scalafmt.conf.mustache", "", ".scalafmt.conf"));
|
||||
supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore"));
|
||||
supportingFiles.add(new SupportingFile("appPackage.mustache", appFolder, "package.scala"));
|
||||
supportingFiles.add(new SupportingFile("apiPackage.mustache", apiPath, "package.scala"));
|
||||
supportingFiles.add(new SupportingFile("modelPackage.mustache", modelPath, "package.scala"));
|
||||
supportingFiles.add(new SupportingFile("exampleApp.mustache", appFolder, "ExampleApp.scala"));
|
||||
supportingFiles.add(new SupportingFile("baseApp.mustache", appFolder, "BaseApp.scala"));
|
||||
supportingFiles.add(new SupportingFile("openapiRoute.mustache", apiPath, "OpenApiRoutes.scala"));
|
||||
supportingFiles.add(new SupportingFile("appRoutes.mustache", appFolder, "AppRoutes.scala"));
|
||||
supportingFiles.add(new SupportingFile("project/build.properties", "project", "build.properties"));
|
||||
supportingFiles.add(new SupportingFile("project/plugins.sbt", "project", "plugins.sbt"));
|
||||
|
||||
|
||||
instantiationTypes.put("array", "Seq");
|
||||
instantiationTypes.put("map", "Map");
|
||||
|
||||
importMapping = new HashMap<String, String>();
|
||||
importMapping.put("BigDecimal", "scala.math.BigDecimal");
|
||||
importMapping.put("UUID", "java.util.UUID");
|
||||
importMapping.put("File", "java.io.File");
|
||||
importMapping.put("Date", "java.time.LocalDate as Date");
|
||||
importMapping.put("Timestamp", "java.sql.Timestamp");
|
||||
importMapping.put("Map", "Map");
|
||||
importMapping.put("HashMap", "Map");
|
||||
importMapping.put("Array", "Seq");
|
||||
importMapping.put("ArrayList", "Seq");
|
||||
importMapping.put("List", "Seq");
|
||||
importMapping.put("DateTime", "java.time.LocalDateTime");
|
||||
importMapping.put("LocalDateTime", "java.time.LocalDateTime");
|
||||
importMapping.put("LocalDate", "java.time.LocalDate");
|
||||
importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
|
||||
importMapping.put("LocalTime", "java.time.LocalTime");
|
||||
}
|
||||
|
||||
static boolean consumesMimetype(CodegenOperation op, String mimetype) {
|
||||
// people don't always/often specify the 'consumes' property, so we assume true when
|
||||
// the optional 'consumes' is null or empty
|
||||
boolean defaultRetValue = true;
|
||||
|
||||
final List<Map<String, String>> consumes = op.consumes;
|
||||
if (consumes != null) {
|
||||
for (Map<String, String> c : consumes) {
|
||||
final String mt = c.get("mediaType");
|
||||
if (mt.equalsIgnoreCase(mimetype)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return defaultRetValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static String formatMap(Map<?, ?> map) {
|
||||
StringBuilder mapAsString = new StringBuilder("{");
|
||||
for (Object key : map.keySet().stream().sorted().collect(Collectors.toList())) {
|
||||
mapAsString.append(key + " -- " + map.get(key) + ",\n");
|
||||
}
|
||||
if (mapAsString.length() > 1) {
|
||||
mapAsString.delete(mapAsString.length() - 2, mapAsString.length());
|
||||
}
|
||||
mapAsString.append("}");
|
||||
return mapAsString.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toApiName(String name) {
|
||||
if (name.isEmpty()) {
|
||||
return "DefaultApi";
|
||||
}
|
||||
name = sanitizeName(name);
|
||||
return camelize(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apiFilename(String templateName, String tag) {
|
||||
String suffix = apiTemplateFiles().get(templateName);
|
||||
String fn = toApiFilename(tag);
|
||||
if (templateName.equals(ApiServiceTemplate)) {
|
||||
return apiFileFolder() + '/' + fn + suffix;
|
||||
} else {
|
||||
return apiFileFolder() + '/' + fn + "Routes" + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
static String capitalise(String p) {
|
||||
if (p.length() < 2) {
|
||||
return p.toUpperCase(Locale.ROOT);
|
||||
} else {
|
||||
String first = "" + p.charAt(0);
|
||||
return first.toUpperCase(Locale.ROOT) + p.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// thanks FlaskConnectionCodeGen
|
||||
private static List<Map<String, Object>> getOperations(Map<String, Object> objs) {
|
||||
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
|
||||
Map<String, Object> apiInfo = (Map<String, Object>) objs.get("apiInfo");
|
||||
List<Map<String, Object>> apis = (List<Map<String, Object>>) apiInfo.get("apis");
|
||||
for (Map<String, Object> api : apis) {
|
||||
Map<String, Object> operations = (Map<String, Object>) api.get("operations");
|
||||
result.add(operations);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
|
||||
List<Map<String, Object>> operations = getOperations(objs);
|
||||
for (int i = 0; i < operations.size(); i++) {
|
||||
operations.get(i).put("hasMore", i < operations.size() - 1);
|
||||
}
|
||||
objs.put("operations", operations);
|
||||
return super.postProcessSupportingFileData(objs);
|
||||
}
|
||||
|
||||
protected String getResourceFolder() {
|
||||
String src = getSourceFolder();
|
||||
|
||||
List<String> parts = Arrays.stream(src.split("/", -1)).collect(Collectors.toList());
|
||||
if (parts.isEmpty()) {
|
||||
return "resources";
|
||||
} else {
|
||||
String srcMain = String.join("/", parts.subList(0, parts.size() - 1));
|
||||
return srcMain + "/resources";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processOpenAPI(OpenAPI openAPI) {
|
||||
String jsonOpenAPI = SerializerUtils.toJsonString(openAPI);
|
||||
|
||||
try {
|
||||
String outputFile = getOutputDir() + "/" + getResourceFolder() + "/openapi.json";
|
||||
FileUtils.writeStringToFile(new File(outputFile), jsonOpenAPI, StandardCharsets.UTF_8);
|
||||
LOGGER.info("wrote file to {}", outputFile);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ParamPart {
|
||||
final CodegenParameter param;
|
||||
final String name;
|
||||
final boolean isParam;
|
||||
|
||||
// flag for if there are more path parts
|
||||
boolean hasMore;
|
||||
// flag for if there are more path parts which are parameters
|
||||
boolean hasMoreParams;
|
||||
|
||||
final String conversion;
|
||||
|
||||
public ParamPart(String name, CodegenParameter param) {
|
||||
this.name = name;
|
||||
this.param = param;
|
||||
this.isParam = param != null;
|
||||
this.hasMore = true;
|
||||
this.conversion = !isParam || param.isString ? "" : ".to" + param.dataType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cask will compile but 'initialize' can throw a route overlap exception:
|
||||
* <p>
|
||||
* {{{
|
||||
* Routes overlap with wildcards: get /user/logout, get /user/:username, get /user/login
|
||||
* }}}
|
||||
* <p>
|
||||
* Note: The same error persists even if the suffixes are unique:
|
||||
* {{{
|
||||
* Routes overlap with wildcards: get /user/logout/3, get /user/:username/1, get /user/login/2
|
||||
* }}}
|
||||
* <p>
|
||||
* To fix this, we need to identify and resolve conflicts in our generated code.
|
||||
* <p>
|
||||
* # How do we identify conflicts?
|
||||
* </p>
|
||||
* <p>
|
||||
* 1. group routes by their non-param prefixes.
|
||||
* <p>
|
||||
* 2. add an "x-annotation" vendor extension for operations
|
||||
* <p>
|
||||
* 3. add a list of "RouteGroups" which can manually delegate as per below
|
||||
* <p>
|
||||
* <p>
|
||||
* # How do we resolve conflicts?
|
||||
* <p>
|
||||
* We leave out the cask route annotation on the conflicting operations, e.g. :
|
||||
* {{{
|
||||
* //conflict: @cask.get("/user/:username")
|
||||
* def getUserByName(username: String, request: cask.Request) = ...
|
||||
* }}}
|
||||
* <p>
|
||||
* and we introduce a new discriminator function to "manually" call those conflicts:
|
||||
* {{{
|
||||
*
|
||||
* @cask.get("/user", subpath = true)
|
||||
* def userRouteDescriminator(request: cask.Request) = {
|
||||
* request.remainingPathSegments match {
|
||||
* case Seq("logout") => logoutUser(request)
|
||||
* case Seq("login") => loginUser(request)
|
||||
* case Seq(param) => getUserByName(param, request)
|
||||
* }
|
||||
* }
|
||||
* }}}
|
||||
*/
|
||||
public static class OperationGroup {
|
||||
List<CodegenOperation> operations = new ArrayList<>();
|
||||
final String pathPrefix;
|
||||
final String httpMethod;
|
||||
final String caskAnnotation;
|
||||
final String methodName;
|
||||
|
||||
// TODO - multiple operations may have the same query params, so we'll need to somehow merge them (and take the right type)
|
||||
public boolean hasGroupQueryParams() {
|
||||
return operations.stream().flatMap(op -> op.queryParams.stream()).count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is invoked from `scala-cask/apiRoutesQueryParamsTyped.mustache`
|
||||
*
|
||||
* @return the CodegenParameters
|
||||
*/
|
||||
public List<CodegenParameter> getGroupQueryParams() {
|
||||
List<CodegenParameter> list = operations.stream().flatMap(op -> op.queryParams.stream()).map(p -> {
|
||||
final CodegenParameter copy = p.copy();
|
||||
copy.vendorExtensions.put("x-default-value", defaultValue(p));
|
||||
copy.required = false; // all our query params are optional for our work-around as it's a super-set of a few different routes
|
||||
copy.dataType = asScalaDataType(copy, false, true, true);
|
||||
copy.defaultValue = defaultValue(copy);
|
||||
return copy;
|
||||
}
|
||||
).collect(Collectors.toList());
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
List<String> ops = operations.stream().map(o -> o.path + "\n").collect(Collectors.toList());
|
||||
return httpMethod + " " + pathPrefix + " w/ " + operations.size() + " operations:\n" + String.join("", ops);
|
||||
}
|
||||
|
||||
public OperationGroup(String httpMethod, String pathPrefix) {
|
||||
this.httpMethod = httpMethod;
|
||||
this.pathPrefix = pathPrefix;
|
||||
caskAnnotation = "@cask." + httpMethod.toLowerCase(Locale.ROOT);
|
||||
|
||||
List<String> stripped = Arrays.stream(pathPrefix.split("/", -1))
|
||||
.map(ScalaCaskServerCodegen::capitalise).collect(Collectors.toList());
|
||||
|
||||
methodName = "routeWorkAroundFor" + capitalise(httpMethod) + String.join("", stripped);
|
||||
}
|
||||
|
||||
public void add(CodegenOperation op) {
|
||||
if (!op.path.startsWith(pathPrefix)) {
|
||||
throw new IllegalArgumentException("inconsistent path: " + pathPrefix);
|
||||
}
|
||||
if (!op.httpMethod.equals(httpMethod)) {
|
||||
throw new IllegalArgumentException("inconsistent method: " + httpMethod);
|
||||
}
|
||||
|
||||
final List<ScalaCaskServerCodegen.ParamPart> pathParts = new ArrayList<>();
|
||||
final List<String> parts = Arrays.stream(op.path.substring(pathPrefix.length()).split("/", -1)).filter(p -> !p.isEmpty()).collect(Collectors.toList());
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
String p = parts.get(i);
|
||||
ScalaCaskServerCodegen.ParamPart pp = hasBrackets(p) ? new ScalaCaskServerCodegen.ParamPart(chompBrackets(p), pathParamForName(op, chompBrackets(p))) : new ScalaCaskServerCodegen.ParamPart(p, null);
|
||||
pathParts.add(pp);
|
||||
}
|
||||
|
||||
List<ScalaCaskServerCodegen.ParamPart> paramPathParts = pathParts.stream().filter(p -> p.isParam).collect(Collectors.toList());
|
||||
if (!paramPathParts.isEmpty()) {
|
||||
final String lastParamName = paramPathParts.get(paramPathParts.size() - 1).name;
|
||||
paramPathParts.forEach(p -> p.hasMoreParams = !p.name.equals(lastParamName));
|
||||
}
|
||||
if (!pathParts.isEmpty()) {
|
||||
pathParts.get(pathParts.size() - 1).hasMore = false;
|
||||
}
|
||||
|
||||
op.vendorExtensions.put("x-path-remaining", pathParts);
|
||||
op.vendorExtensions.put("x-has-path-remaining", !paramPathParts.isEmpty());
|
||||
operations.add(op);
|
||||
}
|
||||
|
||||
public boolean contains(CodegenOperation op) {
|
||||
return operations.contains(op);
|
||||
}
|
||||
|
||||
public void updateAnnotations() {
|
||||
operations.forEach(op -> {
|
||||
String annotation = op.vendorExtensions.get("x-annotation").toString();
|
||||
String conflicts = String.join(", ", operations.stream().map(o -> o.path).collect(Collectors.toList()));
|
||||
op.vendorExtensions.put("x-annotation", "// conflicts with [" + conflicts + "] after" + pathPrefix + ", ignoring " + annotation);
|
||||
});
|
||||
operations = operations.stream().sorted((a, b) -> a.pathParams.size() - b.pathParams.size()).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static List<ScalaCaskServerCodegen.OperationGroup> group(List<CodegenOperation> operationList) {
|
||||
Map<String, ScalaCaskServerCodegen.OperationGroup> groupedByPrefix = new HashMap<>();
|
||||
operationList.forEach(op -> {
|
||||
String prefix = nonParamPathPrefix(op);
|
||||
String key = op.httpMethod + " " + prefix;
|
||||
if (!op.pathParams.isEmpty()) {
|
||||
final ScalaCaskServerCodegen.OperationGroup group = groupedByPrefix.getOrDefault(key, new ScalaCaskServerCodegen.OperationGroup(op.httpMethod, prefix));
|
||||
group.add(op);
|
||||
groupedByPrefix.put(key, group);
|
||||
}
|
||||
});
|
||||
return groupedByPrefix.values().stream().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
static String nonParamPathPrefix(CodegenOperation op) {
|
||||
if (op.pathParams.isEmpty()) {
|
||||
return op.path;
|
||||
}
|
||||
|
||||
final String firstParam = op.pathParams.stream().findFirst().get().paramName;
|
||||
final int i = op.path.indexOf(firstParam);
|
||||
final String path = chompSuffix(op.path.substring(0, i - 1), "/");
|
||||
return path;
|
||||
}
|
||||
|
||||
static List<ScalaCaskServerCodegen.OperationGroup> createRouteGroups(List<CodegenOperation> operationList) {
|
||||
|
||||
List<ScalaCaskServerCodegen.OperationGroup> groups = group(operationList);
|
||||
operationList.forEach((op) -> {
|
||||
for (final ScalaCaskServerCodegen.OperationGroup group : groups) {
|
||||
// for the usage/call site
|
||||
final String scalaPath = pathWithBracketPlaceholdersRemovedAndXPathIndexAdded(op);
|
||||
op.vendorExtensions.put("x-cask-path", scalaPath);
|
||||
|
||||
final String annotation = "@cask." + op.httpMethod.toLowerCase(Locale.ROOT);
|
||||
op.vendorExtensions.put("x-annotation", annotation);
|
||||
if (!group.contains(op)) {
|
||||
if (op.path.startsWith(group.pathPrefix) && op.httpMethod.equalsIgnoreCase(group.httpMethod)) {
|
||||
group.add(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
List<ScalaCaskServerCodegen.OperationGroup> trimmed = groups.stream().filter(g -> g.operations.size() > 1).map(g -> {
|
||||
g.updateAnnotations();
|
||||
return g;
|
||||
}).collect(Collectors.toList());
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
|
||||
final Map<String, Object> operations = (Map<String, Object>) objs.get("operations");
|
||||
final List<CodegenOperation> operationList = (List<CodegenOperation>) operations.get("operation");
|
||||
|
||||
objs.put("route-groups", createRouteGroups(operationList));
|
||||
|
||||
operationList.forEach(ScalaCaskServerCodegen::postProcessOperation);
|
||||
return objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelsMap postProcessModels(ModelsMap objs) {
|
||||
objs.getModels().stream().map(ModelMap::getModel).forEach(this::postProcessModel);
|
||||
return objs;
|
||||
}
|
||||
|
||||
private void setDefaultValueForCodegenProperty(CodegenProperty p) {
|
||||
|
||||
if (p.defaultValue == null || p.defaultValue.trim().isEmpty()) {
|
||||
if (p.getIsEnumOrRef()) {
|
||||
p.defaultValue = "null";
|
||||
} else {
|
||||
p.defaultValue = defaultValueNonOption(p);
|
||||
}
|
||||
} else if (p.defaultValue.contains("Seq.empty")) {
|
||||
p.defaultValue = "Nil";
|
||||
}
|
||||
}
|
||||
|
||||
private void postProcessModel(CodegenModel model) {
|
||||
model.getAllVars().forEach(this::setDefaultValueForCodegenProperty);
|
||||
model.getVars().forEach(this::setDefaultValueForCodegenProperty);
|
||||
|
||||
model.getVars().forEach(ScalaCaskServerCodegen::postProcessProperty);
|
||||
model.getAllVars().forEach(ScalaCaskServerCodegen::postProcessProperty);
|
||||
}
|
||||
|
||||
private static void postProcessOperation(CodegenOperation op) {
|
||||
// force http method to lower case
|
||||
op.httpMethod = op.httpMethod.toLowerCase(Locale.ROOT);
|
||||
|
||||
/* Put in 'x-consumes-json' and 'x-consumes-xml' */
|
||||
op.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json"));
|
||||
op.vendorExtensions.put("x-consumes-xml", consumesMimetype(op, "application/xml"));
|
||||
|
||||
op.bodyParams.stream().filter((b) -> b.isBodyParam).forEach((p) -> {
|
||||
p.vendorExtensions.put("x-consumes-json", consumesMimetype(op, "application/json"));
|
||||
p.vendorExtensions.put("x-consumes-xml", consumesMimetype(op, "application/xml"));
|
||||
});
|
||||
|
||||
/* put in 'x-container-type' to help with unmarshalling from json */
|
||||
op.allParams.forEach((p) -> p.vendorExtensions.put("x-container-type", containerType(p.dataType)));
|
||||
op.bodyParams.forEach((p) -> p.vendorExtensions.put("x-container-type", containerType(p.dataType)));
|
||||
|
||||
final String paramList = op.allParams.stream().map((p) -> p.paramName).collect(Collectors.joining(", "));
|
||||
op.vendorExtensions.put("x-param-list", paramList);
|
||||
|
||||
final Stream<String> typed = op.allParams.stream().map((p) -> p.paramName + " : " + asScalaDataType(p, p.required, false));
|
||||
op.vendorExtensions.put("x-param-list-typed", String.join(", ", typed.collect(Collectors.toList())));
|
||||
|
||||
final Stream<String> typedJson = op.allParams.stream().map((p) -> p.paramName + " : " + asScalaDataType(p, p.required, true));
|
||||
op.vendorExtensions.put("x-param-list-typed-json", String.join(", ", typedJson.collect(Collectors.toList())));
|
||||
|
||||
// for the declaration site
|
||||
op.vendorExtensions.put("x-cask-path-typed", routeArgs(op));
|
||||
op.vendorExtensions.put("x-query-args", queryArgs(op));
|
||||
|
||||
List<String> responses = op.responses.stream().map(r -> r.dataType).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
op.vendorExtensions.put("x-response-type", responses.isEmpty() ? "Unit" : String.join(" | ", responses));
|
||||
}
|
||||
|
||||
private static void postProcessProperty(CodegenProperty p) {
|
||||
p.vendorExtensions.put("x-datatype-model", asScalaDataType(p, p.required, false));
|
||||
p.vendorExtensions.put("x-defaultValue-model", defaultValue(p, p.required, p.defaultValue));
|
||||
String dataTypeData = asScalaDataType(p, p.required, true);
|
||||
p.vendorExtensions.put("x-datatype-data", dataTypeData);
|
||||
|
||||
|
||||
p.vendorExtensions.put("x-containertype-data", containerType(dataTypeData));
|
||||
|
||||
p.vendorExtensions.put("x-defaultValue-data", defaultValueNonOption(p, p.defaultValue));
|
||||
|
||||
// the 'asModel' logic for modelData.mustache
|
||||
//
|
||||
// if it's optional (not required), then wrap the value in Option()
|
||||
// ... unless it's a map or array, in which case it can just be empty
|
||||
//
|
||||
p.vendorExtensions.put("x-wrap-in-optional", !p.required && !p.isArray && !p.isMap);
|
||||
|
||||
// if it's an array or optional, we need to map it as a model -- unless it's a map,
|
||||
// in which case we have to map the values
|
||||
boolean hasItemModel = p.items != null && p.items.isModel;
|
||||
boolean isObjectArray = p.isArray && hasItemModel;
|
||||
boolean isOptionalObj = !p.required && p.isModel;
|
||||
p.vendorExtensions.put("x-map-asModel", (isOptionalObj || isObjectArray) && !p.isMap);
|
||||
|
||||
// when deserialising map objects, the logic is tricky.
|
||||
p.vendorExtensions.put("x-deserialize-asModelMap", p.isMap && hasItemModel);
|
||||
|
||||
// for some reason, an openapi spec with pattern field like this:
|
||||
// pattern: '^[A-Za-z]+$'
|
||||
// will result in the pattern property text of
|
||||
// pattern: '/^[A-Za-z]+$/'
|
||||
if (p.pattern != null && p.pattern.startsWith("/") && p.pattern.endsWith("/")) {
|
||||
p.pattern = p.pattern.substring(1, p.pattern.length() - 1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cask path params use the :pathParam syntax rather than the {pathParam} syntax
|
||||
*
|
||||
* @param op
|
||||
* @return
|
||||
*/
|
||||
private static String pathWithBracketPlaceholdersRemovedAndXPathIndexAdded(CodegenOperation op) {
|
||||
String[] items = op.path.split("/", -1);
|
||||
String scalaPath = "";
|
||||
for (int i = 0; i < items.length; ++i) {
|
||||
final String nextPart = hasBrackets(items[i]) ? ":" + chompBrackets(items[i]) : items[i];
|
||||
if (i != items.length - 1) {
|
||||
scalaPath = scalaPath + nextPart + "/";
|
||||
} else {
|
||||
scalaPath = scalaPath + nextPart;
|
||||
}
|
||||
}
|
||||
return scalaPath;
|
||||
}
|
||||
|
||||
private static CodegenParameter pathParamForName(CodegenOperation op, String pathParam) {
|
||||
final CodegenParameter param = op.pathParams.stream().filter(p -> p.paramName.equals(pathParam)).findFirst().get();
|
||||
if (param == null) {
|
||||
throw new RuntimeException("Bug: path param " + pathParam + " not found");
|
||||
}
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* The path placeholders as well as query parameters
|
||||
*
|
||||
* @param op the codegen operations
|
||||
* @return a list of both the path and query parameters as typed arguments (e.g. "aPathArg : Int, request: cask.Request, aQueryArg : Option[Long]")
|
||||
*/
|
||||
private static String routeArgs(CodegenOperation op) {
|
||||
final Stream<String> pathParamNames = Arrays.stream(op.path.split("/", -1)).filter(ScalaCaskServerCodegen::hasBrackets).map(p -> {
|
||||
final CodegenParameter param = pathParamForName(op, chompBrackets(p));
|
||||
return param.paramName + " : " + asScalaDataType(param, param.required, true);
|
||||
});
|
||||
|
||||
|
||||
final List<String> pathList = pathParamNames.collect(Collectors.toList());
|
||||
|
||||
// we always include the cask request
|
||||
pathList.add("request: cask.Request");
|
||||
|
||||
final Stream<String> queryParams = op.queryParams.stream().map(p -> {
|
||||
p.vendorExtensions.put("x-default-value", defaultValue(p));
|
||||
return p.paramName + " : " + asScalaDataType(p, p.required, true, true);
|
||||
});
|
||||
pathList.addAll(queryParams.collect(Collectors.toList()));
|
||||
return pathList.isEmpty() ? "" : (String.join(", ", pathList));
|
||||
}
|
||||
|
||||
private static String defaultValue(CodegenParameter p) {
|
||||
return defaultValue(p, p.required, p.defaultValue);
|
||||
}
|
||||
|
||||
private static String defaultValue(IJsonSchemaValidationProperties p, boolean required, String fallbackDefaultValue) {
|
||||
if (!required && !(p.getIsArray() || p.getIsMap())) {
|
||||
return "None";
|
||||
}
|
||||
return defaultValueNonOption(p, fallbackDefaultValue);
|
||||
}
|
||||
|
||||
private static String defaultValueNonOption(IJsonSchemaValidationProperties p, String fallbackDefaultValue) {
|
||||
if (p.getIsArray()) {
|
||||
if (p.getUniqueItems()) {
|
||||
return "Set.empty";
|
||||
}
|
||||
return "Nil";
|
||||
}
|
||||
if (p.getIsMap()) {
|
||||
return "Map.empty";
|
||||
}
|
||||
if (p.getIsNumber()) {
|
||||
return "0";
|
||||
}
|
||||
if (p.getIsEnum()) {
|
||||
return fallbackDefaultValue;
|
||||
}
|
||||
if (p.getIsBoolean()) {
|
||||
return "false";
|
||||
}
|
||||
if (p.getIsUuid()) {
|
||||
return "java.util.UUID.randomUUID()";
|
||||
}
|
||||
if (p.getIsString()) {
|
||||
return "\"\"";
|
||||
}
|
||||
return fallbackDefaultValue;
|
||||
}
|
||||
|
||||
private static String defaultValueNonOption(CodegenProperty p) {
|
||||
if (p.getIsArray()) {
|
||||
return "Nil";
|
||||
}
|
||||
if (p.getIsMap()) {
|
||||
return "Map.empty";
|
||||
}
|
||||
if (p.isNumber || p.isNumeric) {
|
||||
return "0";
|
||||
}
|
||||
if (p.isBoolean) {
|
||||
return "false";
|
||||
}
|
||||
if (p.isUuid) {
|
||||
return "java.util.UUID.randomUUID()";
|
||||
}
|
||||
if (p.isModel) {
|
||||
return "null";
|
||||
}
|
||||
if (p.isDate || p.isDateTime) {
|
||||
return "null";
|
||||
}
|
||||
if (p.isString) {
|
||||
return "\"\"";
|
||||
}
|
||||
return p.defaultValue;
|
||||
}
|
||||
|
||||
private static String queryArgs(final CodegenOperation op) {
|
||||
final List<String> list = op.queryParams.stream().map(p -> p.paramName).collect(Collectors.toList());
|
||||
final String prefix = list.isEmpty() ? "" : ", ";
|
||||
return prefix + String.join(", ", list);
|
||||
}
|
||||
|
||||
/**
|
||||
* For our model classes, we have two variants:
|
||||
* <p>
|
||||
* 1) a {model}.scala one which is a validated, model class
|
||||
* 2) a {model}Data.scala one which is just our data-transfer-object (DTO) which is written primarily for e.g. json serialisation
|
||||
* <p>
|
||||
* The data variant can have nulls and other non-scala things, but they know how to create validated model objects.
|
||||
* <p>
|
||||
* This 'asScalaDataType' is used to ensure the type hierarchy is correct for both the model and data varients.
|
||||
* <p>
|
||||
* e.g. consider this example:
|
||||
* ```
|
||||
* case class Foo(bar : Bar, bazz :List[Bazz])
|
||||
* case class Bar(x : Option[String] = None)
|
||||
* case class Bazz(y : Int)
|
||||
* <p>
|
||||
* // vs
|
||||
* <p>
|
||||
* case class FooData(bar : BarData, bazz :List[BazzData])
|
||||
* case class BarData(x : String = "")
|
||||
* case class BazzData(y : Int)
|
||||
* ```
|
||||
*/
|
||||
private static String asScalaDataType(final IJsonSchemaValidationProperties param, boolean required, boolean useJason) {
|
||||
return asScalaDataType(param, required, useJason, !useJason);
|
||||
}
|
||||
|
||||
private static String asScalaDataType(final IJsonSchemaValidationProperties param, boolean required, boolean useJason, boolean allowOptional) {
|
||||
String dataType = (param.getIsModel() && useJason) ? param.getDataType() + "Data" : param.getDataType();
|
||||
|
||||
final String dataSuffix = useJason && param.getItems() != null && param.getItems().getIsModel() ? "Data" : "";
|
||||
if (dataType.startsWith("List[")) {
|
||||
dataType = dataType.replace("List[", "Seq[");
|
||||
dataType = dataType.replace("]", dataSuffix + "]");
|
||||
} else if (dataType.startsWith("Set[")) {
|
||||
dataType = dataType.replace("]", dataSuffix + "]");
|
||||
} else if (!required && allowOptional) {
|
||||
dataType = "Option[" + dataType + "]";
|
||||
}
|
||||
return dataType;
|
||||
}
|
||||
|
||||
private static String chompBrackets(String str) {
|
||||
return str.replace("{", "").replace("}", "");
|
||||
}
|
||||
|
||||
private static String chompSuffix(String str, String suffix) {
|
||||
return str.endsWith(suffix) ? chompSuffix(str.substring(0, str.length() - suffix.length()), suffix) : str;
|
||||
}
|
||||
|
||||
private static boolean hasBrackets(String str) {
|
||||
return str.matches("^\\{(.*)\\}$");
|
||||
}
|
||||
|
||||
static String containerType(String dataType) {
|
||||
String fixedForList = dataType.replaceAll(".*\\[(.*)\\]", "$1");
|
||||
|
||||
// if it is a map, we want the value type
|
||||
final String[] parts = fixedForList.split(",");
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
}
|
@ -115,6 +115,7 @@ org.openapitools.codegen.languages.RustClientCodegen
|
||||
org.openapitools.codegen.languages.RustServerCodegen
|
||||
org.openapitools.codegen.languages.ScalatraServerCodegen
|
||||
org.openapitools.codegen.languages.ScalaAkkaClientCodegen
|
||||
org.openapitools.codegen.languages.ScalaCaskServerCodegen
|
||||
org.openapitools.codegen.languages.ScalaPekkoClientCodegen
|
||||
org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen
|
||||
org.openapitools.codegen.languages.ScalaFinchServerCodegen
|
||||
|
4
modules/openapi-generator/src/main/resources/scala-cask/.scalafmt.conf.mustache
vendored
Normal file
4
modules/openapi-generator/src/main/resources/scala-cask/.scalafmt.conf.mustache
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
version = 3.6.1
|
||||
align.preset = more // For pretty alignment.
|
||||
maxColumn = 100
|
||||
runner.dialect = scala3
|
13
modules/openapi-generator/src/main/resources/scala-cask/Dockerfile.mustache
vendored
Normal file
13
modules/openapi-generator/src/main/resources/scala-cask/Dockerfile.mustache
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
FROM virtuslab/scala-cli:latest as build
|
||||
WORKDIR /app
|
||||
COPY ./Server.scala /app/
|
||||
# note: this assumes a published server stub jar.
|
||||
# If you've published this locally, you would need to copy those into this image,
|
||||
# perhaps by using coursier fetch
|
||||
RUN scala-cli --power package /app/Server.scala --assembly -o app.jar
|
||||
|
||||
# The main image
|
||||
FROM openjdk:23-slim
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/app.jar /app/
|
||||
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
96
modules/openapi-generator/src/main/resources/scala-cask/README.mustache
vendored
Normal file
96
modules/openapi-generator/src/main/resources/scala-cask/README.mustache
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
# REST Service
|
||||
|
||||
This project contains the data models and REST services, generated from the [openapi-generator](https://github.com/OpenAPITools/openapi-generator) project.
|
||||
|
||||
The server implementation is based on Li's excellent [cask](https://com-lihaoyi.github.io/cask/) library.
|
||||
|
||||
# How to use this code
|
||||
|
||||
This code was designed so that it can be packaged up and semantically-versioned alongside your open-api schema.
|
||||
|
||||
That approach supports having separate "contract" repositories for microservice projects, where the pipeline for the
|
||||
contract repo might produce versioned jar artefacts which can then easily be brought in / referenced by a separate service project which simply
|
||||
implements the business logic.
|
||||
|
||||
You can read more about this approach [here](https://github.com/kindservices/contract-first-rest)
|
||||
|
||||
# How to implement your business logic
|
||||
There are a few options for using this code/applying your business logic for the services.
|
||||
|
||||
## Option 1 (preferred): Package and publish this boilerplate
|
||||
Typically, OpenApi templates are written to generate code which co-exists alongside the handwritten business logic.
|
||||
|
||||
While that works, it's also not ideal:
|
||||
* You have to ensure the generated code isn't checked in
|
||||
* Team members, build pipelines, etc all have to regenerate and recompile the same boilerplate code over and over
|
||||
* People can encounter IDE issues with generated code
|
||||
|
||||
Instead, you have the option of simply packaging/publishing this generated code, and then allowing service implementations
|
||||
to simply bring in the published code as a dependency.
|
||||
|
||||
The steps to do that are:
|
||||
|
||||
### Build/Publish
|
||||
This project is built using [sbt](https://www.scala-sbt.org/download/), so you can run `sbt publish` (or `sbt publishLocal`)
|
||||
|
||||
Or, for a zero-install docker build:
|
||||
```bash
|
||||
docker run -it --rm -v $(pwd):/app -w /app sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 sbt publishLocal
|
||||
```
|
||||
|
||||
### Create a new separate implementation project
|
||||
Once published, you can create your server implementation in a new, clean, separate project based on [the example](./example)
|
||||
|
||||
This means all the boilerplate endpoint and model code is brought in as "just another jar", and you're free to
|
||||
create a greenfield project in whatever language (scala, java, kotlin) and build system of your choosing.
|
||||
|
||||
We show a simple, minimalistic example of a starting point in [the example project](./example)
|
||||
|
||||
## Option 2: Extend this generated example
|
||||
You can configure this project (for instance, setting up your own .gitignore rules and scripts) to leave the generated code as-is
|
||||
and provide your implementation alongside the generated code.
|
||||
|
||||
The place to start is by providing your own implementation of the Services defined in the `api` package -
|
||||
perhaps by creating your 'MyService.scala' code in a new `impl` package.
|
||||
|
||||
You then have several options for how to wire those in:
|
||||
|
||||
1) Create a new BaseApp instance to create your own Main entry point
|
||||
Follow the pattern in App.scala, but by passing your own implementations to BaseApp,
|
||||
ensuring you call `start` to start the server
|
||||
|
||||
```bash
|
||||
@main def run() = BaseApp(/* your services here/*).start()
|
||||
```
|
||||
|
||||
2) Extend either BaseApp class or mix in the AppRoutes trait
|
||||
You can create your own main entry point with further control of the main cask app by extending
|
||||
the BaseApp or otherwise creating your own CaskApp which mixes in the AppRoutes
|
||||
|
||||
```bash
|
||||
object MyApp extends BaseApp(/* your services here/*) {
|
||||
// any overrides, new routes, etc here
|
||||
start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Customising the generated code
|
||||
|
||||
A typical config.yml used to alter the generated code may look like this:
|
||||
```
|
||||
groupId: "ex.amp.le"
|
||||
artifactId: "pets-test"
|
||||
apiPackage: "ex.ample.api"
|
||||
modelPackage: "ex.ample.model"
|
||||
```
|
||||
|
||||
Which you would then pass to the generator like this:
|
||||
```
|
||||
docker run --rm \
|
||||
-v ${PWD}:/local openapitools/openapi-generator-cli generate \
|
||||
-i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml \
|
||||
-g scala-cask \
|
||||
-c /local/config.yml \
|
||||
-o /local/path/to/output_dir
|
||||
```
|
0
modules/openapi-generator/src/main/resources/scala-cask/api.mustache
vendored
Normal file
0
modules/openapi-generator/src/main/resources/scala-cask/api.mustache
vendored
Normal file
155
modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache
vendored
Normal file
155
modules/openapi-generator/src/main/resources/scala-cask/apiPackage.mustache
vendored
Normal file
@ -0,0 +1,155 @@
|
||||
{{>licenseInfo}}
|
||||
package {{apiPackage}}
|
||||
|
||||
|
||||
import cask.FormEntry
|
||||
import io.undertow.server.handlers.form.{FormData, FormParserFactory}
|
||||
|
||||
import java.io.File
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.*
|
||||
|
||||
// needed for BigDecimal params
|
||||
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
|
||||
|
||||
// a parsed value from an HTTP request
|
||||
opaque type Parsed[A] = Either[String, A]
|
||||
|
||||
object Parsed {
|
||||
def apply[A](value: A): Parsed[A] = Right(value)
|
||||
|
||||
def eval[A](value: => A): Parsed[A] = Try(value) match {
|
||||
case Failure(exp) => Left(s"Error: ${exp.getMessage}")
|
||||
case Success(ok) => Right(ok)
|
||||
}
|
||||
|
||||
def fromTry[A](value : Try[A]) = value match {
|
||||
case Failure(err) => Left(err.getMessage)
|
||||
case Success(ok) => Right(ok)
|
||||
}
|
||||
|
||||
def fail[A](msg: String): Parsed[A] = Left(msg)
|
||||
|
||||
def optionalValue(map: Map[String, collection.Seq[String]], key: String): Parsed[Option[String]] = {
|
||||
map.get(key) match {
|
||||
case Some(Seq(only: String)) => Parsed(Option(only))
|
||||
case Some(Seq()) => Parsed(None)
|
||||
case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
|
||||
case None => Parsed(None)
|
||||
}
|
||||
}
|
||||
|
||||
def singleValue(map: Map[String, collection.Seq[String]], key : String): Parsed[String] = {
|
||||
map.get(key) match {
|
||||
case Some(Seq(only : String)) => Parsed(only)
|
||||
case Some(Seq()) => Parsed("")
|
||||
case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
|
||||
case None => Parsed.fail(s"required '$key' was not set")
|
||||
}
|
||||
}
|
||||
|
||||
def manyValues(map: Map[String, collection.Seq[String]], key : String, required: Boolean): Parsed[List[String]] = {
|
||||
map.get(key) match {
|
||||
case Some(many) => Parsed(many.toList)
|
||||
case None if required => Parsed.fail(s"required '$key' was not set")
|
||||
case None => Parsed(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension[A] (parsed: Parsed[A]) {
|
||||
def toEither: Either[String, A] = parsed
|
||||
|
||||
def asLong(using ev : A =:= String): Parsed[Long] = as[Long](_.toLongOption)
|
||||
def asBoolean(using ev : A =:= String): Parsed[Boolean] = as[Boolean](_.toBooleanOption)
|
||||
def asInt(using ev : A =:= String): Parsed[Int] = as[Int](_.toIntOption)
|
||||
def asByte(using ev : A =:= String): Parsed[Byte] = as[Byte](_.toByteOption)
|
||||
def asUuid(using ev : A =:= String): Parsed[UUID] = as[UUID](x => Try(UUID.fromString(x)).toOption)
|
||||
def asFloat(using ev : A =:= String): Parsed[Float] = as[Float](_.toFloatOption)
|
||||
def asDouble(using ev : A =:= String): Parsed[Double] = as[Double](_.toDoubleOption)
|
||||
def asDate(using ev: A =:= String): Parsed[LocalDate] = as[LocalDate](x => Try(LocalDate.parse(x)).toOption)
|
||||
|
||||
private def as[B : ClassTag](f : String => Option[B])(using ev : A =:= String): Parsed[B] = parsed.flatMap { str =>
|
||||
f(ev(str)) match {
|
||||
case None => Parsed.fail(s"'$str' cannot be parsed as a ${implicitly[ClassTag[B]].runtimeClass}")
|
||||
case Some(x) => Parsed(x)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mapError(f : String => String) : Parsed[A] = parsed match {
|
||||
case Left(msg) => Left(f(msg))
|
||||
case right => right
|
||||
}
|
||||
|
||||
def map[B](f: A => B): Parsed[B] = parsed match {
|
||||
case Right(value) => Right(f(value))
|
||||
case Left(err) => Left(err)
|
||||
}
|
||||
def flatMap[B](f : A => Parsed[B]): Parsed[B] = parsed match {
|
||||
case Right(value) => f(value)
|
||||
case Left(err) => Left(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension (request: cask.Request) {
|
||||
|
||||
def formSingleValueRequired(paramName: String): Parsed[String] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.singleValue(Map(paramName -> data), paramName)
|
||||
}
|
||||
def formSingleValueOptional(paramName: String): Parsed[Option[String]] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.optionalValue(Map(paramName -> data), paramName)
|
||||
}
|
||||
|
||||
def formValueAsFileOptional(paramName: String): Parsed[Option[File]] = {
|
||||
val data = formDataForKey(paramName)
|
||||
data.map(_.getFileItem.getFile.toFile).toSeq match {
|
||||
case Seq() => Parsed(None)
|
||||
case Seq(file) => Parsed(Option(file))
|
||||
case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
|
||||
}
|
||||
}
|
||||
|
||||
def formValueAsFileRequired(paramName: String): Parsed[File] = {
|
||||
val data = formDataForKey(paramName)
|
||||
data.map(_.getFileItem.getFile.toFile).toSeq match {
|
||||
case Seq() => Parsed.fail(s"No file form data was submitted for '$paramName'. The submitted form keys were: ${formDataKeys.mkString(",")}")
|
||||
case Seq(file) => Parsed(file)
|
||||
case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
|
||||
}
|
||||
}
|
||||
|
||||
def formManyValues(paramName: String, required: Boolean): Parsed[List[String]] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.manyValues(Map(paramName -> data), paramName, required)
|
||||
}
|
||||
|
||||
def formData: FormData = FormParserFactory.builder().build().createParser(request.exchange).parseBlocking()
|
||||
|
||||
def formDataKeys: Iterator[String] = formData.iterator().asScala
|
||||
|
||||
def formDataForKey(paramName: String): Iterable[FormData.FormValue] = formData.get(paramName).asScala
|
||||
|
||||
def headerSingleValueOptional(paramName: String): Parsed[Option[String]] = Parsed.optionalValue(request.headers, paramName)
|
||||
def headerSingleValueRequired(paramName: String): Parsed[String] = Parsed.singleValue(request.headers, paramName)
|
||||
|
||||
def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required)
|
||||
|
||||
def bodyAsString = new String(request.readAllBytes(), "UTF-8")
|
||||
|
||||
def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
|
||||
request
|
||||
.remainingPathSegments
|
||||
.lift(index) match {
|
||||
case Some(value) => Right(value)
|
||||
case None if required => Left(s"'$paramName'' is a required path parameter at path position $index")
|
||||
case None => Right("")
|
||||
}
|
||||
}
|
||||
}
|
65
modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache
vendored
Normal file
65
modules/openapi-generator/src/main/resources/scala-cask/apiRoutes.mustache
vendored
Normal file
@ -0,0 +1,65 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
{{>licenseInfo}}
|
||||
|
||||
// this is generated from apiRoutes.mustache
|
||||
package {{apiPackage}}
|
||||
|
||||
import {{modelPackage}}.*
|
||||
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
|
||||
class {{classname}}Routes(service : {{classname}}Service) extends cask.Routes {
|
||||
|
||||
{{#route-groups}}
|
||||
// route group for {{methodName}}
|
||||
{{caskAnnotation}}("{{pathPrefix}}", true)
|
||||
def {{methodName}}(request: cask.Request{{>apiRoutesQueryParamsTyped}}) = {
|
||||
request.remainingPathSegments match {
|
||||
{{#operations}}
|
||||
case Seq({{>pathExtractor}}) => {{operationId}}({{>pathExtractorParams}}request{{>queryParams}})
|
||||
{{/operations}}
|
||||
case _ => cask.Response("Not Found", statusCode = 404)
|
||||
}
|
||||
}
|
||||
{{/route-groups}}
|
||||
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
/** {{summary}}
|
||||
* {{description}}
|
||||
*/
|
||||
{{vendorExtensions.x-annotation}}("{{vendorExtensions.x-cask-path}}")
|
||||
def {{operationId}}({{vendorExtensions.x-cask-path-typed}}) = {
|
||||
{{#authMethods}}
|
||||
// auth method {{name}} : {{type}}, keyParamName: {{keyParamName}}
|
||||
{{/authMethods}}
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = {{>parseHttpParams}}
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
{{#vendorExtensions.x-has-response-types}}
|
||||
{{#responses}}
|
||||
{{#dataType}}
|
||||
case Right(value : {{dataType}}) => cask.Response(data = write(value), {{code}}, headers = Seq("Content-Type" -> "application/json"))
|
||||
{{/dataType}}
|
||||
{{/responses}}
|
||||
{{/vendorExtensions.x-has-response-types}}
|
||||
{{^vendorExtensions.x-has-response-types}}
|
||||
case Right(_) => cask.Response("", 200)
|
||||
{{/vendorExtensions.x-has-response-types}}
|
||||
}
|
||||
}
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
|
||||
initialize()
|
||||
}
|
1
modules/openapi-generator/src/main/resources/scala-cask/apiRoutesQueryParamsTyped.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/scala-cask/apiRoutesQueryParamsTyped.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#hasGroupQueryParams}},{{/hasGroupQueryParams}}{{#groupQueryParams}}{{paramName}} : {{dataType}} = {{defaultValue}}{{^-last}},{{/-last}}{{/groupQueryParams}}
|
37
modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache
vendored
Normal file
37
modules/openapi-generator/src/main/resources/scala-cask/apiService.mustache
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
{{>licenseInfo}}
|
||||
|
||||
// generated from apiService.mustache
|
||||
package {{apiPackage}}
|
||||
|
||||
{{#imports}}import _root_.{{import}}
|
||||
{{/imports}}
|
||||
|
||||
import _root_.{{modelPackage}}.*
|
||||
|
||||
object {{classname}}Service {
|
||||
def apply() : {{classname}}Service = new {{classname}}Service {
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
override def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}} = ???
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The {{classname}} business-logic
|
||||
*/
|
||||
trait {{classname}}Service {
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
/** {{{summary}}}
|
||||
* {{{description}}}
|
||||
* @return {{returnType}}
|
||||
*/
|
||||
def {{operationId}}({{vendorExtensions.x-param-list-typed}}) : {{vendorExtensions.x-response-type}}
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
}
|
12
modules/openapi-generator/src/main/resources/scala-cask/appPackage.mustache
vendored
Normal file
12
modules/openapi-generator/src/main/resources/scala-cask/appPackage.mustache
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{{>licenseInfo}}
|
||||
package {{packageName}}
|
||||
|
||||
def box(str: String): String = {
|
||||
val lines = str.linesIterator.toList
|
||||
val maxLen = (0 +: lines.map(_.length)).max
|
||||
val boxed = lines.map { line =>
|
||||
s" | ${line.padTo(maxLen, ' ')} |"
|
||||
}
|
||||
val bar = " +-" + ("-" * maxLen) + "-+"
|
||||
(bar +: boxed :+ bar).mkString("\n")
|
||||
}
|
40
modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache
vendored
Normal file
40
modules/openapi-generator/src/main/resources/scala-cask/appRoutes.mustache
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
{{>licenseInfo}}
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package {{packageName}}
|
||||
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
import _root_.{{modelPackage}}.*
|
||||
import _root_.{{apiPackage}}.*
|
||||
|
||||
/**
|
||||
* This trait encapsulates the business logic (services) and the
|
||||
* http routes which handle the http requests sent to those services.
|
||||
*
|
||||
* There are default 'not implemented' implementations for the service.
|
||||
*
|
||||
* If you wanted fine-grained control over the routes and services, you could
|
||||
* extend the cask.MainRoutes and mix in this trait by using this:
|
||||
*
|
||||
* \{\{\{
|
||||
* override def allRoutes = appRoutes
|
||||
* \}\}\}
|
||||
*
|
||||
* More typically, however, you would extend the 'BaseApp' class
|
||||
*/
|
||||
trait AppRoutes {
|
||||
{{#operations}}
|
||||
def app{{classname}}Service : {{classname}}Service = {{classname}}Service()
|
||||
def routeFor{{classname}} : {{classname}}Routes = {{classname}}Routes(app{{classname}}Service)
|
||||
{{/operations}}
|
||||
|
||||
def appRoutes = Seq(
|
||||
{{#operations}}
|
||||
routeFor{{classname}} {{^-last}},{{/-last}}
|
||||
{{/operations}}
|
||||
)
|
||||
}
|
49
modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache
vendored
Normal file
49
modules/openapi-generator/src/main/resources/scala-cask/baseApp.mustache
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
{{>licenseInfo}}
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package {{packageName}}
|
||||
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
import _root_.{{modelPackage}}.*
|
||||
import _root_.{{apiPackage}}.*
|
||||
|
||||
/**
|
||||
* This class was created with the intention of being extended by some runnable object,
|
||||
* passing in the custom business logic services
|
||||
*/
|
||||
class BaseApp({{#operations}}
|
||||
override val app{{classname}}Service : {{classname}}Service = {{classname}}Service(),
|
||||
{{/operations}}
|
||||
override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
|
||||
|
||||
/** routes for the UI
|
||||
* Subclasses can override to turn this off
|
||||
*/
|
||||
def openApiRoute: Option[cask.Routes] = Option(OpenApiRoutes(port))
|
||||
|
||||
override def allRoutes = appRoutes ++ openApiRoute
|
||||
|
||||
{{^operations}}
|
||||
// no operations!
|
||||
{{/operations}}
|
||||
|
||||
override def host: String = "0.0.0.0"
|
||||
|
||||
def start() = locally {
|
||||
initialize()
|
||||
println(box(s""" 🚀 browse to localhost:$port 🚀
|
||||
| host : $host
|
||||
| port : $port
|
||||
| verbose : $verbose
|
||||
| debugMode : $debugMode
|
||||
|""".stripMargin))
|
||||
|
||||
// if java.awt.Desktop.isDesktopSupported then {
|
||||
// java.awt.Desktop.getDesktop.browse(new java.net.URI(s"http://localhost:${port}"))
|
||||
// }
|
||||
}
|
||||
}
|
29
modules/openapi-generator/src/main/resources/scala-cask/build.sbt.mustache
vendored
Normal file
29
modules/openapi-generator/src/main/resources/scala-cask/build.sbt.mustache
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name := "{{artifactId}}"
|
||||
organization:="{{groupId}}"
|
||||
version := "0.0.1-SNAPSHOT"
|
||||
scalaVersion := "3.3.1"
|
||||
scalafmtOnCompile := true
|
||||
libraryDependencies ++= Seq(
|
||||
"com.lihaoyi" %% "cask" % "0.9.2" ,
|
||||
"com.lihaoyi" %% "upickle" % "3.2.0",
|
||||
"org.scalatest" %% "scalatest" % "3.2.18" % Test
|
||||
)
|
||||
|
||||
publishMavenStyle := true
|
||||
|
||||
val githubUser = "{{{gitUserId}}}"
|
||||
val githubRepo = "{{{gitRepoId}}}"
|
||||
publishTo := Some("GitHub Package Registry" at s"https://maven.pkg.github.com/$githubUser/$githubRepo")
|
||||
|
||||
sys.env.get("GITHUB_TOKEN") match {
|
||||
case Some(token) if !token.isEmpty =>
|
||||
credentials += Credentials(
|
||||
"GitHub Package Registry",
|
||||
"maven.pkg.github.com",
|
||||
githubUser,
|
||||
token
|
||||
)
|
||||
case _ =>
|
||||
println("\n\t\tGITHUB_TOKEN not set - assuming a local build\n\n")
|
||||
credentials ++= Nil
|
||||
}
|
43
modules/openapi-generator/src/main/resources/scala-cask/build.sc.mustache
vendored
Normal file
43
modules/openapi-generator/src/main/resources/scala-cask/build.sc.mustache
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
import mill._, scalalib._, scalafmt._, publish._
|
||||
|
||||
// Mill build file (see https://mill-build.com/mill/Intro_to_Mill.html).
|
||||
// run with:
|
||||
//
|
||||
// mill _.compile
|
||||
// mill _.reformat
|
||||
// mill _.publishLocal
|
||||
// mill _.test.test
|
||||
object {{artifactId}} extends SbtModule with ScalafmtModule with PublishModule {
|
||||
def scalaVersion = "3.3.1"
|
||||
|
||||
def pomSettings = PomSettings(
|
||||
description = "{{artifactId}}",
|
||||
organization = "{{groupId}}",
|
||||
url = "https://github.com/<username>/{{artifactId}}",
|
||||
licenses = Seq(License.MIT),
|
||||
versionControl = VersionControl.github("<username>", "{{artifactId}}"),
|
||||
developers = Seq(
|
||||
// Developer("<username>", "<your name>", "https://github.com/<you>")
|
||||
)
|
||||
)
|
||||
|
||||
def publishVersion: mill.T[String] = T("0.0.1-SNAPSHOT")
|
||||
|
||||
def ivyDeps = Agg(
|
||||
ivy"com.lihaoyi::cask:0.9.2" ,
|
||||
ivy"com.lihaoyi::upickle:3.2.0"
|
||||
)
|
||||
|
||||
override def sources = T.sources(millSourcePath / os.up / "src" / "main" / "scala")
|
||||
override def resources = T.sources(millSourcePath / os.up / "src" / "main" / "resources")
|
||||
|
||||
object test extends SbtModuleTests {
|
||||
def ivyDeps = Agg(
|
||||
ivy"org.scalactic::scalactic:3.2.18",
|
||||
ivy"org.scalatest::scalatest:3.2.18"
|
||||
)
|
||||
|
||||
def testFramework = "org.scalatest.tools.Framework"
|
||||
override def sources = T.sources(millSourcePath / os.up / "src" / "test" / "scala")
|
||||
}
|
||||
}
|
41
modules/openapi-generator/src/main/resources/scala-cask/bulidAndPublish.yml.mustache
vendored
Normal file
41
modules/openapi-generator/src/main/resources/scala-cask/bulidAndPublish.yml.mustache
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Scala CI with sbt
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Cache sbt dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.ivy2/cache
|
||||
~/.sbt
|
||||
~/.m2
|
||||
key: ${{{openbrackets}}} runner.os {{{closebrackets}}}-sbt-${{{openbrackets}}} hashFiles('**/*.sbt') {{{closebrackets}}}
|
||||
restore-keys: |
|
||||
${{{openbrackets}}} runner.os {{{closebrackets}}}-sbt-
|
||||
|
||||
- name: Build with sbt
|
||||
run: sbt clean compile
|
||||
|
||||
- name: Test with sbt
|
||||
run: sbt test
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: sbt publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{{openbrackets}}} secrets.GITHUB_TOKEN {{{closebrackets}}}
|
60
modules/openapi-generator/src/main/resources/scala-cask/example.mustache
vendored
Normal file
60
modules/openapi-generator/src/main/resources/scala-cask/example.mustache
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "{{groupId}}::{{artifactId}}:0.0.1-SNAPSHOT"
|
||||
//> using repositories https://maven.pkg.github.com/{{{gitUserId}}}/{{{gitRepoId}}}
|
||||
|
||||
|
||||
/**
|
||||
* This single file can contain the business logic for a REST service.
|
||||
* ====================================
|
||||
* == zero-install build with docker ==
|
||||
* ====================================
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* docker build . -t {{artifactId}}:latest
|
||||
* ```
|
||||
* ======================
|
||||
* == Building Locally ==
|
||||
* ======================
|
||||
* This project can be built using [[scala-clit][https://scala-cli.virtuslab.org]]
|
||||
*
|
||||
* To simply run the project
|
||||
* ```
|
||||
* scala-cli Server.scala
|
||||
* ```
|
||||
*
|
||||
* To create a runnable jar, run:
|
||||
* ```
|
||||
* scala-cli --power package Server.scala -o app-assembly --assembly
|
||||
* ```
|
||||
*
|
||||
* To produce a docker image (no need for the Dockerfile), run:
|
||||
* ```
|
||||
* scala-cli --power package --docker Server.scala --docker-image-repository app-docker
|
||||
* ```
|
||||
*
|
||||
* To generate an IDE project:
|
||||
* ```
|
||||
* scala-cli setup-ide . --scala 3.3
|
||||
* ```
|
||||
*/
|
||||
package app
|
||||
|
||||
import {{packageName}}.BaseApp
|
||||
import {{apiPackage}}.*
|
||||
import {{modelPackage}}.*
|
||||
|
||||
import java.io.File
|
||||
|
||||
// TODO - write your business logic for your services here (the defaults all return 'not implemented'):
|
||||
{{#operations}}
|
||||
val my{{classname}}Service : {{classname}}Service = {{classname}}Service() // <-- replace this with your implementation
|
||||
{{/operations}}
|
||||
|
||||
/** This is your main entry point for your REST service
|
||||
* It extends BaseApp which defines the business logic for your services
|
||||
*/
|
||||
object Server extends BaseApp({{#operations}}app{{classname}}Service = my{{classname}}Service{{^-last}},
|
||||
{{/-last}}{{/operations}}):
|
||||
start()
|
||||
|
21
modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache
vendored
Normal file
21
modules/openapi-generator/src/main/resources/scala-cask/exampleApp.mustache
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
{{>licenseInfo}}
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package {{packageName}}
|
||||
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
import _root_.{{modelPackage}}.*
|
||||
import _root_.{{apiPackage}}.*
|
||||
|
||||
/**
|
||||
* This is an example of how you might extends BaseApp for a runnable application.
|
||||
*
|
||||
* See the README.md for how to create your own app
|
||||
*/
|
||||
object ExampleApp extends BaseApp() {
|
||||
start()
|
||||
}
|
25
modules/openapi-generator/src/main/resources/scala-cask/gitignore.mustache
vendored
Normal file
25
modules/openapi-generator/src/main/resources/scala-cask/gitignore.mustache
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# scala specific
|
||||
*.class
|
||||
*.log
|
||||
|
||||
# sbt specific
|
||||
.cache
|
||||
.history
|
||||
.lib/
|
||||
dist/*
|
||||
target/
|
||||
lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Mill specific
|
||||
out
|
||||
|
||||
# IntelliJ specific
|
||||
.idea
|
||||
*.iml
|
16
modules/openapi-generator/src/main/resources/scala-cask/licenseInfo.mustache
vendored
Normal file
16
modules/openapi-generator/src/main/resources/scala-cask/licenseInfo.mustache
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* {{{appName}}}
|
||||
* {{{appDescription}}}
|
||||
*
|
||||
{{#version}}
|
||||
* OpenAPI spec version: {{{version}}}
|
||||
*
|
||||
{{/version}}
|
||||
{{#infoEmail}}
|
||||
* Contact: {{{infoEmail}}}
|
||||
*
|
||||
{{/infoEmail}}
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
60
modules/openapi-generator/src/main/resources/scala-cask/model.mustache
vendored
Normal file
60
modules/openapi-generator/src/main/resources/scala-cask/model.mustache
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
{{>licenseInfo}}
|
||||
// this model was generated using model.mustache
|
||||
package {{modelPackage}}
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
case class {{classname}}(
|
||||
{{#vars}}
|
||||
{{#description}}
|
||||
/* {{{description}}} */
|
||||
{{/description}}
|
||||
{{name}}: {{#isEnum}}{{^required}}Option[{{/required}}{{classname}}.{{datatypeWithEnum}}{{^required}}]{{/required}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-model}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-model}}} {{/required}}{{^-last}},{{/-last}}
|
||||
|
||||
{{/vars}}) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : {{classname}}Data = {
|
||||
{{classname}}Data(
|
||||
{{#vars}}
|
||||
{{name}} = {{name}}{{#vendorExtensions.x-map-asModel}}.map(_.asData){{/vendorExtensions.x-map-asModel}}{{#vendorExtensions.x-wrap-in-optional}}.getOrElse({{{defaultValue}}}){{/vendorExtensions.x-wrap-in-optional}}{{^-last}},{{/-last}}
|
||||
{{/vars}}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object {{classname}}{
|
||||
|
||||
given RW[{{classname}}] = {{classname}}Data.readWriter.bimap[{{classname}}](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
{{#vars}}
|
||||
case {{name}} extends Fields("{{name}}")
|
||||
{{/vars}}
|
||||
}
|
||||
|
||||
{{#vars}}
|
||||
{{#isEnum}}
|
||||
// baseName={{{baseName}}}
|
||||
// nameInCamelCase = {{{nameInCamelCase}}}
|
||||
enum {{datatypeWithEnum}} derives ReadWriter {
|
||||
{{#_enum}}
|
||||
case {{.}}
|
||||
{{/_enum}}
|
||||
}
|
||||
{{/isEnum}}
|
||||
{{/vars}}
|
||||
|
||||
}
|
||||
|
||||
{{/model}}
|
||||
{{/models}}
|
250
modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache
vendored
Normal file
250
modules/openapi-generator/src/main/resources/scala-cask/modelData.mustache
vendored
Normal file
@ -0,0 +1,250 @@
|
||||
{{>licenseInfo}}
|
||||
// this model was generated using modelData.mustache
|
||||
package {{modelPackage}}
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
/** {{classname}}Data a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class {{classname}}Data(
|
||||
{{#vars}}
|
||||
{{#description}}
|
||||
/* {{{description}}} */
|
||||
{{/description}}
|
||||
{{name}}: {{#isEnum}}{{classname}}.{{datatypeWithEnum}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.x-datatype-data}}}{{/isEnum}}{{^required}} = {{{vendorExtensions.x-defaultValue-data}}} {{/required}}{{^-last}},{{/-last}}
|
||||
|
||||
{{/vars}}) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
{{#vars}}
|
||||
// ==================
|
||||
// {{name}}
|
||||
{{#pattern}}
|
||||
// validate against pattern '{{{pattern}}}'
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val regex = """{{{pattern}}}"""
|
||||
if {{name}} == null || !regex.r.matches({{name}}) then
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' doesn't match pattern $regex")
|
||||
}
|
||||
{{/pattern}}
|
||||
|
||||
{{#minimum}}
|
||||
// validate against {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum {{minimum}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if !({{name}} >{{^exclusiveMinimum}}={{/exclusiveMinimum}} {{minimum}}) then
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMinimum}}exclusive {{/exclusiveMinimum}}minimum value {{minimum}}")
|
||||
}
|
||||
{{/minimum}}
|
||||
|
||||
{{#maximum}}
|
||||
// validate against {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum {{maximum}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if !({{name}} <{{^exclusiveMaximum}}={{/exclusiveMaximum}} {{maximum}}) then
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"value '${{name}}' is not greater than the {{#exclusiveMaximum}}exclusive {{/exclusiveMaximum}}maximum value {{maximum}}")
|
||||
}
|
||||
{{/maximum}}
|
||||
|
||||
{{#minLength}}
|
||||
// validate min length {{minLength}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val len = if {{name}} == null then 0 else {{name}}.length
|
||||
if (len < {{minLength}}) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"length $len is shorter than the min length {{minLength}}")
|
||||
}
|
||||
}
|
||||
{{/minLength}}
|
||||
|
||||
{{#maxLength}}
|
||||
// validate max length {{maxLength}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val len = if {{name}} == null then 0 else {{name}}.length
|
||||
if (len < {{maxLength}}) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"length $len is longer than the max length {{maxLength}}")
|
||||
}
|
||||
}
|
||||
{{/maxLength}}
|
||||
|
||||
{{#isEmail}}
|
||||
// validate {{name}} is a valid email address
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val emailRegex = """^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"""
|
||||
// validate {{name}} is email
|
||||
if ({{name}} == null || !emailRegex.r.matches({{name}})) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"${{name}} is not a valid email address according to the pattern $emailRegex")
|
||||
}
|
||||
}
|
||||
{{/isEmail}}
|
||||
|
||||
{{#required}}{{^isPrimitiveType}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if ({{name}} == null) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, "{{name}} is a required field and cannot be null")
|
||||
}
|
||||
}
|
||||
{{/isPrimitiveType}}{{/required}}
|
||||
|
||||
{{#uniqueItems}}
|
||||
// validate {{name}} has unique items
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if ({{name}} != null) {
|
||||
{{name}}.foldLeft(Set[{{{vendorExtensions.x-containertype-data}}}]()) {
|
||||
case (set, next) if set.contains(next) =>
|
||||
errors += ValidationError(
|
||||
path :+ {{classname}}.Fields.{{name}},
|
||||
s"duplicate value: $next"
|
||||
)
|
||||
set + next
|
||||
case (set, next) => set + next
|
||||
}
|
||||
}
|
||||
}
|
||||
{{/uniqueItems}}
|
||||
|
||||
{{#multipleOf}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
// validate {{name}} multiple of {{multipleOf}}
|
||||
if ({{name}} % {{multipleOf}} != 0) {
|
||||
errors += ValidationError(
|
||||
path :+ {{classname}}.Fields.{{name}},
|
||||
s"${{name}} is not a multiple of {{multipleOf}}"
|
||||
)
|
||||
}
|
||||
}
|
||||
{{/multipleOf}}
|
||||
|
||||
{{#minItems}}
|
||||
// validate min items {{minItems}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val len = if {{name}} == null then 0 else {{name}}.size
|
||||
if (len < {{minItems}}) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"{{name}} has $len, which is less than the min items {{minItems}}")
|
||||
}
|
||||
}
|
||||
{{/minItems}}
|
||||
|
||||
{{#maxItems}}
|
||||
// validate min items {{maxItems}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val len = if {{name}} == null then 0 else {{name}}.size
|
||||
if (len > {{maxItems}}) {
|
||||
errors += ValidationError(path :+ {{classname}}.Fields.{{name}}, s"{{name}} has $len, which is greater than the max items {{maxItems}}")
|
||||
}
|
||||
}
|
||||
{{/maxItems}}
|
||||
|
||||
{{#minProperties}}
|
||||
TODO - minProperties
|
||||
{{/minProperties}}
|
||||
|
||||
{{#maxProperties}}
|
||||
TODO - maxProperties
|
||||
{{/maxProperties}}
|
||||
|
||||
{{#items}}{{#isModel}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if ({{name}} != null) {
|
||||
{{name}}.zipWithIndex.foreach {
|
||||
case (value, i) if errors.isEmpty || !failFast =>
|
||||
errors ++= value.validationErrors(
|
||||
path :+ {{classname}}.Fields.{{name}} :+ Field(i.toString),
|
||||
failFast)
|
||||
case (value, i) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
{{/isModel}}{{/items}}
|
||||
{{#isModel}}
|
||||
// validating {{name}}
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if {{name}} != null then errors ++= {{name}}.validationErrors(path :+ {{classname}}.Fields.{{name}}, failFast)
|
||||
}
|
||||
{{/isModel}}
|
||||
|
||||
{{/vars}}
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[{{classname}}] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : {{classname}} = {
|
||||
{{classname}}(
|
||||
{{#vars}}
|
||||
{{name}} = {{#vendorExtensions.x-wrap-in-optional}}Option({{/vendorExtensions.x-wrap-in-optional}}
|
||||
{{name}}
|
||||
{{#vendorExtensions.x-wrap-in-optional}}){{/vendorExtensions.x-wrap-in-optional}}
|
||||
{{#vendorExtensions.x-map-asModel}}.map(_.asModel){{/vendorExtensions.x-map-asModel}}{{^-last}},{{/-last}}
|
||||
{{/vars}}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object {{classname}}Data {
|
||||
|
||||
given readWriter : RW[{{classname}}Data] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : {{classname}}Data = try {
|
||||
read[{{classname}}Data](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[{{classname}}Data] = try {
|
||||
read[List[{{classname}}Data]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[{{classname}}]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[{{classname}}]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, {{classname}}Data] = try {
|
||||
read[Map[String, {{classname}}Data]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, {{classname}}]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, {{classname}}]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{{/model}}
|
||||
{{/models}}
|
53
modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache
vendored
Normal file
53
modules/openapi-generator/src/main/resources/scala-cask/modelPackage.mustache
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
{{>licenseInfo}}
|
||||
package {{modelPackage}}
|
||||
|
||||
// model package
|
||||
import upickle.default._
|
||||
import java.time.*
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* This base class lets us refer to fields in exceptions
|
||||
*/
|
||||
class Field(val name : String)
|
||||
|
||||
final case class ValidationErrors(
|
||||
first: ValidationError,
|
||||
remaining: Seq[ValidationError],
|
||||
message: String
|
||||
) extends Exception(message)
|
||||
|
||||
object ValidationErrors {
|
||||
def apply(first: ValidationError, remaining: Seq[ValidationError]) = {
|
||||
val noun = if remaining.isEmpty then "error" else "errors"
|
||||
new ValidationErrors(
|
||||
first,
|
||||
remaining,
|
||||
remaining.mkString(s"${remaining.size + 1} $noun found: ${first}", "\n\t", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final case class ValidationError(path : Seq[Field], message : String) extends Exception(message) {
|
||||
override def toString = s"ValidationError for ${path.mkString(".")}: $message"
|
||||
}
|
||||
|
||||
given ReadWriter[ZonedDateTime] = readwriter[String].bimap[ZonedDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => ZonedDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[LocalDateTime] = readwriter[String].bimap[LocalDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => LocalDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[LocalDate] = readwriter[String].bimap[LocalDate](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => LocalDate.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse(
|
||||
OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
|
||||
)
|
||||
)
|
37
modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache
vendored
Normal file
37
modules/openapi-generator/src/main/resources/scala-cask/modelTest.mustache
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
{{>licenseInfo}}
|
||||
// this model was generated using modelTest.mustache
|
||||
package {{modelPackage}}
|
||||
{{#imports}}import {{import}}
|
||||
{{/imports}}
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
class {{classname}}Test extends AnyWordSpec with Matchers {
|
||||
|
||||
{{#operations}}
|
||||
// operation {{classname}}
|
||||
//
|
||||
{{#examples}}
|
||||
key = {{key}}
|
||||
value= {{value}}
|
||||
{{/examples}}
|
||||
{{/operations}}
|
||||
"{{classname}}.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try({{classname}}Data.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse {{example}}""" ignore {
|
||||
val Failure(err : ValidationErrors) = {{classname}}Data.fromJsonString("""{{example}}""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
{{/model}}
|
||||
{{/models}}
|
116
modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache
vendored
Normal file
116
modules/openapi-generator/src/main/resources/scala-cask/openapiRoute.mustache
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
{{>licenseInfo}}
|
||||
|
||||
// generated from openapiRoute.mustache
|
||||
package {{apiPackage}}
|
||||
|
||||
import cask.model.Response
|
||||
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
|
||||
/**
|
||||
* This code will try and download the swagger UI static files on startup
|
||||
*
|
||||
* That behaviour can be altered by:
|
||||
* - setting the environment variable SWAGGER_ON to false
|
||||
* - setting the environment variable SWAGGER_UI_URL to either the URL of a swagger UI zip or setting it to the empty string
|
||||
*
|
||||
*/
|
||||
object OpenApiRoutes {
|
||||
|
||||
def swaggerUIUrl: Option[String] = {
|
||||
// flag to turn SWAGGER off
|
||||
def useSwaggerUI = sys.env.get("SWAGGER_ON").map(_.toBoolean).getOrElse(true)
|
||||
|
||||
val defaultUrl = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.11.9.zip"
|
||||
Option(sys.env.getOrElse("SWAGGER_UI_URL", defaultUrl))
|
||||
.map(_.trim)
|
||||
.filterNot(_.isEmpty)
|
||||
.filter(_ => useSwaggerUI)
|
||||
}
|
||||
def apply(localPort: Int) = new OpenApiRoutes(localPort, swaggerUIUrl)
|
||||
}
|
||||
|
||||
class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Routes {
|
||||
|
||||
def openApiDir = "ui"
|
||||
|
||||
@cask.get("/")
|
||||
def index() = cask.Redirect("/ui/index.html")
|
||||
|
||||
@cask.staticFiles("/ui")
|
||||
def staticUI() = openApiDir
|
||||
|
||||
@cask.staticResources("/openapi.json")
|
||||
def staticOpenApi() = "openapi.json"
|
||||
|
||||
/** This code will try and download the swagger UI artefacts to a local directory to serve up
|
||||
*/
|
||||
object extract {
|
||||
|
||||
def openApiDirPath: Path = Paths.get(openApiDir)
|
||||
def hasSwagger = Files.exists(openApiDirPath) && Files.isDirectory(openApiDirPath)
|
||||
|
||||
import java.io.{BufferedInputStream, FileOutputStream, InputStream}
|
||||
import java.net.URL
|
||||
import java.util.zip.{ZipEntry, ZipInputStream}
|
||||
import scala.util.Using
|
||||
|
||||
def apply(url: String) = {
|
||||
if !hasSwagger then downloadAndExtractZip(url, openApiDir)
|
||||
}
|
||||
|
||||
def downloadAndExtractZip(url: String, outputDir: String): Unit = {
|
||||
val urlConn = new URL(url).openConnection()
|
||||
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
|
||||
|
||||
Using(urlConn.getInputStream) { inputStream =>
|
||||
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
|
||||
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
|
||||
|
||||
def isDist = entry.getName.contains("/dist/")
|
||||
def isNotMap = !entry.getName.endsWith(".map")
|
||||
|
||||
if (!entry.isDirectory && isDist && isNotMap) {
|
||||
val fileName = entry.getName.split("/").last
|
||||
extractFile(entry.getName, zipIn, s"$outputDir/$fileName")
|
||||
}
|
||||
zipIn.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {
|
||||
val fullPath = Paths.get(filePath).toAbsolutePath
|
||||
if !Files.exists(fullPath.getParent) then {
|
||||
Files.createDirectories(fullPath.getParent)
|
||||
}
|
||||
|
||||
// config hack - we replace the default url from this swagger conf to use our localhost
|
||||
//
|
||||
if name.endsWith("swagger-initializer.js") then {
|
||||
val textLines = scala.io.Source.fromInputStream(zipIn).getLines().map {
|
||||
case line if line.contains("url:") =>
|
||||
s""" url: "http://localhost:$localPort/openapi.json","""
|
||||
case line => line
|
||||
}
|
||||
|
||||
// keeping this compatible for java 8, where this is from >= java 11:
|
||||
// Files.writeString(fullPath, textLines.mkString("\n"))
|
||||
scala.util.Using(new java.io.PrintWriter(fullPath.toFile))(_.write(textLines.mkString("\n")))
|
||||
} else {
|
||||
Using(new FileOutputStream(filePath)) { outputStream =>
|
||||
val buffer = new Array[Byte](1024)
|
||||
LazyList
|
||||
.continually(zipIn.read(buffer))
|
||||
.takeWhile(_ != -1)
|
||||
.foreach(outputStream.write(buffer, 0, _))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract the swagger UI resources to our local directory
|
||||
swaggerUrl.foreach(url => extract(url))
|
||||
|
||||
initialize()
|
||||
}
|
56
modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache
vendored
Normal file
56
modules/openapi-generator/src/main/resources/scala-cask/parseHttpParams.mustache
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
for {
|
||||
{{#pathParams}}
|
||||
{{#isString}}
|
||||
{{paramName}} <- Parsed({{paramName}})
|
||||
{{/isString}}
|
||||
{{^isString}}
|
||||
{{paramName}} <- Parsed({{paramName}})
|
||||
{{/isString}}
|
||||
{{/pathParams}}
|
||||
{{#headerParams}}
|
||||
{{#required}}
|
||||
{{paramName}} <- request.headerSingleValueRequired("{{paramName}}")
|
||||
{{/required}}
|
||||
{{^required}}
|
||||
{{paramName}} <- request.headerSingleValueOptional("{{paramName}}")
|
||||
{{/required}}
|
||||
{{/headerParams}}
|
||||
{{#formParams}}
|
||||
{{#required}}
|
||||
{{paramName}} <- {{#isFile}}request.formValueAsFileRequired("{{paramName}}"){{/isFile}}{{^isFile}}request.formSingleValueRequired("{{paramName}}"){{/isFile}}
|
||||
{{/required}}
|
||||
{{^required}}
|
||||
{{paramName}} <- {{#isFile}}request.formValueAsFileOptional("{{paramName}}"){{/isFile}}{{^isFile}}request.formSingleValueOptional("{{paramName}}"){{/isFile}}
|
||||
{{/required}}
|
||||
{{/formParams}}
|
||||
{{#bodyParams}}
|
||||
{{#vendorExtensions.x-consumes-json}}
|
||||
{{#isArray}}
|
||||
{{paramName}} <- Parsed.fromTry({{vendorExtensions.x-container-type}}Data.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* array */
|
||||
{{/isArray}}
|
||||
{{^isArray}}
|
||||
{{#isMap}}
|
||||
{{#vendorExtensions.x-deserialize-asModelMap}}
|
||||
{{paramName}} <- Parsed.eval(read[Map[String, {{vendorExtensions.x-container-type}}](request.bodyAsString)).mapError(e => s"Error parsing json as a string map of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* x-deserialize-asModelMap */
|
||||
{{/vendorExtensions.x-deserialize-asModelMap}}
|
||||
{{^vendorExtensions.x-deserialize-asModelMap}}
|
||||
{{paramName}} <- Parsed.eval(read[Map[String, {{vendorExtensions.x-container-type}}]](request.bodyAsString)).mapError(e => s"Error parsing json as a string map of {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* not x-deserialize-asModelMap */
|
||||
{{/vendorExtensions.x-deserialize-asModelMap}}
|
||||
{{/isMap}}
|
||||
{{^isMap}}
|
||||
{{paramName}}Data <- Parsed.eval({{vendorExtensions.x-container-type}}Data.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as {{vendorExtensions.x-container-type}} from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
{{paramName}} <- Parsed.fromTry({{paramName}}Data.validated(failFast))
|
||||
{{/isMap}}
|
||||
{{/isArray}}
|
||||
{{/vendorExtensions.x-consumes-json}}
|
||||
{{^vendorExtensions.x-consumes-json}}
|
||||
{{#vendorExtensions.x-consumes-xml}}
|
||||
{{paramName}} <- Parsed.fail("TODO - xml deserialisation not yet supported. see src/main/resources/scala-cask/parseHttpParams.mustache in https://github.com/OpenAPITools/openapi-generator")
|
||||
{{/vendorExtensions.x-consumes-xml}}
|
||||
{{^vendorExtensions.x-consumes-xml}}
|
||||
{{paramName}} <- Parsed.fail("TODO - content deserialisation. see src/main/resources/scala-cask/parseHttpParams.mustache in https://github.com/OpenAPITools/openapi-generator")
|
||||
{{/vendorExtensions.x-consumes-xml}}
|
||||
{{/vendorExtensions.x-consumes-json}}
|
||||
{{/bodyParams}}
|
||||
result <- Parsed.eval(service.{{operationId}}({{vendorExtensions.x-param-list}}))
|
||||
} yield result
|
1
modules/openapi-generator/src/main/resources/scala-cask/pathExtractor.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/scala-cask/pathExtractor.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#vendorExtensions.x-path-remaining}}{{#isParam}}{{name}}{{/isParam}}{{^isParam}}"{{name}}"{{/isParam}}{{^-last}},{{/-last}}{{/vendorExtensions.x-path-remaining}}
|
1
modules/openapi-generator/src/main/resources/scala-cask/pathExtractorParams.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/scala-cask/pathExtractorParams.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#vendorExtensions.x-path-remaining}}{{#isParam}}{{name}}{{conversion}}{{/isParam}}{{#hasMoreParams}}, {{/hasMoreParams}}{{/vendorExtensions.x-path-remaining}}{{#vendorExtensions.x-has-path-remaining}},{{/vendorExtensions.x-has-path-remaining}}
|
@ -0,0 +1 @@
|
||||
sbt.version=1.9.9
|
@ -0,0 +1,3 @@
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
|
1
modules/openapi-generator/src/main/resources/scala-cask/queryParams.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/scala-cask/queryParams.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{#hasQueryParams}},{{/hasQueryParams}}{{#queryParams}}{{paramName}}{{#required}}{{^isMap}}{{^isArray}}.getOrElse({{#isString}}""{{/isString}}{{#isNumber}}0{{/isNumber}}{{#isBoolean}}false{{/isBoolean}}){{/isArray}}{{/isMap}}{{/required}}{{^-last}}, {{/-last}}{{/queryParams}}
|
41
samples/server/petstore/scala-cask/.github/workflows/bulidAndPublish.yml
vendored
Normal file
41
samples/server/petstore/scala-cask/.github/workflows/bulidAndPublish.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Scala CI with sbt
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
|
||||
- name: Cache sbt dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.ivy2/cache
|
||||
~/.sbt
|
||||
~/.m2
|
||||
key: ${{ runner.os }}-sbt-${{ hashFiles('**/*.sbt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-sbt-
|
||||
|
||||
- name: Build with sbt
|
||||
run: sbt clean compile
|
||||
|
||||
- name: Test with sbt
|
||||
run: sbt test
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
run: sbt publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
25
samples/server/petstore/scala-cask/.gitignore
vendored
Normal file
25
samples/server/petstore/scala-cask/.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# scala specific
|
||||
*.class
|
||||
*.log
|
||||
|
||||
# sbt specific
|
||||
.cache
|
||||
.history
|
||||
.lib/
|
||||
dist/*
|
||||
target/
|
||||
lib_managed/
|
||||
src_managed/
|
||||
project/boot/
|
||||
project/plugins/project/
|
||||
|
||||
# Scala-IDE specific
|
||||
.scala_dependencies
|
||||
.worksheet
|
||||
|
||||
# Mill specific
|
||||
out
|
||||
|
||||
# IntelliJ specific
|
||||
.idea
|
||||
*.iml
|
23
samples/server/petstore/scala-cask/.openapi-generator-ignore
Normal file
23
samples/server/petstore/scala-cask/.openapi-generator-ignore
Normal file
@ -0,0 +1,23 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
39
samples/server/petstore/scala-cask/.openapi-generator/FILES
Normal file
39
samples/server/petstore/scala-cask/.openapi-generator/FILES
Normal file
@ -0,0 +1,39 @@
|
||||
.github/workflows/bulidAndPublish.yml
|
||||
.gitignore
|
||||
.scalafmt.conf
|
||||
README.md
|
||||
README.md
|
||||
build.sbt
|
||||
build.sc
|
||||
example/Dockerfile
|
||||
example/Server.scala
|
||||
project/build.properties
|
||||
project/plugins.sbt
|
||||
src/main/scala/sample/cask/AppRoutes.scala
|
||||
src/main/scala/sample/cask/BaseApp.scala
|
||||
src/main/scala/sample/cask/ExampleApp.scala
|
||||
src/main/scala/sample/cask/api/OpenApiRoutes.scala
|
||||
src/main/scala/sample/cask/api/PetRoutes.scala
|
||||
src/main/scala/sample/cask/api/PetRoutes.scala
|
||||
src/main/scala/sample/cask/api/PetService.scala
|
||||
src/main/scala/sample/cask/api/StoreRoutes.scala
|
||||
src/main/scala/sample/cask/api/StoreRoutes.scala
|
||||
src/main/scala/sample/cask/api/StoreService.scala
|
||||
src/main/scala/sample/cask/api/UserRoutes.scala
|
||||
src/main/scala/sample/cask/api/UserRoutes.scala
|
||||
src/main/scala/sample/cask/api/UserService.scala
|
||||
src/main/scala/sample/cask/api/package.scala
|
||||
src/main/scala/sample/cask/model/ApiResponse.scala
|
||||
src/main/scala/sample/cask/model/ApiResponseData.scala
|
||||
src/main/scala/sample/cask/model/Category.scala
|
||||
src/main/scala/sample/cask/model/CategoryData.scala
|
||||
src/main/scala/sample/cask/model/Order.scala
|
||||
src/main/scala/sample/cask/model/OrderData.scala
|
||||
src/main/scala/sample/cask/model/Pet.scala
|
||||
src/main/scala/sample/cask/model/PetData.scala
|
||||
src/main/scala/sample/cask/model/Tag.scala
|
||||
src/main/scala/sample/cask/model/TagData.scala
|
||||
src/main/scala/sample/cask/model/User.scala
|
||||
src/main/scala/sample/cask/model/UserData.scala
|
||||
src/main/scala/sample/cask/model/package.scala
|
||||
src/main/scala/sample/cask/package.scala
|
@ -0,0 +1 @@
|
||||
7.5.0-SNAPSHOT
|
4
samples/server/petstore/scala-cask/.scalafmt.conf
Normal file
4
samples/server/petstore/scala-cask/.scalafmt.conf
Normal file
@ -0,0 +1,4 @@
|
||||
version = 3.6.1
|
||||
align.preset = more // For pretty alignment.
|
||||
maxColumn = 100
|
||||
runner.dialect = scala3
|
96
samples/server/petstore/scala-cask/README.md
Normal file
96
samples/server/petstore/scala-cask/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
# REST Service
|
||||
|
||||
This project contains the data models and REST services, generated from the [openapi-generator](https://github.com/OpenAPITools/openapi-generator) project.
|
||||
|
||||
The server implementation is based on Li's excellent [cask](https://com-lihaoyi.github.io/cask/) library.
|
||||
|
||||
# How to use this code
|
||||
|
||||
This code was designed so that it can be packaged up and semantically-versioned alongside your open-api schema.
|
||||
|
||||
That approach supports having separate "contract" repositories for microservice projects, where the pipeline for the
|
||||
contract repo might produce versioned jar artefacts which can then easily be brought in / referenced by a separate service project which simply
|
||||
implements the business logic.
|
||||
|
||||
You can read more about this approach [here](https://github.com/kindservices/contract-first-rest)
|
||||
|
||||
# How to implement your business logic
|
||||
There are a few options for using this code/applying your business logic for the services.
|
||||
|
||||
## Option 1 (preferred): Package and publish this boilerplate
|
||||
Typically, OpenApi templates are written to generate code which co-exists alongside the handwritten business logic.
|
||||
|
||||
While that works, it's also not ideal:
|
||||
* You have to ensure the generated code isn't checked in
|
||||
* Team members, build pipelines, etc all have to regenerate and recompile the same boilerplate code over and over
|
||||
* People can encounter IDE issues with generated code
|
||||
|
||||
Instead, you have the option of simply packaging/publishing this generated code, and then allowing service implementations
|
||||
to simply bring in the published code as a dependency.
|
||||
|
||||
The steps to do that are:
|
||||
|
||||
### Build/Publish
|
||||
This project is built using [sbt](https://www.scala-sbt.org/download/), so you can run `sbt publish` (or `sbt publishLocal`)
|
||||
|
||||
Or, for a zero-install docker build:
|
||||
```bash
|
||||
docker run -it --rm -v $(pwd):/app -w /app sbtscala/scala-sbt:eclipse-temurin-17.0.4_1.7.1_3.2.0 sbt publishLocal
|
||||
```
|
||||
|
||||
### Create a new separate implementation project
|
||||
Once published, you can create your server implementation in a new, clean, separate project based on [the example](./example)
|
||||
|
||||
This means all the boilerplate endpoint and model code is brought in as "just another jar", and you're free to
|
||||
create a greenfield project in whatever language (scala, java, kotlin) and build system of your choosing.
|
||||
|
||||
We show a simple, minimalistic example of a starting point in [the example project](./example)
|
||||
|
||||
## Option 2: Extend this generated example
|
||||
You can configure this project (for instance, setting up your own .gitignore rules and scripts) to leave the generated code as-is
|
||||
and provide your implementation alongside the generated code.
|
||||
|
||||
The place to start is by providing your own implementation of the Services defined in the `api` package -
|
||||
perhaps by creating your 'MyService.scala' code in a new `impl` package.
|
||||
|
||||
You then have several options for how to wire those in:
|
||||
|
||||
1) Create a new BaseApp instance to create your own Main entry point
|
||||
Follow the pattern in App.scala, but by passing your own implementations to BaseApp,
|
||||
ensuring you call `start` to start the server
|
||||
|
||||
```bash
|
||||
@main def run() = BaseApp(/* your services here/*).start()
|
||||
```
|
||||
|
||||
2) Extend either BaseApp class or mix in the AppRoutes trait
|
||||
You can create your own main entry point with further control of the main cask app by extending
|
||||
the BaseApp or otherwise creating your own CaskApp which mixes in the AppRoutes
|
||||
|
||||
```bash
|
||||
object MyApp extends BaseApp(/* your services here/*) {
|
||||
// any overrides, new routes, etc here
|
||||
start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Customising the generated code
|
||||
|
||||
A typical config.yml used to alter the generated code may look like this:
|
||||
```
|
||||
groupId: "ex.amp.le"
|
||||
artifactId: "pets-test"
|
||||
apiPackage: "ex.ample.api"
|
||||
modelPackage: "ex.ample.model"
|
||||
```
|
||||
|
||||
Which you would then pass to the generator like this:
|
||||
```
|
||||
docker run --rm \
|
||||
-v ${PWD}:/local openapitools/openapi-generator-cli generate \
|
||||
-i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v3.0/petstore.yaml \
|
||||
-g scala-cask \
|
||||
-c /local/config.yml \
|
||||
-o /local/path/to/output_dir
|
||||
```
|
29
samples/server/petstore/scala-cask/build.sbt
Normal file
29
samples/server/petstore/scala-cask/build.sbt
Normal file
@ -0,0 +1,29 @@
|
||||
name := "scala-cask-petstore"
|
||||
organization:="cask.groupId"
|
||||
version := "0.0.1-SNAPSHOT"
|
||||
scalaVersion := "3.3.1"
|
||||
scalafmtOnCompile := true
|
||||
libraryDependencies ++= Seq(
|
||||
"com.lihaoyi" %% "cask" % "0.9.2" ,
|
||||
"com.lihaoyi" %% "upickle" % "3.2.0",
|
||||
"org.scalatest" %% "scalatest" % "3.2.18" % Test
|
||||
)
|
||||
|
||||
publishMavenStyle := true
|
||||
|
||||
val githubUser = "GIT_USER_ID"
|
||||
val githubRepo = "GIT_REPO_ID"
|
||||
publishTo := Some("GitHub Package Registry" at s"https://maven.pkg.github.com/$githubUser/$githubRepo")
|
||||
|
||||
sys.env.get("GITHUB_TOKEN") match {
|
||||
case Some(token) if !token.isEmpty =>
|
||||
credentials += Credentials(
|
||||
"GitHub Package Registry",
|
||||
"maven.pkg.github.com",
|
||||
githubUser,
|
||||
token
|
||||
)
|
||||
case _ =>
|
||||
println("\n\t\tGITHUB_TOKEN not set - assuming a local build\n\n")
|
||||
credentials ++= Nil
|
||||
}
|
43
samples/server/petstore/scala-cask/build.sc
Normal file
43
samples/server/petstore/scala-cask/build.sc
Normal file
@ -0,0 +1,43 @@
|
||||
import mill._, scalalib._, scalafmt._, publish._
|
||||
|
||||
// Mill build file (see https://mill-build.com/mill/Intro_to_Mill.html).
|
||||
// run with:
|
||||
//
|
||||
// mill _.compile
|
||||
// mill _.reformat
|
||||
// mill _.publishLocal
|
||||
// mill _.test.test
|
||||
object scala-cask-petstore extends SbtModule with ScalafmtModule with PublishModule {
|
||||
def scalaVersion = "3.3.1"
|
||||
|
||||
def pomSettings = PomSettings(
|
||||
description = "scala-cask-petstore",
|
||||
organization = "cask.groupId",
|
||||
url = "https://github.com/<username>/scala-cask-petstore",
|
||||
licenses = Seq(License.MIT),
|
||||
versionControl = VersionControl.github("<username>", "scala-cask-petstore"),
|
||||
developers = Seq(
|
||||
// Developer("<username>", "<your name>", "https://github.com/<you>")
|
||||
)
|
||||
)
|
||||
|
||||
def publishVersion: mill.T[String] = T("0.0.1-SNAPSHOT")
|
||||
|
||||
def ivyDeps = Agg(
|
||||
ivy"com.lihaoyi::cask:0.9.2" ,
|
||||
ivy"com.lihaoyi::upickle:3.2.0"
|
||||
)
|
||||
|
||||
override def sources = T.sources(millSourcePath / os.up / "src" / "main" / "scala")
|
||||
override def resources = T.sources(millSourcePath / os.up / "src" / "main" / "resources")
|
||||
|
||||
object test extends SbtModuleTests {
|
||||
def ivyDeps = Agg(
|
||||
ivy"org.scalactic::scalactic:3.2.18",
|
||||
ivy"org.scalatest::scalatest:3.2.18"
|
||||
)
|
||||
|
||||
def testFramework = "org.scalatest.tools.Framework"
|
||||
override def sources = T.sources(millSourcePath / os.up / "src" / "test" / "scala")
|
||||
}
|
||||
}
|
13
samples/server/petstore/scala-cask/example/Dockerfile
Normal file
13
samples/server/petstore/scala-cask/example/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM virtuslab/scala-cli:latest as build
|
||||
WORKDIR /app
|
||||
COPY ./Server.scala /app/
|
||||
# note: this assumes a published server stub jar.
|
||||
# If you've published this locally, you would need to copy those into this image,
|
||||
# perhaps by using coursier fetch
|
||||
RUN scala-cli --power package /app/Server.scala --assembly -o app.jar
|
||||
|
||||
# The main image
|
||||
FROM openjdk:23-slim
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/app.jar /app/
|
||||
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
|
61
samples/server/petstore/scala-cask/example/Server.scala
Normal file
61
samples/server/petstore/scala-cask/example/Server.scala
Normal file
@ -0,0 +1,61 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "cask.groupId::scala-cask-petstore:0.0.1-SNAPSHOT"
|
||||
//> using repositories https://maven.pkg.github.com/GIT_USER_ID/GIT_REPO_ID
|
||||
|
||||
|
||||
/**
|
||||
* This single file can contain the business logic for a REST service.
|
||||
* ====================================
|
||||
* == zero-install build with docker ==
|
||||
* ====================================
|
||||
*
|
||||
*
|
||||
* ```
|
||||
* docker build . -t scala-cask-petstore:latest
|
||||
* ```
|
||||
* ======================
|
||||
* == Building Locally ==
|
||||
* ======================
|
||||
* This project can be built using [[scala-clit][https://scala-cli.virtuslab.org]]
|
||||
*
|
||||
* To simply run the project
|
||||
* ```
|
||||
* scala-cli Server.scala
|
||||
* ```
|
||||
*
|
||||
* To create a runnable jar, run:
|
||||
* ```
|
||||
* scala-cli --power package Server.scala -o app-assembly --assembly
|
||||
* ```
|
||||
*
|
||||
* To produce a docker image (no need for the Dockerfile), run:
|
||||
* ```
|
||||
* scala-cli --power package --docker Server.scala --docker-image-repository app-docker
|
||||
* ```
|
||||
*
|
||||
* To generate an IDE project:
|
||||
* ```
|
||||
* scala-cli setup-ide . --scala 3.3
|
||||
* ```
|
||||
*/
|
||||
package app
|
||||
|
||||
import cask.groupId.server.BaseApp
|
||||
import sample.cask.api.*
|
||||
import sample.cask.model.*
|
||||
|
||||
import java.io.File
|
||||
|
||||
// TODO - write your business logic for your services here (the defaults all return 'not implemented'):
|
||||
val myPetService : PetService = PetService() // <-- replace this with your implementation
|
||||
val myStoreService : StoreService = StoreService() // <-- replace this with your implementation
|
||||
val myUserService : UserService = UserService() // <-- replace this with your implementation
|
||||
|
||||
/** This is your main entry point for your REST service
|
||||
* It extends BaseApp which defines the business logic for your services
|
||||
*/
|
||||
object Server extends BaseApp(appPetService = myPetService,
|
||||
appStoreService = myStoreService,
|
||||
appUserService = myUserService):
|
||||
start()
|
||||
|
@ -0,0 +1 @@
|
||||
sbt.version=1.9.9
|
3
samples/server/petstore/scala-cask/project/plugins.sbt
Normal file
3
samples/server/petstore/scala-cask/project/plugins.sbt
Normal file
@ -0,0 +1,3 @@
|
||||
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
|
||||
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6")
|
1032
samples/server/petstore/scala-cask/src/main/resources/openapi.json
Normal file
1032
samples/server/petstore/scala-cask/src/main/resources/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package cask.groupId.server
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
import _root_.sample.cask.api.*
|
||||
|
||||
/**
|
||||
* This trait encapsulates the business logic (services) and the
|
||||
* http routes which handle the http requests sent to those services.
|
||||
*
|
||||
* There are default 'not implemented' implementations for the service.
|
||||
*
|
||||
* If you wanted fine-grained control over the routes and services, you could
|
||||
* extend the cask.MainRoutes and mix in this trait by using this:
|
||||
*
|
||||
* \{\{\{
|
||||
* override def allRoutes = appRoutes
|
||||
* \}\}\}
|
||||
*
|
||||
* More typically, however, you would extend the 'BaseApp' class
|
||||
*/
|
||||
trait AppRoutes {
|
||||
def appPetService : PetService = PetService()
|
||||
def routeForPet : PetRoutes = PetRoutes(appPetService)
|
||||
def appStoreService : StoreService = StoreService()
|
||||
def routeForStore : StoreRoutes = StoreRoutes(appStoreService)
|
||||
def appUserService : UserService = UserService()
|
||||
def routeForUser : UserRoutes = UserRoutes(appUserService)
|
||||
|
||||
def appRoutes = Seq(
|
||||
routeForPet ,
|
||||
routeForStore ,
|
||||
routeForUser
|
||||
)
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package cask.groupId.server
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
import _root_.sample.cask.api.*
|
||||
|
||||
/**
|
||||
* This class was created with the intention of being extended by some runnable object,
|
||||
* passing in the custom business logic services
|
||||
*/
|
||||
class BaseApp(
|
||||
override val appPetService : PetService = PetService(),
|
||||
|
||||
override val appStoreService : StoreService = StoreService(),
|
||||
|
||||
override val appUserService : UserService = UserService(),
|
||||
override val port : Int = sys.env.get("PORT").map(_.toInt).getOrElse(8080)) extends cask.MainRoutes with AppRoutes {
|
||||
|
||||
/** routes for the UI
|
||||
* Subclasses can override to turn this off
|
||||
*/
|
||||
def openApiRoute: Option[cask.Routes] = Option(OpenApiRoutes(port))
|
||||
|
||||
override def allRoutes = appRoutes ++ openApiRoute
|
||||
|
||||
|
||||
override def host: String = "0.0.0.0"
|
||||
|
||||
def start() = locally {
|
||||
initialize()
|
||||
println(box(s""" 🚀 browse to localhost:$port 🚀
|
||||
| host : $host
|
||||
| port : $port
|
||||
| verbose : $verbose
|
||||
| debugMode : $debugMode
|
||||
|""".stripMargin))
|
||||
|
||||
// if java.awt.Desktop.isDesktopSupported then {
|
||||
// java.awt.Desktop.getDesktop.browse(new java.net.URI(s"http://localhost:${port}"))
|
||||
// }
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.9.2"
|
||||
//> using lib "com.lihaoyi::scalatags:0.8.2"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this file was generated from app.mustache
|
||||
package cask.groupId.server
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
import _root_.sample.cask.api.*
|
||||
|
||||
/**
|
||||
* This is an example of how you might extends BaseApp for a runnable application.
|
||||
*
|
||||
* See the README.md for how to create your own app
|
||||
*/
|
||||
object ExampleApp extends BaseApp() {
|
||||
start()
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// generated from openapiRoute.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import cask.model.Response
|
||||
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
|
||||
/**
|
||||
* This code will try and download the swagger UI static files on startup
|
||||
*
|
||||
* That behaviour can be altered by:
|
||||
* - setting the environment variable SWAGGER_ON to false
|
||||
* - setting the environment variable SWAGGER_UI_URL to either the URL of a swagger UI zip or setting it to the empty string
|
||||
*
|
||||
*/
|
||||
object OpenApiRoutes {
|
||||
|
||||
def swaggerUIUrl: Option[String] = {
|
||||
// flag to turn SWAGGER off
|
||||
def useSwaggerUI = sys.env.get("SWAGGER_ON").map(_.toBoolean).getOrElse(true)
|
||||
|
||||
val defaultUrl = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.11.9.zip"
|
||||
Option(sys.env.getOrElse("SWAGGER_UI_URL", defaultUrl))
|
||||
.map(_.trim)
|
||||
.filterNot(_.isEmpty)
|
||||
.filter(_ => useSwaggerUI)
|
||||
}
|
||||
def apply(localPort: Int) = new OpenApiRoutes(localPort, swaggerUIUrl)
|
||||
}
|
||||
|
||||
class OpenApiRoutes(localPort: Int, swaggerUrl: Option[String]) extends cask.Routes {
|
||||
|
||||
def openApiDir = "ui"
|
||||
|
||||
@cask.get("/")
|
||||
def index() = cask.Redirect("/ui/index.html")
|
||||
|
||||
@cask.staticFiles("/ui")
|
||||
def staticUI() = openApiDir
|
||||
|
||||
@cask.staticResources("/openapi.json")
|
||||
def staticOpenApi() = "openapi.json"
|
||||
|
||||
/** This code will try and download the swagger UI artefacts to a local directory to serve up
|
||||
*/
|
||||
object extract {
|
||||
|
||||
def openApiDirPath: Path = Paths.get(openApiDir)
|
||||
def hasSwagger = Files.exists(openApiDirPath) && Files.isDirectory(openApiDirPath)
|
||||
|
||||
import java.io.{BufferedInputStream, FileOutputStream, InputStream}
|
||||
import java.net.URL
|
||||
import java.util.zip.{ZipEntry, ZipInputStream}
|
||||
import scala.util.Using
|
||||
|
||||
def apply(url: String) = {
|
||||
if !hasSwagger then downloadAndExtractZip(url, openApiDir)
|
||||
}
|
||||
|
||||
def downloadAndExtractZip(url: String, outputDir: String): Unit = {
|
||||
val urlConn = new URL(url).openConnection()
|
||||
urlConn.setRequestProperty("User-Agent", "Mozilla/5.0")
|
||||
|
||||
Using(urlConn.getInputStream) { inputStream =>
|
||||
val zipIn = new ZipInputStream(new BufferedInputStream(inputStream))
|
||||
LazyList.continually(zipIn.getNextEntry).takeWhile(_ != null).foreach { entry =>
|
||||
|
||||
def isDist = entry.getName.contains("/dist/")
|
||||
def isNotMap = !entry.getName.endsWith(".map")
|
||||
|
||||
if (!entry.isDirectory && isDist && isNotMap) {
|
||||
val fileName = entry.getName.split("/").last
|
||||
extractFile(entry.getName, zipIn, s"$outputDir/$fileName")
|
||||
}
|
||||
zipIn.closeEntry()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def extractFile(name: String, zipIn: ZipInputStream, filePath: String): Unit = {
|
||||
val fullPath = Paths.get(filePath).toAbsolutePath
|
||||
if !Files.exists(fullPath.getParent) then {
|
||||
Files.createDirectories(fullPath.getParent)
|
||||
}
|
||||
|
||||
// config hack - we replace the default url from this swagger conf to use our localhost
|
||||
//
|
||||
if name.endsWith("swagger-initializer.js") then {
|
||||
val textLines = scala.io.Source.fromInputStream(zipIn).getLines().map {
|
||||
case line if line.contains("url:") =>
|
||||
s""" url: "http://localhost:$localPort/openapi.json","""
|
||||
case line => line
|
||||
}
|
||||
|
||||
// keeping this compatible for java 8, where this is from >= java 11:
|
||||
// Files.writeString(fullPath, textLines.mkString("\n"))
|
||||
scala.util.Using(new java.io.PrintWriter(fullPath.toFile))(_.write(textLines.mkString("\n")))
|
||||
} else {
|
||||
Using(new FileOutputStream(filePath)) { outputStream =>
|
||||
val buffer = new Array[Byte](1024)
|
||||
LazyList
|
||||
.continually(zipIn.read(buffer))
|
||||
.takeWhile(_ != -1)
|
||||
.foreach(outputStream.write(buffer, 0, _))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extract the swagger UI resources to our local directory
|
||||
swaggerUrl.foreach(url => extract(url))
|
||||
|
||||
initialize()
|
||||
}
|
@ -0,0 +1,212 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this is generated from apiRoutes.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import sample.cask.model.*
|
||||
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
import sample.cask.model.ApiResponse
|
||||
import java.io.File
|
||||
import sample.cask.model.Pet
|
||||
|
||||
class PetRoutes(service : PetService) extends cask.Routes {
|
||||
|
||||
// route group for routeWorkAroundForPOSTPet
|
||||
@cask.post("/pet", true)
|
||||
def routeWorkAroundForPOSTPet(request: cask.Request) = {
|
||||
request.remainingPathSegments match {
|
||||
case Seq() => addPet(request)
|
||||
case Seq(petId) => updatePetWithForm(petId.toLong,request)
|
||||
case Seq(petId,"uploadImage") => uploadFile(petId.toLong,request)
|
||||
case _ => cask.Response("Not Found", statusCode = 404)
|
||||
}
|
||||
}
|
||||
// route group for routeWorkAroundForGETPet
|
||||
@cask.get("/pet", true)
|
||||
def routeWorkAroundForGETPet(request: cask.Request,status : Seq[String] = Nil,tags : Seq[String] = Nil) = {
|
||||
request.remainingPathSegments match {
|
||||
case Seq("findByStatus") => findPetsByStatus(request,status)
|
||||
case Seq("findByTags") => findPetsByTags(request,tags)
|
||||
case Seq(petId) => getPetById(petId.toLong,request)
|
||||
case _ => cask.Response("Not Found", statusCode = 404)
|
||||
}
|
||||
}
|
||||
|
||||
/** Add a new pet to the store
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet")
|
||||
def addPet(request: cask.Request) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
pet <- Parsed.fromTry(petData.validated(failFast))
|
||||
result <- Parsed.eval(service.addPet(pet))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Deletes a pet
|
||||
*
|
||||
*/
|
||||
@cask.delete("/pet/:petId")
|
||||
def deletePet(petId : Long, request: cask.Request) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petId <- Parsed(petId)
|
||||
apiKey <- request.headerSingleValueOptional("apiKey")
|
||||
result <- Parsed.eval(service.deletePet(petId, apiKey))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Finds Pets by status
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/findByStatus")
|
||||
def findPetsByStatus(request: cask.Request, status : Seq[String]) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
result <- Parsed.eval(service.findPetsByStatus(status))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Finds Pets by tags
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/findByTags")
|
||||
def findPetsByTags(request: cask.Request, tags : Seq[String]) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
result <- Parsed.eval(service.findPetsByTags(tags))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Find pet by ID
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/findByStatus, /pet/findByTags] after/pet, ignoring @cask.get("/pet/:petId")
|
||||
def getPetById(petId : Long, request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petId <- Parsed(petId)
|
||||
result <- Parsed.eval(service.getPetById(petId))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Update an existing pet
|
||||
*
|
||||
*/
|
||||
@cask.put("/pet")
|
||||
def updatePet(request: cask.Request) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petData <- Parsed.eval(PetData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Pet from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
pet <- Parsed.fromTry(petData.validated(failFast))
|
||||
result <- Parsed.eval(service.updatePet(pet))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Updates a pet in the store with form data
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet/:petId")
|
||||
def updatePetWithForm(petId : Long, request: cask.Request) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petId <- Parsed(petId)
|
||||
name <- request.formSingleValueOptional("name")
|
||||
status <- request.formSingleValueOptional("status")
|
||||
result <- Parsed.eval(service.updatePetWithForm(petId, name, status))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** uploads an image
|
||||
*
|
||||
*/
|
||||
// conflicts with [/pet/{petId}, /pet/{petId}/uploadImage, /pet] after/pet, ignoring @cask.post("/pet/:petId/uploadImage")
|
||||
def uploadFile(petId : Long, request: cask.Request) = {
|
||||
// auth method petstore_auth : oauth2, keyParamName:
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
petId <- Parsed(petId)
|
||||
additionalMetadata <- request.formSingleValueOptional("additionalMetadata")
|
||||
file <- request.formValueAsFileOptional("file")
|
||||
result <- Parsed.eval(service.uploadFile(petId, additionalMetadata, file))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// generated from apiService.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import _root_.sample.cask.model.ApiResponse
|
||||
import _root_.java.io.File
|
||||
import _root_.sample.cask.model.Pet
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
|
||||
object PetService {
|
||||
def apply() : PetService = new PetService {
|
||||
override def addPet(pet : Pet) : Pet = ???
|
||||
override def deletePet(petId : Long, apiKey : Option[String]) : Unit = ???
|
||||
override def findPetsByStatus(status : Seq[String]) : List[Pet] = ???
|
||||
override def findPetsByTags(tags : Seq[String]) : List[Pet] = ???
|
||||
override def getPetById(petId : Long) : Pet = ???
|
||||
override def updatePet(pet : Pet) : Pet = ???
|
||||
override def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit = ???
|
||||
override def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse = ???
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Pet business-logic
|
||||
*/
|
||||
trait PetService {
|
||||
/** Add a new pet to the store
|
||||
*
|
||||
* @return Pet
|
||||
*/
|
||||
def addPet(pet : Pet) : Pet
|
||||
/** Deletes a pet
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def deletePet(petId : Long, apiKey : Option[String]) : Unit
|
||||
/** Finds Pets by status
|
||||
*
|
||||
* @return List[Pet]
|
||||
*/
|
||||
def findPetsByStatus(status : Seq[String]) : List[Pet]
|
||||
/** Finds Pets by tags
|
||||
*
|
||||
* @return List[Pet]
|
||||
*/
|
||||
def findPetsByTags(tags : Seq[String]) : List[Pet]
|
||||
/** Find pet by ID
|
||||
*
|
||||
* @return Pet
|
||||
*/
|
||||
def getPetById(petId : Long) : Pet
|
||||
/** Update an existing pet
|
||||
*
|
||||
* @return Pet
|
||||
*/
|
||||
def updatePet(pet : Pet) : Pet
|
||||
/** Updates a pet in the store with form data
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def updatePetWithForm(petId : Long, name : Option[String], status : Option[String]) : Unit
|
||||
/** uploads an image
|
||||
*
|
||||
* @return ApiResponse
|
||||
*/
|
||||
def uploadFile(petId : Long, additionalMetadata : Option[String], file : Option[File]) : ApiResponse
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this is generated from apiRoutes.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import sample.cask.model.*
|
||||
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
import sample.cask.model.Order
|
||||
|
||||
class StoreRoutes(service : StoreService) extends cask.Routes {
|
||||
|
||||
|
||||
/** Delete purchase order by ID
|
||||
*
|
||||
*/
|
||||
@cask.delete("/store/order/:orderId")
|
||||
def deleteOrder(orderId : String, request: cask.Request) = {
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
orderId <- Parsed(orderId)
|
||||
result <- Parsed.eval(service.deleteOrder(orderId))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Returns pet inventories by status
|
||||
*
|
||||
*/
|
||||
@cask.get("/store/inventory")
|
||||
def getInventory(request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
result <- Parsed.eval(service.getInventory())
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Find purchase order by ID
|
||||
*
|
||||
*/
|
||||
@cask.get("/store/order/:orderId")
|
||||
def getOrderById(orderId : Long, request: cask.Request) = {
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
orderId <- Parsed(orderId)
|
||||
result <- Parsed.eval(service.getOrderById(orderId))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Place an order for a pet
|
||||
*
|
||||
*/
|
||||
@cask.post("/store/order")
|
||||
def placeOrder(request: cask.Request) = {
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
orderData <- Parsed.eval(OrderData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as Order from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
order <- Parsed.fromTry(orderData.validated(failFast))
|
||||
result <- Parsed.eval(service.placeOrder(order))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// generated from apiService.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import _root_.sample.cask.model.Order
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
|
||||
object StoreService {
|
||||
def apply() : StoreService = new StoreService {
|
||||
override def deleteOrder(orderId : String) : Unit = ???
|
||||
override def getInventory() : Map[String, Int] = ???
|
||||
override def getOrderById(orderId : Long) : Order = ???
|
||||
override def placeOrder(order : Order) : Order = ???
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Store business-logic
|
||||
*/
|
||||
trait StoreService {
|
||||
/** Delete purchase order by ID
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def deleteOrder(orderId : String) : Unit
|
||||
/** Returns pet inventories by status
|
||||
*
|
||||
* @return Map[String, Int]
|
||||
*/
|
||||
def getInventory() : Map[String, Int]
|
||||
/** Find purchase order by ID
|
||||
*
|
||||
* @return Order
|
||||
*/
|
||||
def getOrderById(orderId : Long) : Order
|
||||
/** Place an order for a pet
|
||||
*
|
||||
* @return Order
|
||||
*/
|
||||
def placeOrder(order : Order) : Order
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// this is generated from apiRoutes.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import sample.cask.model.*
|
||||
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
import sample.cask.model.User
|
||||
|
||||
class UserRoutes(service : UserService) extends cask.Routes {
|
||||
|
||||
// route group for routeWorkAroundForGETUser
|
||||
@cask.get("/user", true)
|
||||
def routeWorkAroundForGETUser(request: cask.Request,username : Option[String] = None,password : Option[String] = None) = {
|
||||
request.remainingPathSegments match {
|
||||
case Seq("login") => loginUser(request,username.getOrElse(""), password.getOrElse(""))
|
||||
case Seq("logout") => logoutUser(request)
|
||||
case Seq(username) => getUserByName(username,request)
|
||||
case _ => cask.Response("Not Found", statusCode = 404)
|
||||
}
|
||||
}
|
||||
|
||||
/** Create user
|
||||
*
|
||||
*/
|
||||
@cask.post("/user")
|
||||
def createUser(request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
user <- Parsed.fromTry(userData.validated(failFast))
|
||||
result <- Parsed.eval(service.createUser(user))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Creates list of users with given input array
|
||||
*
|
||||
*/
|
||||
@cask.post("/user/createWithArray")
|
||||
def createUsersWithArrayInput(request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
user <- Parsed.fromTry(UserData.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of User from >${request.bodyAsString}< : ${e}") /* array */
|
||||
result <- Parsed.eval(service.createUsersWithArrayInput(user))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Creates list of users with given input array
|
||||
*
|
||||
*/
|
||||
@cask.post("/user/createWithList")
|
||||
def createUsersWithListInput(request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
user <- Parsed.fromTry(UserData.manyFromJsonStringValidated(request.bodyAsString)).mapError(e => s"Error parsing json as an array of User from >${request.bodyAsString}< : ${e}") /* array */
|
||||
result <- Parsed.eval(service.createUsersWithListInput(user))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Delete user
|
||||
*
|
||||
*/
|
||||
@cask.delete("/user/:username")
|
||||
def deleteUser(username : String, request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
username <- Parsed(username)
|
||||
result <- Parsed.eval(service.deleteUser(username))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Get user by user name
|
||||
*
|
||||
*/
|
||||
// conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/:username")
|
||||
def getUserByName(username : String, request: cask.Request) = {
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
username <- Parsed(username)
|
||||
result <- Parsed.eval(service.getUserByName(username))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Logs user into the system
|
||||
*
|
||||
*/
|
||||
// conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/login")
|
||||
def loginUser(request: cask.Request, username : String, password : String) = {
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
result <- Parsed.eval(service.loginUser(username, password))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Logs out current logged in user session
|
||||
*
|
||||
*/
|
||||
// conflicts with [/user/{username}, /user/login, /user/logout] after/user, ignoring @cask.get("/user/logout")
|
||||
def logoutUser(request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
result <- Parsed.eval(service.logoutUser())
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
/** Updated user
|
||||
*
|
||||
*/
|
||||
@cask.put("/user/:username")
|
||||
def updateUser(username : String, request: cask.Request) = {
|
||||
// auth method api_key : apiKey, keyParamName: api_key
|
||||
|
||||
def failFast = request.queryParams.keySet.contains("failFast")
|
||||
|
||||
val result = for {
|
||||
username <- Parsed(username)
|
||||
userData <- Parsed.eval(UserData.fromJsonString(request.bodyAsString)).mapError(e => s"Error parsing json as User from >${request.bodyAsString}< : ${e}") /* not array or map */
|
||||
user <- Parsed.fromTry(userData.validated(failFast))
|
||||
result <- Parsed.eval(service.updateUser(username, user))
|
||||
} yield result
|
||||
|
||||
result match {
|
||||
case Left(error) => cask.Response(error, 500)
|
||||
case Right(_) => cask.Response("", 200)
|
||||
}
|
||||
}
|
||||
|
||||
initialize()
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
//> using scala "3.3.1"
|
||||
//> using lib "com.lihaoyi::cask:0.8.3"
|
||||
//> using lib "com.lihaoyi::scalatags:0.12.0"
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
|
||||
// generated from apiService.mustache
|
||||
package sample.cask.api
|
||||
|
||||
import _root_.java.time.OffsetDateTime
|
||||
import _root_.sample.cask.model.User
|
||||
|
||||
import _root_.sample.cask.model.*
|
||||
|
||||
object UserService {
|
||||
def apply() : UserService = new UserService {
|
||||
override def createUser(user : User) : Unit = ???
|
||||
override def createUsersWithArrayInput(user : Seq[User]) : Unit = ???
|
||||
override def createUsersWithListInput(user : Seq[User]) : Unit = ???
|
||||
override def deleteUser(username : String) : Unit = ???
|
||||
override def getUserByName(username : String) : User = ???
|
||||
override def loginUser(username : String, password : String) : String = ???
|
||||
override def logoutUser() : Unit = ???
|
||||
override def updateUser(username : String, user : User) : Unit = ???
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The User business-logic
|
||||
*/
|
||||
trait UserService {
|
||||
/** Create user
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def createUser(user : User) : Unit
|
||||
/** Creates list of users with given input array
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def createUsersWithArrayInput(user : Seq[User]) : Unit
|
||||
/** Creates list of users with given input array
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def createUsersWithListInput(user : Seq[User]) : Unit
|
||||
/** Delete user
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def deleteUser(username : String) : Unit
|
||||
/** Get user by user name
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
def getUserByName(username : String) : User
|
||||
/** Logs user into the system
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
def loginUser(username : String, password : String) : String
|
||||
/** Logs out current logged in user session
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def logoutUser() : Unit
|
||||
/** Updated user
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
def updateUser(username : String, user : User) : Unit
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
package sample.cask.api
|
||||
|
||||
|
||||
import cask.FormEntry
|
||||
import io.undertow.server.handlers.form.{FormData, FormParserFactory}
|
||||
|
||||
import java.io.File
|
||||
import scala.jdk.CollectionConverters.*
|
||||
import java.time.LocalDate
|
||||
import java.util.UUID
|
||||
import scala.reflect.ClassTag
|
||||
import scala.util.*
|
||||
|
||||
// needed for BigDecimal params
|
||||
given cask.endpoints.QueryParamReader.SimpleParam[BigDecimal](BigDecimal.apply)
|
||||
|
||||
// a parsed value from an HTTP request
|
||||
opaque type Parsed[A] = Either[String, A]
|
||||
|
||||
object Parsed {
|
||||
def apply[A](value: A): Parsed[A] = Right(value)
|
||||
|
||||
def eval[A](value: => A): Parsed[A] = Try(value) match {
|
||||
case Failure(exp) => Left(s"Error: ${exp.getMessage}")
|
||||
case Success(ok) => Right(ok)
|
||||
}
|
||||
|
||||
def fromTry[A](value : Try[A]) = value match {
|
||||
case Failure(err) => Left(err.getMessage)
|
||||
case Success(ok) => Right(ok)
|
||||
}
|
||||
|
||||
def fail[A](msg: String): Parsed[A] = Left(msg)
|
||||
|
||||
def optionalValue(map: Map[String, collection.Seq[String]], key: String): Parsed[Option[String]] = {
|
||||
map.get(key) match {
|
||||
case Some(Seq(only: String)) => Parsed(Option(only))
|
||||
case Some(Seq()) => Parsed(None)
|
||||
case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
|
||||
case None => Parsed(None)
|
||||
}
|
||||
}
|
||||
|
||||
def singleValue(map: Map[String, collection.Seq[String]], key : String): Parsed[String] = {
|
||||
map.get(key) match {
|
||||
case Some(Seq(only : String)) => Parsed(only)
|
||||
case Some(Seq()) => Parsed("")
|
||||
case Some(many) => Parsed.fail(s"${many.size} values set for '$key'")
|
||||
case None => Parsed.fail(s"required '$key' was not set")
|
||||
}
|
||||
}
|
||||
|
||||
def manyValues(map: Map[String, collection.Seq[String]], key : String, required: Boolean): Parsed[List[String]] = {
|
||||
map.get(key) match {
|
||||
case Some(many) => Parsed(many.toList)
|
||||
case None if required => Parsed.fail(s"required '$key' was not set")
|
||||
case None => Parsed(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension[A] (parsed: Parsed[A]) {
|
||||
def toEither: Either[String, A] = parsed
|
||||
|
||||
def asLong(using ev : A =:= String): Parsed[Long] = as[Long](_.toLongOption)
|
||||
def asBoolean(using ev : A =:= String): Parsed[Boolean] = as[Boolean](_.toBooleanOption)
|
||||
def asInt(using ev : A =:= String): Parsed[Int] = as[Int](_.toIntOption)
|
||||
def asByte(using ev : A =:= String): Parsed[Byte] = as[Byte](_.toByteOption)
|
||||
def asUuid(using ev : A =:= String): Parsed[UUID] = as[UUID](x => Try(UUID.fromString(x)).toOption)
|
||||
def asFloat(using ev : A =:= String): Parsed[Float] = as[Float](_.toFloatOption)
|
||||
def asDouble(using ev : A =:= String): Parsed[Double] = as[Double](_.toDoubleOption)
|
||||
def asDate(using ev: A =:= String): Parsed[LocalDate] = as[LocalDate](x => Try(LocalDate.parse(x)).toOption)
|
||||
|
||||
private def as[B : ClassTag](f : String => Option[B])(using ev : A =:= String): Parsed[B] = parsed.flatMap { str =>
|
||||
f(ev(str)) match {
|
||||
case None => Parsed.fail(s"'$str' cannot be parsed as a ${implicitly[ClassTag[B]].runtimeClass}")
|
||||
case Some(x) => Parsed(x)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def mapError(f : String => String) : Parsed[A] = parsed match {
|
||||
case Left(msg) => Left(f(msg))
|
||||
case right => right
|
||||
}
|
||||
|
||||
def map[B](f: A => B): Parsed[B] = parsed match {
|
||||
case Right(value) => Right(f(value))
|
||||
case Left(err) => Left(err)
|
||||
}
|
||||
def flatMap[B](f : A => Parsed[B]): Parsed[B] = parsed match {
|
||||
case Right(value) => f(value)
|
||||
case Left(err) => Left(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension (request: cask.Request) {
|
||||
|
||||
def formSingleValueRequired(paramName: String): Parsed[String] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.singleValue(Map(paramName -> data), paramName)
|
||||
}
|
||||
def formSingleValueOptional(paramName: String): Parsed[Option[String]] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.optionalValue(Map(paramName -> data), paramName)
|
||||
}
|
||||
|
||||
def formValueAsFileOptional(paramName: String): Parsed[Option[File]] = {
|
||||
val data = formDataForKey(paramName)
|
||||
data.map(_.getFileItem.getFile.toFile).toSeq match {
|
||||
case Seq() => Parsed(None)
|
||||
case Seq(file) => Parsed(Option(file))
|
||||
case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
|
||||
}
|
||||
}
|
||||
|
||||
def formValueAsFileRequired(paramName: String): Parsed[File] = {
|
||||
val data = formDataForKey(paramName)
|
||||
data.map(_.getFileItem.getFile.toFile).toSeq match {
|
||||
case Seq() => Parsed.fail(s"No file form data was submitted for '$paramName'. The submitted form keys were: ${formDataKeys.mkString(",")}")
|
||||
case Seq(file) => Parsed(file)
|
||||
case many => Parsed.fail(s"${many.size} file values set for '$paramName'")
|
||||
}
|
||||
}
|
||||
|
||||
def formManyValues(paramName: String, required: Boolean): Parsed[List[String]] = {
|
||||
val data = formDataForKey(paramName).map(_.getValue).toSeq
|
||||
Parsed.manyValues(Map(paramName -> data), paramName, required)
|
||||
}
|
||||
|
||||
def formData: FormData = FormParserFactory.builder().build().createParser(request.exchange).parseBlocking()
|
||||
|
||||
def formDataKeys: Iterator[String] = formData.iterator().asScala
|
||||
|
||||
def formDataForKey(paramName: String): Iterable[FormData.FormValue] = formData.get(paramName).asScala
|
||||
|
||||
def headerSingleValueOptional(paramName: String): Parsed[Option[String]] = Parsed.optionalValue(request.headers, paramName)
|
||||
def headerSingleValueRequired(paramName: String): Parsed[String] = Parsed.singleValue(request.headers, paramName)
|
||||
|
||||
def headerManyValues(paramName: String, required: Boolean): Parsed[List[String]] = Parsed.manyValues(request.headers, paramName, required)
|
||||
|
||||
def bodyAsString = new String(request.readAllBytes(), "UTF-8")
|
||||
|
||||
def pathValue(index: Int, paramName: String, required : Boolean): Parsed[String] = {
|
||||
request
|
||||
.remainingPathSegments
|
||||
.lift(index) match {
|
||||
case Some(value) => Right(value)
|
||||
case None if required => Left(s"'$paramName'' is a required path parameter at path position $index")
|
||||
case None => Right("")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class ApiResponse(
|
||||
code: Option[Int] = None ,
|
||||
|
||||
`type`: Option[String] = None ,
|
||||
|
||||
message: Option[String] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : ApiResponseData = {
|
||||
ApiResponseData(
|
||||
code = code.getOrElse(0),
|
||||
`type` = `type`.getOrElse(""),
|
||||
message = message.getOrElse("")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ApiResponse{
|
||||
|
||||
given RW[ApiResponse] = ApiResponseData.readWriter.bimap[ApiResponse](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case code extends Fields("code")
|
||||
case `type` extends Fields("`type`")
|
||||
case message extends Fields("message")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** ApiResponseData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class ApiResponseData(
|
||||
code: Int = 0 ,
|
||||
|
||||
`type`: String = "" ,
|
||||
|
||||
message: String = ""
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// code
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// `type`
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// message
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[ApiResponse] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : ApiResponse = {
|
||||
ApiResponse(
|
||||
code = Option(
|
||||
code
|
||||
)
|
||||
,
|
||||
`type` = Option(
|
||||
`type`
|
||||
)
|
||||
,
|
||||
message = Option(
|
||||
message
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object ApiResponseData {
|
||||
|
||||
given readWriter : RW[ApiResponseData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : ApiResponseData = try {
|
||||
read[ApiResponseData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[ApiResponseData] = try {
|
||||
read[List[ApiResponseData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[ApiResponse]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[ApiResponse]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, ApiResponseData] = try {
|
||||
read[Map[String, ApiResponseData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, ApiResponse]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, ApiResponse]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class Category(
|
||||
id: Option[Long] = None ,
|
||||
|
||||
name: Option[String] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : CategoryData = {
|
||||
CategoryData(
|
||||
id = id.getOrElse(0),
|
||||
name = name.getOrElse("")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Category{
|
||||
|
||||
given RW[Category] = CategoryData.readWriter.bimap[Category](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case id extends Fields("id")
|
||||
case name extends Fields("name")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** CategoryData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class CategoryData(
|
||||
id: Long = 0 ,
|
||||
|
||||
name: String = ""
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// name
|
||||
// validate against pattern '^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$'
|
||||
if (errors.isEmpty || !failFast) {
|
||||
val regex = """^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"""
|
||||
if name == null || !regex.r.matches(name) then
|
||||
errors += ValidationError(path :+ Category.Fields.name, s"value '$name' doesn't match pattern $regex")
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[Category] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : Category = {
|
||||
Category(
|
||||
id = Option(
|
||||
id
|
||||
)
|
||||
,
|
||||
name = Option(
|
||||
name
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object CategoryData {
|
||||
|
||||
given readWriter : RW[CategoryData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : CategoryData = try {
|
||||
read[CategoryData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[CategoryData] = try {
|
||||
read[List[CategoryData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Category]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[Category]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, CategoryData] = try {
|
||||
read[Map[String, CategoryData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Category]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, Category]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import java.time.OffsetDateTime
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class Order(
|
||||
id: Option[Long] = None ,
|
||||
|
||||
petId: Option[Long] = None ,
|
||||
|
||||
quantity: Option[Int] = None ,
|
||||
|
||||
shipDate: Option[OffsetDateTime] = None ,
|
||||
|
||||
/* Order Status */
|
||||
status: Option[Order.StatusEnum] = None ,
|
||||
|
||||
complete: Option[Boolean] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : OrderData = {
|
||||
OrderData(
|
||||
id = id.getOrElse(0),
|
||||
petId = petId.getOrElse(0),
|
||||
quantity = quantity.getOrElse(0),
|
||||
shipDate = shipDate.getOrElse(null),
|
||||
status = status.getOrElse(null),
|
||||
complete = complete.getOrElse(false)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Order{
|
||||
|
||||
given RW[Order] = OrderData.readWriter.bimap[Order](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case id extends Fields("id")
|
||||
case petId extends Fields("petId")
|
||||
case quantity extends Fields("quantity")
|
||||
case shipDate extends Fields("shipDate")
|
||||
case status extends Fields("status")
|
||||
case complete extends Fields("complete")
|
||||
}
|
||||
|
||||
// baseName=status
|
||||
// nameInCamelCase = status
|
||||
enum StatusEnum derives ReadWriter {
|
||||
case placed
|
||||
case approved
|
||||
case delivered
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,245 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import java.time.OffsetDateTime
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** OrderData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class OrderData(
|
||||
id: Long = 0 ,
|
||||
|
||||
petId: Long = 0 ,
|
||||
|
||||
quantity: Int = 0 ,
|
||||
|
||||
shipDate: OffsetDateTime = null ,
|
||||
|
||||
/* Order Status */
|
||||
status: Order.StatusEnum = null ,
|
||||
|
||||
complete: Boolean = false
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// petId
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// quantity
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// shipDate
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// status
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// complete
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[Order] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : Order = {
|
||||
Order(
|
||||
id = Option(
|
||||
id
|
||||
)
|
||||
,
|
||||
petId = Option(
|
||||
petId
|
||||
)
|
||||
,
|
||||
quantity = Option(
|
||||
quantity
|
||||
)
|
||||
,
|
||||
shipDate = Option(
|
||||
shipDate
|
||||
)
|
||||
,
|
||||
status = Option(
|
||||
status
|
||||
)
|
||||
,
|
||||
complete = Option(
|
||||
complete
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object OrderData {
|
||||
|
||||
given readWriter : RW[OrderData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : OrderData = try {
|
||||
read[OrderData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[OrderData] = try {
|
||||
read[List[OrderData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Order]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[Order]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, OrderData] = try {
|
||||
read[Map[String, OrderData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Order]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, Order]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import sample.cask.model.Category
|
||||
import sample.cask.model.Tag
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class Pet(
|
||||
id: Option[Long] = None ,
|
||||
|
||||
category: Option[Category] = None ,
|
||||
|
||||
name: String,
|
||||
|
||||
photoUrls: Seq[String],
|
||||
|
||||
tags: Seq[Tag] = Nil ,
|
||||
|
||||
/* pet status in the store */
|
||||
status: Option[Pet.StatusEnum] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : PetData = {
|
||||
PetData(
|
||||
id = id.getOrElse(0),
|
||||
category = category.map(_.asData).getOrElse(null),
|
||||
name = name,
|
||||
photoUrls = photoUrls,
|
||||
tags = tags.map(_.asData),
|
||||
status = status.getOrElse(null)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Pet{
|
||||
|
||||
given RW[Pet] = PetData.readWriter.bimap[Pet](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case id extends Fields("id")
|
||||
case category extends Fields("category")
|
||||
case name extends Fields("name")
|
||||
case photoUrls extends Fields("photoUrls")
|
||||
case tags extends Fields("tags")
|
||||
case status extends Fields("status")
|
||||
}
|
||||
|
||||
// baseName=status
|
||||
// nameInCamelCase = status
|
||||
enum StatusEnum derives ReadWriter {
|
||||
case available
|
||||
case pending
|
||||
case sold
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import sample.cask.model.Category
|
||||
import sample.cask.model.Tag
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** PetData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class PetData(
|
||||
id: Long = 0 ,
|
||||
|
||||
category: CategoryData = null ,
|
||||
|
||||
name: String,
|
||||
|
||||
photoUrls: Seq[String],
|
||||
|
||||
tags: Seq[TagData] = Nil ,
|
||||
|
||||
/* pet status in the store */
|
||||
status: Pet.StatusEnum = null
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// category
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// validating category
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if category != null then errors ++= category.validationErrors(path :+ Pet.Fields.category, failFast)
|
||||
}
|
||||
|
||||
// ==================
|
||||
// name
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// photoUrls
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// tags
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (errors.isEmpty || !failFast) {
|
||||
if (tags != null) {
|
||||
tags.zipWithIndex.foreach {
|
||||
case (value, i) if errors.isEmpty || !failFast =>
|
||||
errors ++= value.validationErrors(
|
||||
path :+ Pet.Fields.tags :+ Field(i.toString),
|
||||
failFast)
|
||||
case (value, i) =>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ==================
|
||||
// status
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[Pet] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : Pet = {
|
||||
Pet(
|
||||
id = Option(
|
||||
id
|
||||
)
|
||||
,
|
||||
category = Option(
|
||||
category
|
||||
)
|
||||
.map(_.asModel),
|
||||
name =
|
||||
name
|
||||
|
||||
,
|
||||
photoUrls =
|
||||
photoUrls
|
||||
|
||||
,
|
||||
tags =
|
||||
tags
|
||||
|
||||
.map(_.asModel),
|
||||
status = Option(
|
||||
status
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object PetData {
|
||||
|
||||
given readWriter : RW[PetData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : PetData = try {
|
||||
read[PetData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[PetData] = try {
|
||||
read[List[PetData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Pet]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[Pet]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, PetData] = try {
|
||||
read[Map[String, PetData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Pet]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, Pet]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class Tag(
|
||||
id: Option[Long] = None ,
|
||||
|
||||
name: Option[String] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : TagData = {
|
||||
TagData(
|
||||
id = id.getOrElse(0),
|
||||
name = name.getOrElse("")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Tag{
|
||||
|
||||
given RW[Tag] = TagData.readWriter.bimap[Tag](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case id extends Fields("id")
|
||||
case name extends Fields("name")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** TagData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class TagData(
|
||||
id: Long = 0 ,
|
||||
|
||||
name: String = ""
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// name
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[Tag] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : Tag = {
|
||||
Tag(
|
||||
id = Option(
|
||||
id
|
||||
)
|
||||
,
|
||||
name = Option(
|
||||
name
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object TagData {
|
||||
|
||||
given readWriter : RW[TagData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : TagData = try {
|
||||
read[TagData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[TagData] = try {
|
||||
read[List[TagData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[Tag]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[Tag]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, TagData] = try {
|
||||
read[Map[String, TagData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, Tag]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, Tag]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using model.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
case class User(
|
||||
id: Option[Long] = None ,
|
||||
|
||||
username: Option[String] = None ,
|
||||
|
||||
firstName: Option[String] = None ,
|
||||
|
||||
lastName: Option[String] = None ,
|
||||
|
||||
email: Option[String] = None ,
|
||||
|
||||
password: Option[String] = None ,
|
||||
|
||||
phone: Option[String] = None ,
|
||||
|
||||
/* User Status */
|
||||
userStatus: Option[Int] = None
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = asData.asJson
|
||||
|
||||
def asData : UserData = {
|
||||
UserData(
|
||||
id = id.getOrElse(0),
|
||||
username = username.getOrElse(""),
|
||||
firstName = firstName.getOrElse(""),
|
||||
lastName = lastName.getOrElse(""),
|
||||
email = email.getOrElse(""),
|
||||
password = password.getOrElse(""),
|
||||
phone = phone.getOrElse(""),
|
||||
userStatus = userStatus.getOrElse(0)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object User{
|
||||
|
||||
given RW[User] = UserData.readWriter.bimap[User](_.asData, _.asModel)
|
||||
|
||||
enum Fields(fieldName : String) extends Field(fieldName) {
|
||||
case id extends Fields("id")
|
||||
case username extends Fields("username")
|
||||
case firstName extends Fields("firstName")
|
||||
case lastName extends Fields("lastName")
|
||||
case email extends Fields("email")
|
||||
case password extends Fields("password")
|
||||
case phone extends Fields("phone")
|
||||
case userStatus extends Fields("userStatus")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
// this model was generated using modelData.mustache
|
||||
package sample.cask.model
|
||||
import scala.util.control.NonFatal
|
||||
import scala.util.*
|
||||
|
||||
// see https://com-lihaoyi.github.io/upickle/
|
||||
import upickle.default.{ReadWriter => RW, macroRW}
|
||||
import upickle.default.*
|
||||
|
||||
/** UserData a data transfer object, primarily for simple json serialisation.
|
||||
* It has no validation - there may be nulls, values out of range, etc
|
||||
*/
|
||||
case class UserData(
|
||||
id: Long = 0 ,
|
||||
|
||||
username: String = "" ,
|
||||
|
||||
firstName: String = "" ,
|
||||
|
||||
lastName: String = "" ,
|
||||
|
||||
email: String = "" ,
|
||||
|
||||
password: String = "" ,
|
||||
|
||||
phone: String = "" ,
|
||||
|
||||
/* User Status */
|
||||
userStatus: Int = 0
|
||||
|
||||
) {
|
||||
|
||||
def asJson: String = write(this)
|
||||
|
||||
def validationErrors(path : Seq[Field], failFast : Boolean) : Seq[ValidationError] = {
|
||||
val errors = scala.collection.mutable.ListBuffer[ValidationError]()
|
||||
// ==================
|
||||
// id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// username
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// firstName
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// lastName
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// email
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// password
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// phone
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// userStatus
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
errors.toSeq
|
||||
}
|
||||
|
||||
def validated(failFast : Boolean = false) : scala.util.Try[User] = {
|
||||
validationErrors(Vector(), failFast) match {
|
||||
case Seq() => Success(asModel)
|
||||
case first +: theRest => Failure(ValidationErrors(first, theRest))
|
||||
}
|
||||
}
|
||||
|
||||
/** use 'validated' to check validation */
|
||||
def asModel : User = {
|
||||
User(
|
||||
id = Option(
|
||||
id
|
||||
)
|
||||
,
|
||||
username = Option(
|
||||
username
|
||||
)
|
||||
,
|
||||
firstName = Option(
|
||||
firstName
|
||||
)
|
||||
,
|
||||
lastName = Option(
|
||||
lastName
|
||||
)
|
||||
,
|
||||
email = Option(
|
||||
email
|
||||
)
|
||||
,
|
||||
password = Option(
|
||||
password
|
||||
)
|
||||
,
|
||||
phone = Option(
|
||||
phone
|
||||
)
|
||||
,
|
||||
userStatus = Option(
|
||||
userStatus
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object UserData {
|
||||
|
||||
given readWriter : RW[UserData] = macroRW
|
||||
|
||||
def fromJsonString(jason : String) : UserData = try {
|
||||
read[UserData](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason': $e")
|
||||
}
|
||||
|
||||
def manyFromJsonString(jason : String) : Seq[UserData] = try {
|
||||
read[List[UserData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as list: $e")
|
||||
}
|
||||
|
||||
def manyFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Seq[User]] = {
|
||||
Try(manyFromJsonString(jason)).flatMap { list =>
|
||||
list.zipWithIndex.foldLeft(Try(Vector[User]())) {
|
||||
case (Success(list), (next, i)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(list :+ ok)
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $i: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def mapFromJsonString(jason : String) : Map[String, UserData] = try {
|
||||
read[Map[String, UserData]](jason)
|
||||
} catch {
|
||||
case NonFatal(e) => sys.error(s"Error parsing json '$jason' as map: $e")
|
||||
}
|
||||
|
||||
|
||||
def mapFromJsonStringValidated(jason : String, failFast : Boolean = false) : Try[Map[String, User]] = {
|
||||
Try(mapFromJsonString(jason)).flatMap { map =>
|
||||
map.foldLeft(Try(Map[String, User]())) {
|
||||
case (Success(map), (key, next)) =>
|
||||
next.validated(failFast) match {
|
||||
case Success(ok) => Success(map.updated(key, ok))
|
||||
case Failure(err) => Failure(new Exception(s"Validation error on element $key: ${err.getMessage}", err))
|
||||
}
|
||||
case (fail, _) => fail
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
package sample.cask.model
|
||||
|
||||
// model package
|
||||
import upickle.default._
|
||||
import java.time.*
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* This base class lets us refer to fields in exceptions
|
||||
*/
|
||||
class Field(val name : String)
|
||||
|
||||
final case class ValidationErrors(
|
||||
first: ValidationError,
|
||||
remaining: Seq[ValidationError],
|
||||
message: String
|
||||
) extends Exception(message)
|
||||
|
||||
object ValidationErrors {
|
||||
def apply(first: ValidationError, remaining: Seq[ValidationError]) = {
|
||||
val noun = if remaining.isEmpty then "error" else "errors"
|
||||
new ValidationErrors(
|
||||
first,
|
||||
remaining,
|
||||
remaining.mkString(s"${remaining.size + 1} $noun found: ${first}", "\n\t", "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final case class ValidationError(path : Seq[Field], message : String) extends Exception(message) {
|
||||
override def toString = s"ValidationError for ${path.mkString(".")}: $message"
|
||||
}
|
||||
|
||||
given ReadWriter[ZonedDateTime] = readwriter[String].bimap[ZonedDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => ZonedDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[LocalDateTime] = readwriter[String].bimap[LocalDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => LocalDateTime.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[LocalDate] = readwriter[String].bimap[LocalDate](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => LocalDate.parse(str, DateTimeFormatter.ISO_INSTANT))
|
||||
|
||||
given ReadWriter[OffsetDateTime] = readwriter[String].bimap[OffsetDateTime](
|
||||
zonedDateTime => DateTimeFormatter.ISO_INSTANT.format(zonedDateTime),
|
||||
str => scala.util.Try(OffsetDateTime.parse(str, DateTimeFormatter.ISO_DATE_TIME)).getOrElse(
|
||||
OffsetDateTime.parse(str, DateTimeFormatter.ISO_INSTANT)
|
||||
)
|
||||
)
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
*
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator.
|
||||
*
|
||||
* https://openapi-generator.tech
|
||||
*/
|
||||
|
||||
package cask.groupId.server
|
||||
|
||||
def box(str: String): String = {
|
||||
val lines = str.linesIterator.toList
|
||||
val maxLen = (0 +: lines.map(_.length)).max
|
||||
val boxed = lines.map { line =>
|
||||
s" | ${line.padTo(maxLen, ' ')} |"
|
||||
}
|
||||
val bar = " +-" + ("-" * maxLen) + "-+"
|
||||
(bar +: boxed :+ bar).mkString("\n")
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class ApiResponseTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"ApiResponse.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(ApiResponseData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = ApiResponseData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class CategoryTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"Category.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(CategoryData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = CategoryData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class OrderTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"Order.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(OrderData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = OrderData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
import sample.cask.model.Category
|
||||
import sample.cask.model.Tag
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class PetTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"Pet.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(PetData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = PetData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class TagTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"Tag.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(TagData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = TagData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* OpenAPI spec version: 1.0.0
|
||||
* Contact: team@openapitools.org
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
*/
|
||||
|
||||
// this model was generated using modelTest.mustache
|
||||
package sample.cask.model
|
||||
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
import scala.util.*
|
||||
|
||||
class UserTest extends AnyWordSpec with Matchers {
|
||||
|
||||
"User.fromJson" should {
|
||||
"""not parse invalid json""" in {
|
||||
val Failure(err) = Try(UserData.fromJsonString("invalid jason"))
|
||||
err.getMessage should startWith ("Error parsing json 'invalid jason'")
|
||||
}
|
||||
"""parse """ ignore {
|
||||
val Failure(err : ValidationErrors) = UserData.fromJsonString("""""").validated()
|
||||
|
||||
sys.error("TODO")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user