[Scala] scala-akka-http-server support pekko-http using useApachePekko flag (#16255)

* introduce useApachePekko flag into scala-akka-http-server

* useApachePekko flag in scala-akka-http-server templates

* introduce scala-pekko-http-server samples
This commit is contained in:
Ken Kaizu 2023-08-05 14:32:02 +09:00 committed by GitHub
parent 82516c75d2
commit c080660cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1038 additions and 47 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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}}

View File

@ -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}}
)

View File

@ -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) {

View File

@ -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

View File

@ -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] =

View File

@ -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, _))

View 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

View File

@ -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

View File

@ -0,0 +1 @@
7.0.0-SNAPSHOT

View File

@ -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

View File

@ -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"
)

View File

@ -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 }
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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) }
}
}
}

View File

@ -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]
}

View File

@ -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]
}

View File

@ -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]
}

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)

View File

@ -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
)