diff --git a/.github/workflows/samples-scala.yaml b/.github/workflows/samples-scala.yaml index 0d71f743d67..dfb11048bd3 100644 --- a/.github/workflows/samples-scala.yaml +++ b/.github/workflows/samples-scala.yaml @@ -27,6 +27,7 @@ jobs: - samples/server/petstore/scala-lagom-server - samples/server/petstore/scala-play-server - samples/server/petstore/scala-akka-http-server + - samples/server/petstore/scala-pekko-http-server - samples/server/petstore/scalatra - samples/server/petstore/scala-finch # cannot be tested with jdk11 steps: diff --git a/bin/configs/scala-pekko-http-server.yaml b/bin/configs/scala-pekko-http-server.yaml new file mode 100644 index 00000000000..1758feaf615 --- /dev/null +++ b/bin/configs/scala-pekko-http-server.yaml @@ -0,0 +1,7 @@ +generatorName: scala-akka-http-server +outputDir: samples/server/petstore/scala-pekko-http-server +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/scala-akka-http-server +additionalProperties: + artifactId: openapi-scala-pekko-http-server + useApachePekko: true diff --git a/docs/generators/scala-akka-http-server.md b/docs/generators/scala-akka-http-server.md index df3cef97920..8419c093758 100644 --- a/docs/generators/scala-akka-http-server.md +++ b/docs/generators/scala-akka-http-server.md @@ -37,6 +37,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |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| +|useApachePekko|Use apache pekko-http instead of akka-http.| |false| ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java index dd50aec4889..7ec9dd1c694 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java @@ -44,15 +44,27 @@ public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements protected String akkaHttpVersion; protected boolean generateAsManagedSources; + protected boolean useApachePekko; public static final String AKKA_HTTP_VERSION = "akkaHttpVersion"; public static final String AKKA_HTTP_VERSION_DESC = "The version of akka-http"; public static final String DEFAULT_AKKA_HTTP_VERSION = "10.1.10"; + public static final String DEFAULT_PEKKO_HTTP_VERSION = "1.0.0"; public static final String GENERATE_AS_MANAGED_SOURCES = "asManagedSources"; public static final String GENERATE_AS_MANAGED_SOURCES_DESC = "Resulting files cab be used as managed resources. No build files or default controllers will be generated"; public static final boolean DEFAULT_GENERATE_AS_MANAGED_SOURCES = false; + public static final String USE_APACHE_PEKKO = "useApachePekko"; + public static final String USE_APACHE_PEKKO_DESC = "Use apache pekko-http instead of akka-http."; + public static final boolean DEFAULT_USE_APACHE_PEKKO = false; + + // scala-akka-http-server specific properties + private static final String IS_10_1_10_PLUS = "akkaHttp10_1_10_plus"; + private static final String AKKA_IMPORT_GROUP_ID = "akkaImportGroupId"; + + private static final Pattern akkaVersionPattern = Pattern.compile("([0-9]+)(\\.([0-9]+))?(\\.([0-9]+))?(.\\+)?"); + final Logger LOGGER = LoggerFactory.getLogger(ScalaAkkaHttpServerCodegen.class); public CodegenType getTag() { @@ -108,6 +120,7 @@ public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements invokerPackage = "org.openapitools.server"; akkaHttpVersion = DEFAULT_AKKA_HTTP_VERSION; generateAsManagedSources = DEFAULT_GENERATE_AS_MANAGED_SOURCES; + useApachePekko = DEFAULT_USE_APACHE_PEKKO; setReservedWordsLowerCase( Arrays.asList( @@ -124,6 +137,7 @@ public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements cliOptions.add(CliOption.newString(CodegenConstants.ARTIFACT_VERSION, CodegenConstants.ARTIFACT_VERSION_DESC).defaultValue(artifactVersion)); cliOptions.add(CliOption.newString(AKKA_HTTP_VERSION, AKKA_HTTP_VERSION_DESC).defaultValue(akkaHttpVersion)); cliOptions.add(CliOption.newBoolean(GENERATE_AS_MANAGED_SOURCES, GENERATE_AS_MANAGED_SOURCES_DESC).defaultValue(Boolean.valueOf(DEFAULT_GENERATE_AS_MANAGED_SOURCES).toString())); + cliOptions.add(CliOption.newBoolean(USE_APACHE_PEKKO, USE_APACHE_PEKKO_DESC).defaultValue(Boolean.valueOf(DEFAULT_USE_APACHE_PEKKO).toString())); importMapping.remove("Seq"); importMapping.remove("List"); @@ -181,14 +195,27 @@ public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); } + if (additionalProperties.containsKey(USE_APACHE_PEKKO)) { + useApachePekko = Boolean.parseBoolean(additionalProperties.get(USE_APACHE_PEKKO).toString()); + } else { + additionalProperties.put(USE_APACHE_PEKKO, useApachePekko); + } + if (additionalProperties.containsKey(AKKA_HTTP_VERSION)) { akkaHttpVersion = (String) additionalProperties.get(AKKA_HTTP_VERSION); } else { - additionalProperties.put(AKKA_HTTP_VERSION, akkaHttpVersion); + String version = useApachePekko ? DEFAULT_PEKKO_HTTP_VERSION : DEFAULT_AKKA_HTTP_VERSION; + additionalProperties.put(AKKA_HTTP_VERSION, version); } - parseAkkaHttpVersion(); - + if (useApachePekko) { + additionalProperties.put(IS_10_1_10_PLUS, true); // Pekko HTTP is a fork of Akka HTTP 10.2.x + additionalProperties.put(USE_APACHE_PEKKO, true); + additionalProperties.put(AKKA_IMPORT_GROUP_ID, "org.apache.pekko"); + } else { + additionalProperties.put(AKKA_IMPORT_GROUP_ID, "akka"); + parseAkkaHttpVersion(); + } if (additionalProperties.containsKey(GENERATE_AS_MANAGED_SOURCES)) { generateAsManagedSources = Boolean.parseBoolean(additionalProperties.get(GENERATE_AS_MANAGED_SOURCES).toString()); @@ -210,12 +237,8 @@ public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), "MultipartDirectives.scala")); } - private static final String IS_10_1_10_PLUS = "akkaHttp10_1_10_plus"; - private boolean is10_1_10AndAbove = false; - - private static final Pattern akkaVersionPattern = Pattern.compile("([0-9]+)(\\.([0-9]+))?(\\.([0-9]+))?(.\\+)?"); - private void parseAkkaHttpVersion() { + boolean is10_1_10AndAbove = false; Matcher matcher = akkaVersionPattern.matcher(akkaHttpVersion); if (matcher.matches()) { String majorS = matcher.group(1); diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/api.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/api.mustache index 27cb2cb8d4f..57a72c7387e 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/api.mustache @@ -1,15 +1,15 @@ package {{package}} -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.model.StatusCodes -{{^pathMatcherPatterns.isEmpty}}import akka.http.scaladsl.server.{PathMatcher, PathMatcher1} +import {{akkaImportGroupId}}.http.scaladsl.server.Directives._ +import {{akkaImportGroupId}}.http.scaladsl.server.Route +import {{akkaImportGroupId}}.http.scaladsl.model.StatusCodes +{{^pathMatcherPatterns.isEmpty}}import {{akkaImportGroupId}}.http.scaladsl.server.{PathMatcher, PathMatcher1} {{/pathMatcherPatterns.isEmpty}} -{{#hasMarshalling}}import akka.http.scaladsl.marshalling.ToEntityMarshaller -import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller -import akka.http.scaladsl.unmarshalling.FromStringUnmarshaller +{{#hasMarshalling}}import {{akkaImportGroupId}}.http.scaladsl.marshalling.ToEntityMarshaller +import {{akkaImportGroupId}}.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import {{akkaImportGroupId}}.http.scaladsl.unmarshalling.FromStringUnmarshaller {{/hasMarshalling}} -{{#hasCookieParams}}import akka.http.scaladsl.model.headers.HttpCookiePair +{{#hasCookieParams}}import {{akkaImportGroupId}}.http.scaladsl.model.headers.HttpCookiePair {{/hasCookieParams}} import {{invokerPackage}}.AkkaHttpHelper._ {{#hasMultipart}}import {{invokerPackage}}.StringDirectives @@ -20,8 +20,8 @@ import {{invokerPackage}}.PartsAndFiles {{#imports}}import {{import}} {{/imports}} {{#hasMultipart}}import scala.util.Try -import akka.http.scaladsl.server.MalformedRequestContentRejection -import akka.http.scaladsl.server.directives.FileInfo +import {{akkaImportGroupId}}.http.scaladsl.server.MalformedRequestContentRejection +import {{akkaImportGroupId}}.http.scaladsl.server.directives.FileInfo {{/hasMultipart}} diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/build.sbt.mustache index d4b57b67614..f2f036b613f 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/build.sbt.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/build.sbt.mustache @@ -3,7 +3,9 @@ name := "{{artifactId}}" organization := "{{groupId}}" scalaVersion := "2.12.8" -libraryDependencies ++= Seq( +libraryDependencies ++= Seq({{#useApachePekko}} + "org.apache.pekko" %% "pekko-stream" % "{{akkaHttpVersion}}", + "org.apache.pekko" %% "pekko-http" % "{{akkaHttpVersion}}"{{/useApachePekko}}{{^useApachePekko}} "com.typesafe.akka" %% "akka-stream" % "2.5.21", - "com.typesafe.akka" %% "akka-http" % "{{akkaHttpVersion}}" + "com.typesafe.akka" %% "akka-http" % "{{akkaHttpVersion}}"{{/useApachePekko}} ) diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/controller.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/controller.mustache index 7aa237fa6e6..b67a75b83c1 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/controller.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/controller.mustache @@ -1,12 +1,12 @@ package {{invokerPackage}} -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Route +import {{akkaImportGroupId}}.http.scaladsl.Http +import {{akkaImportGroupId}}.http.scaladsl.server.Route {{#apiInfo}}{{#apis}}{{#operations}}import {{package}}.{{classname}} {{/operations}}{{/apis}}{{/apiInfo}} -import akka.http.scaladsl.server.Directives._ -import akka.actor.ActorSystem -import akka.stream.ActorMaterializer +import {{akkaImportGroupId}}.http.scaladsl.server.Directives._ +import {{akkaImportGroupId}}.actor.ActorSystem +import {{akkaImportGroupId}}.stream.ActorMaterializer class Controller({{#apiInfo}}{{#apis}}{{#operations}}{{classVarName}}: {{classname}}{{^-last}}, {{/-last}}{{/operations}}{{/apis}}{{/apiInfo}})(implicit system: ActorSystem, materializer: ActorMaterializer) { diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/helper.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/helper.mustache index 8aa3c0f8c26..ffd7090e2d4 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/helper.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/helper.mustache @@ -1,7 +1,7 @@ package {{invokerPackage}} -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{PathMatcher, PathMatcher1} +import {{akkaImportGroupId}}.http.scaladsl.server.Directives._ +import {{akkaImportGroupId}}.http.scaladsl.server.{PathMatcher, PathMatcher1} import scala.util.{Failure, Success, Try} import scala.util.control.NoStackTrace diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache index 6e802204c57..97427179ff2 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache @@ -3,22 +3,22 @@ package {{invokerPackage}} import java.io.File import java.nio.file.Files -import akka.annotation.ApiMayChange -import akka.http.scaladsl.model.Multipart.FormData -import akka.http.scaladsl.model.{ContentType, HttpEntity, Multipart} -import akka.http.scaladsl.server.Directive1 -import akka.http.scaladsl.server.directives._ -import akka.stream.Materializer -import akka.stream.scaladsl._ +import {{akkaImportGroupId}}.annotation.ApiMayChange +import {{akkaImportGroupId}}.http.scaladsl.model.Multipart.FormData +import {{akkaImportGroupId}}.http.scaladsl.model.{ContentType, HttpEntity, Multipart} +import {{akkaImportGroupId}}.http.scaladsl.server.Directive1 +import {{akkaImportGroupId}}.http.scaladsl.server.directives._ +import {{akkaImportGroupId}}.stream.Materializer +import {{akkaImportGroupId}}.stream.scaladsl._ import scala.collection.immutable import scala.concurrent.{ExecutionContextExecutor, Future} trait MultipartDirectives { - import akka.http.scaladsl.server.directives.BasicDirectives._ - import akka.http.scaladsl.server.directives.FutureDirectives._ - import akka.http.scaladsl.server.directives.MarshallingDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.BasicDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.FutureDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.MarshallingDirectives._ @ApiMayChange def formAndFiles(fileFields: FileField*): Directive1[PartsAndFiles] = diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/stringDirectives.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/stringDirectives.mustache index 5640a9d4549..5ee2f6035f9 100644 --- a/modules/openapi-generator/src/main/resources/scala-akka-http-server/stringDirectives.mustache +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/stringDirectives.mustache @@ -1,9 +1,9 @@ package {{invokerPackage}} -import akka.http.scaladsl.common._ -import akka.http.scaladsl.server.{Directive, Directive0, Directive1, InvalidRequiredValueForQueryParamRejection, MalformedFormFieldRejection, MissingFormFieldRejection, MissingQueryParamRejection, UnsupportedRequestContentTypeRejection} -import akka.http.scaladsl.server.directives.BasicDirectives -import akka.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException +import {{akkaImportGroupId}}.http.scaladsl.common._ +import {{akkaImportGroupId}}.http.scaladsl.server.{Directive, Directive0, Directive1, InvalidRequiredValueForQueryParamRejection, MalformedFormFieldRejection, MissingFormFieldRejection, MissingQueryParamRejection, UnsupportedRequestContentTypeRejection} +import {{akkaImportGroupId}}.http.scaladsl.server.directives.BasicDirectives +import {{akkaImportGroupId}}.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException import scala.concurrent.Future import scala.util.{Failure, Success} @@ -48,10 +48,10 @@ object StringDirectives extends StringDirectives { def apply(value: A): B = f(value) } - import akka.http.scaladsl.server.directives.BasicDirectives._ - import akka.http.scaladsl.server.directives.FutureDirectives._ - import akka.http.scaladsl.server.directives.RouteDirectives._ - import akka.http.scaladsl.unmarshalling._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.BasicDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.FutureDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.server.directives.RouteDirectives._ + import {{akkaImportGroupId}}.http.scaladsl.unmarshalling._ type FSU[T] = FromStringUnmarshaller[T] type FSOU[T] = Unmarshaller[Option[String], T] @@ -112,8 +112,8 @@ object StringDirectives extends StringDirectives { //////////////////// tuple support //////////////////// - import akka.http.scaladsl.server.util.BinaryPolyFunc - import akka.http.scaladsl.server.util.TupleOps._ + import {{akkaImportGroupId}}.http.scaladsl.server.util.BinaryPolyFunc + import {{akkaImportGroupId}}.http.scaladsl.server.util.TupleOps._ implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertStringDefAndConcatenate.type]): StringDefAux[T, fold.Out] = stringDef[T, fold.Out](fold(BasicDirectives.pass, _)) diff --git a/samples/server/petstore/scala-pekko-http-server/.openapi-generator-ignore b/samples/server/petstore/scala-pekko-http-server/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/scala-pekko-http-server/.openapi-generator/FILES b/samples/server/petstore/scala-pekko-http-server/.openapi-generator/FILES new file mode 100644 index 00000000000..cfcbab175e5 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/.openapi-generator/FILES @@ -0,0 +1,15 @@ +README.md +build.sbt +src/main/scala/org/openapitools/server/AkkaHttpHelper.scala +src/main/scala/org/openapitools/server/Controller.scala +src/main/scala/org/openapitools/server/MultipartDirectives.scala +src/main/scala/org/openapitools/server/StringDirectives.scala +src/main/scala/org/openapitools/server/api/PetApi.scala +src/main/scala/org/openapitools/server/api/StoreApi.scala +src/main/scala/org/openapitools/server/api/UserApi.scala +src/main/scala/org/openapitools/server/model/ApiResponse.scala +src/main/scala/org/openapitools/server/model/Category.scala +src/main/scala/org/openapitools/server/model/Order.scala +src/main/scala/org/openapitools/server/model/Pet.scala +src/main/scala/org/openapitools/server/model/Tag.scala +src/main/scala/org/openapitools/server/model/User.scala diff --git a/samples/server/petstore/scala-pekko-http-server/.openapi-generator/VERSION b/samples/server/petstore/scala-pekko-http-server/.openapi-generator/VERSION new file mode 100644 index 00000000000..757e6740040 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.0.0-SNAPSHOT \ No newline at end of file diff --git a/samples/server/petstore/scala-pekko-http-server/README.md b/samples/server/petstore/scala-pekko-http-server/README.md new file mode 100644 index 00000000000..a1c3b50c09b --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/README.md @@ -0,0 +1,54 @@ +# 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. + + + ## API + + ### Pet + + |Name|Role| + |----|----| + |`org.openapitools.server.api.PetController`|akka-http API controller| + |`org.openapitools.server.api.PetApi`|Representing trait| + |`org.openapitools.server.api.PetApiImpl`|Default implementation| + + * `POST /v2/pet` - Add a new pet to the store + * `DELETE /v2/pet/{petId}` - Deletes a pet + * `GET /v2/pet/findByStatus?status=[value]` - Finds Pets by status + * `GET /v2/pet/findByTags?tags=[value]` - Finds Pets by tags + * `GET /v2/pet/{petId}` - Find pet by ID + * `PUT /v2/pet` - Update an existing pet + * `POST /v2/pet/{petId}` - Updates a pet in the store with form data + * `POST /v2/pet/{petId}/uploadImage` - uploads an image + + ### Store + + |Name|Role| + |----|----| + |`org.openapitools.server.api.StoreController`|akka-http API controller| + |`org.openapitools.server.api.StoreApi`|Representing trait| + |`org.openapitools.server.api.StoreApiImpl`|Default implementation| + + * `DELETE /v2/store/order/{orderId}` - Delete purchase order by ID + * `GET /v2/store/inventory` - Returns pet inventories by status + * `GET /v2/store/order/{orderId}` - Find purchase order by ID + * `POST /v2/store/order` - Place an order for a pet + + ### User + + |Name|Role| + |----|----| + |`org.openapitools.server.api.UserController`|akka-http API controller| + |`org.openapitools.server.api.UserApi`|Representing trait| + |`org.openapitools.server.api.UserApiImpl`|Default implementation| + + * `POST /v2/user` - Create user + * `POST /v2/user/createWithArray` - Creates list of users with given input array + * `POST /v2/user/createWithList` - Creates list of users with given input array + * `DELETE /v2/user/{username}` - Delete user + * `GET /v2/user/{username}` - Get user by user name + * `GET /v2/user/login?username=[value]&password=[value]` - Logs user into the system + * `GET /v2/user/logout` - Logs out current logged in user session + * `PUT /v2/user/{username}` - Updated user + diff --git a/samples/server/petstore/scala-pekko-http-server/build.sbt b/samples/server/petstore/scala-pekko-http-server/build.sbt new file mode 100644 index 00000000000..8e54aeb598f --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/build.sbt @@ -0,0 +1,9 @@ +version := "1.0.0" +name := "openapi-scala-pekko-http-server" +organization := "org.openapitools" +scalaVersion := "2.12.8" + +libraryDependencies ++= Seq( + "org.apache.pekko" %% "pekko-stream" % "1.0.0", + "org.apache.pekko" %% "pekko-http" % "1.0.0" +) diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala new file mode 100644 index 00000000000..ba240dedd8a --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala @@ -0,0 +1,34 @@ +package org.openapitools.server + +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.{PathMatcher, PathMatcher1} +import scala.util.{Failure, Success, Try} +import scala.util.control.NoStackTrace + +object AkkaHttpHelper { + def optToTry[T](opt: Option[T], err: => String): Try[T] = + opt.map[Try[T]](Success(_)) getOrElse Failure(new RuntimeException(err) with NoStackTrace) + + /** + * A PathMatcher that matches and extracts a Float value. The matched string representation is the pure decimal, + * optionally signed form of a float value, i.e. without exponent. + * + * @group pathmatcher + */ + val FloatNumber: PathMatcher1[Float] = + PathMatcher("""[+-]?\d*\.?\d*""".r) flatMap { string => + try Some(java.lang.Float.parseFloat(string)) + catch { case _: NumberFormatException => None } + } + + /** + * A PathMatcher that matches and extracts a Boolean value. + * + * @group pathmatcher + */ + val Boolean: PathMatcher1[Boolean] = + Segment.flatMap { string => + try Some(string.toBoolean) + catch { case _: IllegalArgumentException => None } + } +} diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/Controller.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/Controller.scala new file mode 100644 index 00000000000..58b99cbe0ee --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/Controller.scala @@ -0,0 +1,18 @@ +package org.openapitools.server + +import org.apache.pekko.http.scaladsl.Http +import org.apache.pekko.http.scaladsl.server.Route +import org.openapitools.server.api.PetApi +import org.openapitools.server.api.StoreApi +import org.openapitools.server.api.UserApi + +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.actor.ActorSystem +import org.apache.pekko.stream.ActorMaterializer + +class Controller(pet: PetApi, store: StoreApi, user: UserApi)(implicit system: ActorSystem, materializer: ActorMaterializer) { + + lazy val routes: Route = pet.route ~ store.route ~ user.route + + Http().bindAndHandle(routes, "0.0.0.0", 9000) +} \ No newline at end of file diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala new file mode 100644 index 00000000000..87c1ef2c852 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/MultipartDirectives.scala @@ -0,0 +1,89 @@ +package org.openapitools.server + +import java.io.File +import java.nio.file.Files + +import org.apache.pekko.annotation.ApiMayChange +import org.apache.pekko.http.scaladsl.model.Multipart.FormData +import org.apache.pekko.http.scaladsl.model.{ContentType, HttpEntity, Multipart} +import org.apache.pekko.http.scaladsl.server.Directive1 +import org.apache.pekko.http.scaladsl.server.directives._ +import org.apache.pekko.stream.Materializer +import org.apache.pekko.stream.scaladsl._ + +import scala.collection.immutable +import scala.concurrent.{ExecutionContextExecutor, Future} + +trait MultipartDirectives { + + import org.apache.pekko.http.scaladsl.server.directives.BasicDirectives._ + import org.apache.pekko.http.scaladsl.server.directives.FutureDirectives._ + import org.apache.pekko.http.scaladsl.server.directives.MarshallingDirectives._ + + @ApiMayChange + def formAndFiles(fileFields: FileField*): Directive1[PartsAndFiles] = + entity(as[Multipart.FormData]).flatMap { + formData => + extractRequestContext.flatMap { ctx => + implicit val mat: Materializer = ctx.materializer + implicit val ec: ExecutionContextExecutor = ctx.executionContext + + val uploadingSink: Sink[FormData.BodyPart, Future[PartsAndFiles]] = + Sink.foldAsync[PartsAndFiles, Multipart.FormData.BodyPart](PartsAndFiles.Empty) { + (acc, part) => + def discard(p: Multipart.FormData.BodyPart): Future[PartsAndFiles] = { + p.entity.discardBytes() + Future.successful(acc) + } + + part.filename.map { + fileName => + fileFields.find(_.fieldName == part.name) + .map { + case FileField(_, destFn) => + val fileInfo = FileInfo(part.name, fileName, part.entity.contentType) + val dest = destFn(fileInfo) + + part.entity.dataBytes.runWith(FileIO.toPath(dest.toPath)).map { _ => + acc.addFile(fileInfo, dest) + } + }.getOrElse(discard(part)) + } getOrElse { + part.entity match { + case HttpEntity.Strict(ct: ContentType.NonBinary, data) => + val charsetName = ct.charset.nioCharset.name + val partContent = data.decodeString(charsetName) + + Future.successful(acc.addForm(part.name, partContent)) + case _ => + discard(part) + } + } + } + + val uploadedF = formData.parts.runWith(uploadingSink) + + onSuccess(uploadedF) + } + } +} + +object MultipartDirectives extends MultipartDirectives with FileUploadDirectives { + val tempFileFromFileInfo: FileInfo => File = { + file: FileInfo => Files.createTempFile(file.fileName, ".tmp").toFile() + } +} + +final case class FileField(fieldName: String, fileNameF: FileInfo => File = MultipartDirectives.tempFileFromFileInfo) + +final case class PartsAndFiles(form: immutable.Map[String, String], files: Map[String, (FileInfo, File)]) { + def addForm(fieldName: String, content: String): PartsAndFiles = this.copy(form.updated(fieldName, content)) + + def addFile(info: FileInfo, file: File): PartsAndFiles = this.copy( + files = files.updated(info.fieldName, (info, file)) + ) +} + +object PartsAndFiles { + val Empty: PartsAndFiles = PartsAndFiles(immutable.Map.empty, immutable.Map.empty) +} \ No newline at end of file diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/StringDirectives.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/StringDirectives.scala new file mode 100644 index 00000000000..6e83b56aa6f --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/StringDirectives.scala @@ -0,0 +1,126 @@ +package org.openapitools.server + +import org.apache.pekko.http.scaladsl.common._ +import org.apache.pekko.http.scaladsl.server.{Directive, Directive0, Directive1, InvalidRequiredValueForQueryParamRejection, MalformedFormFieldRejection, MissingFormFieldRejection, MissingQueryParamRejection, UnsupportedRequestContentTypeRejection} +import org.apache.pekko.http.scaladsl.server.directives.BasicDirectives +import org.apache.pekko.http.scaladsl.unmarshalling.Unmarshaller.UnsupportedContentTypeException + +import scala.concurrent.Future +import scala.util.{Failure, Success} + +trait StringDirectives { + implicit def _symbol2NR(symbol: Symbol): NameReceptacle[String] = new NameReceptacle[String](symbol.name) + implicit def _string2NR(string: String): NameReceptacle[String] = new NameReceptacle[String](string) + + import StringDirectives._ + type StringValueProvider = Map[String, String] + + def stringField(pdm: StringMagnet): pdm.Out = pdm() + + def stringFields(pdm: StringMagnet): pdm.Out = pdm() + +} + +object StringDirectives extends StringDirectives { + + sealed trait StringMagnet { + type Out + def apply(): Out + } + object StringMagnet { + implicit def apply[T](value: T)(implicit sdef: StringDef[T]): StringMagnet { type Out = sdef.Out } = + new StringMagnet { + type Out = sdef.Out + def apply(): sdef.Out = sdef(value) + } + } + + type StringDefAux[A, B] = StringDef[A] { type Out = B } + sealed trait StringDef[T] { + type Out + def apply(value: T): Out + } + object StringDef { + protected def stringDef[A, B](f: A => B): StringDefAux[A, B] = + new StringDef[A] { + type Out = B + + def apply(value: A): B = f(value) + } + + import org.apache.pekko.http.scaladsl.server.directives.BasicDirectives._ + import org.apache.pekko.http.scaladsl.server.directives.FutureDirectives._ + import org.apache.pekko.http.scaladsl.server.directives.RouteDirectives._ + import org.apache.pekko.http.scaladsl.unmarshalling._ + + type FSU[T] = FromStringUnmarshaller[T] + type FSOU[T] = Unmarshaller[Option[String], T] + type SFVP = StringValueProvider + + protected def extractField[A, B](f: A => Directive1[B]): StringDefAux[A, Directive1[B]] = stringDef(f) + + protected def handleFieldResult[T](fieldName: String, result: Future[T]): Directive1[T] = onComplete(result).flatMap { + case Success(x) => provide(x) + case Failure(Unmarshaller.NoContentException) => reject(MissingFormFieldRejection(fieldName)) + case Failure(x: UnsupportedContentTypeException) => reject(UnsupportedRequestContentTypeRejection(x.supported, x.actualContentType)) + case Failure(x) => reject(MalformedFormFieldRejection(fieldName, if (x.getMessage == null) "" else x.getMessage, Option(x.getCause))) + } + + private def filter[T](paramName: String, fsou: FSOU[T])(implicit vp: SFVP): Directive1[T] = { + extract { ctx => + import ctx.{executionContext, materializer} + handleFieldResult(paramName, fsou(vp.get(paramName))) + }.flatMap(identity) + } + + implicit def forString(implicit fsu: FSU[String], vp: SFVP): StringDefAux[String, Directive1[String]] = + extractField[String, String] { string => filter(string, fsu) } + implicit def forSymbol(implicit fsu: FSU[String], vp: SFVP): StringDefAux[Symbol, Directive1[String]] = + extractField[Symbol, String] { symbol => filter(symbol.name, fsu) } + implicit def forNR[T](implicit fsu: FSU[T], vp: SFVP): StringDefAux[NameReceptacle[T], Directive1[T]] = + extractField[NameReceptacle[T], T] { nr => filter(nr.name, fsu) } + implicit def forNUR[T](implicit vp: SFVP): StringDefAux[NameUnmarshallerReceptacle[T], Directive1[T]] = + extractField[NameUnmarshallerReceptacle[T], T] { nr => filter(nr.name, nr.um) } + implicit def forNOR[T](implicit fsou: FSOU[T], vp: SFVP): StringDefAux[NameOptionReceptacle[T], Directive1[Option[T]]] = + extractField[NameOptionReceptacle[T], Option[T]] { nr => filter[Option[T]](nr.name, fsou) } + implicit def forNDR[T](implicit fsou: FSOU[T], vp: SFVP): StringDefAux[NameDefaultReceptacle[T], Directive1[T]] = + extractField[NameDefaultReceptacle[T], T] { nr => filter[T](nr.name, fsou withDefaultValue nr.default) } + implicit def forNOUR[T](implicit vp: SFVP): StringDefAux[NameOptionUnmarshallerReceptacle[T], Directive1[Option[T]]] = + extractField[NameOptionUnmarshallerReceptacle[T], Option[T]] { nr => filter(nr.name, nr.um: FSOU[T]) } + implicit def forNDUR[T](implicit vp: SFVP): StringDefAux[NameDefaultUnmarshallerReceptacle[T], Directive1[T]] = + extractField[NameDefaultUnmarshallerReceptacle[T], T] { nr => filter[T](nr.name, (nr.um: FSOU[T]) withDefaultValue nr.default) } + + //////////////////// required parameter support //////////////////// + + private def requiredFilter[T](paramName: String, fsou: FSOU[T], requiredValue: Any)(implicit vp: SFVP): Directive0 = { + extract { ctx => + import ctx.{executionContext, materializer} + onComplete(fsou(vp.get(paramName))) flatMap { + case Success(value) if value == requiredValue => pass + case Success(value) => reject(InvalidRequiredValueForQueryParamRejection(paramName, requiredValue.toString, value.toString)).toDirective[Unit] + case _ => reject(MissingQueryParamRejection(paramName)).toDirective[Unit] + } + }.flatMap(identity) + } + + implicit def forRVR[T](implicit fsu: FSU[T], vp: SFVP): StringDefAux[RequiredValueReceptacle[T], Directive0] = + stringDef[RequiredValueReceptacle[T], Directive0] { rvr => requiredFilter(rvr.name, fsu, rvr.requiredValue) } + + implicit def forRVDR[T](implicit vp: SFVP): StringDefAux[RequiredValueUnmarshallerReceptacle[T], Directive0] = + stringDef[RequiredValueUnmarshallerReceptacle[T], Directive0] { rvr => requiredFilter(rvr.name, rvr.um, rvr.requiredValue) } + + //////////////////// tuple support //////////////////// + + import org.apache.pekko.http.scaladsl.server.util.BinaryPolyFunc + import org.apache.pekko.http.scaladsl.server.util.TupleOps._ + + implicit def forTuple[T](implicit fold: FoldLeft[Directive0, T, ConvertStringDefAndConcatenate.type]): StringDefAux[T, fold.Out] = + stringDef[T, fold.Out](fold(BasicDirectives.pass, _)) + + object ConvertStringDefAndConcatenate extends BinaryPolyFunc { + implicit def from[P, TA, TB](implicit sdef: StringDef[P] {type Out = Directive[TB]}, ev: Join[TA, TB]): BinaryPolyFunc.Case[Directive[TA], P, ConvertStringDefAndConcatenate.type] {type Out = Directive[ev.Out]} = + at[Directive[TA], P] { (a, t) => a & sdef(t) } + } + + } +} diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/PetApi.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/PetApi.scala new file mode 100644 index 00000000000..23f7b88abce --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/PetApi.scala @@ -0,0 +1,198 @@ +package org.openapitools.server.api + +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromStringUnmarshaller +import org.openapitools.server.AkkaHttpHelper._ +import org.openapitools.server.StringDirectives +import org.openapitools.server.MultipartDirectives +import org.openapitools.server.FileField +import org.openapitools.server.PartsAndFiles +import org.openapitools.server.model.ApiResponse +import java.io.File +import org.openapitools.server.model.Pet +import scala.util.Try +import org.apache.pekko.http.scaladsl.server.MalformedRequestContentRejection +import org.apache.pekko.http.scaladsl.server.directives.FileInfo + + +class PetApi( + petService: PetApiService, + petMarshaller: PetApiMarshaller +) extends MultipartDirectives with StringDirectives { + + + import petMarshaller._ + + lazy val route: Route = + path("pet") { + post { + entity(as[Pet]){ pet => + petService.addPet(pet = pet) + } + } + } ~ + path("pet" / LongNumber) { (petId) => + delete { + optionalHeaderValueByName("api_key") { apiKey => + petService.deletePet(petId = petId, apiKey = apiKey) + } + } + } ~ + path("pet" / "findByStatus") { + get { + parameters("status".as[String]) { (status) => + petService.findPetsByStatus(status = status) + } + } + } ~ + path("pet" / "findByTags") { + get { + parameters("tags".as[String]) { (tags) => + petService.findPetsByTags(tags = tags) + } + } + } ~ + path("pet" / LongNumber) { (petId) => + get { + petService.getPetById(petId = petId) + } + } ~ + path("pet") { + put { + entity(as[Pet]){ pet => + petService.updatePet(pet = pet) + } + } + } ~ + path("pet" / LongNumber) { (petId) => + post { + formFields("name".as[String].?, "status".as[String].?) { (name, status) => + petService.updatePetWithForm(petId = petId, name = name, status = status) + } + } + } ~ + path("pet" / LongNumber / "uploadImage") { (petId) => + post { + formAndFiles(FileField("file")) { partsAndFiles => + val _____ : Try[Route] = for { + file <- optToTry(partsAndFiles.files.get("file"), s"File file missing") + } yield { + implicit val vp: StringValueProvider = partsAndFiles.form + stringFields("additionalMetadata".as[String].?) { (additionalMetadata) => + petService.uploadFile(petId = petId, additionalMetadata = additionalMetadata, file = file) + } + } + _____.fold[Route](t => reject(MalformedRequestContentRejection("Missing file.", t)), identity) + } + } + } +} + + +trait PetApiService { + + def addPet200(responsePet: Pet)(implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route = + complete((200, responsePet)) + def addPet405: Route = + complete((405, "Invalid input")) + /** + * Code: 200, Message: successful operation, DataType: Pet + * Code: 405, Message: Invalid input + */ + def addPet(pet: Pet) + (implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route + + def deletePet400: Route = + complete((400, "Invalid pet value")) + /** + * Code: 400, Message: Invalid pet value + */ + def deletePet(petId: Long, apiKey: Option[String]): Route + + def findPetsByStatus200(responsePetarray: Seq[Pet])(implicit toEntityMarshallerPetarray: ToEntityMarshaller[Seq[Pet]]): Route = + complete((200, responsePetarray)) + def findPetsByStatus400: Route = + complete((400, "Invalid status value")) + /** + * Code: 200, Message: successful operation, DataType: Seq[Pet] + * Code: 400, Message: Invalid status value + */ + def findPetsByStatus(status: String) + (implicit toEntityMarshallerPetarray: ToEntityMarshaller[Seq[Pet]]): Route + + def findPetsByTags200(responsePetarray: Seq[Pet])(implicit toEntityMarshallerPetarray: ToEntityMarshaller[Seq[Pet]]): Route = + complete((200, responsePetarray)) + def findPetsByTags400: Route = + complete((400, "Invalid tag value")) + /** + * Code: 200, Message: successful operation, DataType: Seq[Pet] + * Code: 400, Message: Invalid tag value + */ + def findPetsByTags(tags: String) + (implicit toEntityMarshallerPetarray: ToEntityMarshaller[Seq[Pet]]): Route + + def getPetById200(responsePet: Pet)(implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route = + complete((200, responsePet)) + def getPetById400: Route = + complete((400, "Invalid ID supplied")) + def getPetById404: Route = + complete((404, "Pet not found")) + /** + * Code: 200, Message: successful operation, DataType: Pet + * Code: 400, Message: Invalid ID supplied + * Code: 404, Message: Pet not found + */ + def getPetById(petId: Long) + (implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route + + def updatePet200(responsePet: Pet)(implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route = + complete((200, responsePet)) + def updatePet400: Route = + complete((400, "Invalid ID supplied")) + def updatePet404: Route = + complete((404, "Pet not found")) + def updatePet405: Route = + complete((405, "Validation exception")) + /** + * Code: 200, Message: successful operation, DataType: Pet + * Code: 400, Message: Invalid ID supplied + * Code: 404, Message: Pet not found + * Code: 405, Message: Validation exception + */ + def updatePet(pet: Pet) + (implicit toEntityMarshallerPet: ToEntityMarshaller[Pet]): Route + + def updatePetWithForm405: Route = + complete((405, "Invalid input")) + /** + * Code: 405, Message: Invalid input + */ + def updatePetWithForm(petId: Long, name: Option[String], status: Option[String]): Route + + def uploadFile200(responseApiResponse: ApiResponse)(implicit toEntityMarshallerApiResponse: ToEntityMarshaller[ApiResponse]): Route = + complete((200, responseApiResponse)) + /** + * Code: 200, Message: successful operation, DataType: ApiResponse + */ + def uploadFile(petId: Long, additionalMetadata: Option[String], file: (FileInfo, File)) + (implicit toEntityMarshallerApiResponse: ToEntityMarshaller[ApiResponse]): Route + +} + +trait PetApiMarshaller { + implicit def fromEntityUnmarshallerPet: FromEntityUnmarshaller[Pet] + + + + implicit def toEntityMarshallerPetarray: ToEntityMarshaller[Seq[Pet]] + + implicit def toEntityMarshallerPet: ToEntityMarshaller[Pet] + + implicit def toEntityMarshallerApiResponse: ToEntityMarshaller[ApiResponse] + +} + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/StoreApi.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/StoreApi.scala new file mode 100644 index 00000000000..300d018d394 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/StoreApi.scala @@ -0,0 +1,101 @@ +package org.openapitools.server.api + +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromStringUnmarshaller +import org.openapitools.server.AkkaHttpHelper._ +import org.openapitools.server.model.Order + + +class StoreApi( + storeService: StoreApiService, + storeMarshaller: StoreApiMarshaller +) { + + + import storeMarshaller._ + + lazy val route: Route = + path("store" / "order" / Segment) { (orderId) => + delete { + storeService.deleteOrder(orderId = orderId) + } + } ~ + path("store" / "inventory") { + get { + storeService.getInventory() + } + } ~ + path("store" / "order" / LongNumber) { (orderId) => + get { + storeService.getOrderById(orderId = orderId) + } + } ~ + path("store" / "order") { + post { + entity(as[Order]){ order => + storeService.placeOrder(order = order) + } + } + } +} + + +trait StoreApiService { + + def deleteOrder400: Route = + complete((400, "Invalid ID supplied")) + def deleteOrder404: Route = + complete((404, "Order not found")) + /** + * Code: 400, Message: Invalid ID supplied + * Code: 404, Message: Order not found + */ + def deleteOrder(orderId: String): Route + + def getInventory200(responseMapmap: Map[String, Int])(implicit toEntityMarshallerMapmap: ToEntityMarshaller[Map[String, Int]]): Route = + complete((200, responseMapmap)) + /** + * Code: 200, Message: successful operation, DataType: Map[String, Int] + */ + def getInventory(): Route + + def getOrderById200(responseOrder: Order)(implicit toEntityMarshallerOrder: ToEntityMarshaller[Order]): Route = + complete((200, responseOrder)) + def getOrderById400: Route = + complete((400, "Invalid ID supplied")) + def getOrderById404: Route = + complete((404, "Order not found")) + /** + * Code: 200, Message: successful operation, DataType: Order + * Code: 400, Message: Invalid ID supplied + * Code: 404, Message: Order not found + */ + def getOrderById(orderId: Long) + (implicit toEntityMarshallerOrder: ToEntityMarshaller[Order]): Route + + def placeOrder200(responseOrder: Order)(implicit toEntityMarshallerOrder: ToEntityMarshaller[Order]): Route = + complete((200, responseOrder)) + def placeOrder400: Route = + complete((400, "Invalid Order")) + /** + * Code: 200, Message: successful operation, DataType: Order + * Code: 400, Message: Invalid Order + */ + def placeOrder(order: Order) + (implicit toEntityMarshallerOrder: ToEntityMarshaller[Order]): Route + +} + +trait StoreApiMarshaller { + implicit def fromEntityUnmarshallerOrder: FromEntityUnmarshaller[Order] + + + + implicit def toEntityMarshallerOrder: ToEntityMarshaller[Order] + +} + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/UserApi.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/UserApi.scala new file mode 100644 index 00000000000..e1e39d96471 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/api/UserApi.scala @@ -0,0 +1,162 @@ +package org.openapitools.server.api + +import org.apache.pekko.http.scaladsl.server.Directives._ +import org.apache.pekko.http.scaladsl.server.Route +import org.apache.pekko.http.scaladsl.model.StatusCodes +import org.apache.pekko.http.scaladsl.marshalling.ToEntityMarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import org.apache.pekko.http.scaladsl.unmarshalling.FromStringUnmarshaller +import org.openapitools.server.AkkaHttpHelper._ +import java.time.OffsetDateTime +import org.openapitools.server.model.User + + +class UserApi( + userService: UserApiService, + userMarshaller: UserApiMarshaller +) { + + + import userMarshaller._ + + lazy val route: Route = + path("user") { + post { + entity(as[User]){ user => + userService.createUser(user = user) + } + } + } ~ + path("user" / "createWithArray") { + post { + entity(as[Seq[User]]){ user => + userService.createUsersWithArrayInput(user = user) + } + } + } ~ + path("user" / "createWithList") { + post { + entity(as[Seq[User]]){ user => + userService.createUsersWithListInput(user = user) + } + } + } ~ + path("user" / Segment) { (username) => + delete { + userService.deleteUser(username = username) + } + } ~ + path("user" / Segment) { (username) => + get { + userService.getUserByName(username = username) + } + } ~ + path("user" / "login") { + get { + parameters("username".as[String], "password".as[String]) { (username, password) => + userService.loginUser(username = username, password = password) + } + } + } ~ + path("user" / "logout") { + get { + userService.logoutUser() + } + } ~ + path("user" / Segment) { (username) => + put { + entity(as[User]){ user => + userService.updateUser(username = username, user = user) + } + } + } +} + + +trait UserApiService { + + def createUserDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUser(user: User): Route + + def createUsersWithArrayInputDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUsersWithArrayInput(user: Seq[User]): Route + + def createUsersWithListInputDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUsersWithListInput(user: Seq[User]): Route + + def deleteUser400: Route = + complete((400, "Invalid username supplied")) + def deleteUser404: Route = + complete((404, "User not found")) + /** + * Code: 400, Message: Invalid username supplied + * Code: 404, Message: User not found + */ + def deleteUser(username: String): Route + + def getUserByName200(responseUser: User)(implicit toEntityMarshallerUser: ToEntityMarshaller[User]): Route = + complete((200, responseUser)) + def getUserByName400: Route = + complete((400, "Invalid username supplied")) + def getUserByName404: Route = + complete((404, "User not found")) + /** + * Code: 200, Message: successful operation, DataType: User + * Code: 400, Message: Invalid username supplied + * Code: 404, Message: User not found + */ + def getUserByName(username: String) + (implicit toEntityMarshallerUser: ToEntityMarshaller[User]): Route + + def loginUser200(responseString: String)(implicit toEntityMarshallerString: ToEntityMarshaller[String]): Route = + complete((200, responseString)) + def loginUser400: Route = + complete((400, "Invalid username/password supplied")) + /** + * Code: 200, Message: successful operation, DataType: String + * Code: 400, Message: Invalid username/password supplied + */ + def loginUser(username: String, password: String): Route + + def logoutUserDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def logoutUser(): Route + + def updateUser400: Route = + complete((400, "Invalid user supplied")) + def updateUser404: Route = + complete((404, "User not found")) + /** + * Code: 400, Message: Invalid user supplied + * Code: 404, Message: User not found + */ + def updateUser(username: String, user: User): Route + +} + +trait UserApiMarshaller { + implicit def fromEntityUnmarshallerUser: FromEntityUnmarshaller[User] + + implicit def fromEntityUnmarshallerUserList: FromEntityUnmarshaller[Seq[User]] + + + + implicit def toEntityMarshallerUser: ToEntityMarshaller[User] + +} + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/ApiResponse.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/ApiResponse.scala new file mode 100644 index 00000000000..abe04c18183 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/ApiResponse.scala @@ -0,0 +1,18 @@ +package org.openapitools.server.model + + +/** + * = An uploaded response = + * + * Describes the result of uploading an image resource + * + * @param code for example: ''null'' + * @param `type` for example: ''null'' + * @param message for example: ''null'' +*/ +final case class ApiResponse ( + code: Option[Int] = None, + `type`: Option[String] = None, + message: Option[String] = None +) + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Category.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Category.scala new file mode 100644 index 00000000000..f01b8adf6e0 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Category.scala @@ -0,0 +1,16 @@ +package org.openapitools.server.model + + +/** + * = Pet category = + * + * A category for a pet + * + * @param id for example: ''null'' + * @param name for example: ''null'' +*/ +final case class Category ( + id: Option[Long] = None, + name: Option[String] = None +) + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Order.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Order.scala new file mode 100644 index 00000000000..e79e2c15cf6 --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Order.scala @@ -0,0 +1,25 @@ +package org.openapitools.server.model + +import java.time.OffsetDateTime + +/** + * = Pet Order = + * + * An order for a pets from the pet store + * + * @param id for example: ''null'' + * @param petId for example: ''null'' + * @param quantity for example: ''null'' + * @param shipDate for example: ''null'' + * @param status Order Status for example: ''null'' + * @param complete for example: ''null'' +*/ +final case class Order ( + id: Option[Long] = None, + petId: Option[Long] = None, + quantity: Option[Int] = None, + shipDate: Option[OffsetDateTime] = None, + status: Option[String] = None, + complete: Option[Boolean] = None +) + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Pet.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Pet.scala new file mode 100644 index 00000000000..0f58bcee1da --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Pet.scala @@ -0,0 +1,24 @@ +package org.openapitools.server.model + + +/** + * = a Pet = + * + * A pet for sale in the pet store + * + * @param id for example: ''null'' + * @param category for example: ''null'' + * @param name for example: ''doggie'' + * @param photoUrls for example: ''null'' + * @param tags for example: ''null'' + * @param status pet status in the store for example: ''null'' +*/ +final case class Pet ( + id: Option[Long] = None, + category: Option[Category] = None, + name: String, + photoUrls: Seq[String], + tags: Option[Seq[Tag]] = None, + status: Option[String] = None +) + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Tag.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Tag.scala new file mode 100644 index 00000000000..9daae0f44ff --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/Tag.scala @@ -0,0 +1,16 @@ +package org.openapitools.server.model + + +/** + * = Pet Tag = + * + * A tag for a pet + * + * @param id for example: ''null'' + * @param name for example: ''null'' +*/ +final case class Tag ( + id: Option[Long] = None, + name: Option[String] = None +) + diff --git a/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/User.scala b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/User.scala new file mode 100644 index 00000000000..18e1772ba4d --- /dev/null +++ b/samples/server/petstore/scala-pekko-http-server/src/main/scala/org/openapitools/server/model/User.scala @@ -0,0 +1,28 @@ +package org.openapitools.server.model + + +/** + * = a User = + * + * A User who is purchasing from the pet store + * + * @param id for example: ''null'' + * @param username for example: ''null'' + * @param firstName for example: ''null'' + * @param lastName for example: ''null'' + * @param email for example: ''null'' + * @param password for example: ''null'' + * @param phone for example: ''null'' + * @param userStatus User Status for example: ''null'' +*/ +final 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, + userStatus: Option[Int] = None +) +