diff --git a/bin/scala-akka-http-server-petstore.sh b/bin/scala-akka-http-server-petstore.sh new file mode 100644 index 000000000000..83ca77a4c384 --- /dev/null +++ b/bin/scala-akka-http-server-petstore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +if [ ! -d "${APP_DIR}" ]; then + APP_DIR=$(dirname "$SCRIPT")/.. + APP_DIR=$(cd "${APP_DIR}"; pwd) +fi + +executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar" + +if [ ! -f "$executable" ] +then + mvn clean package +fi + +# if you've executed sbt assembly previously it will use that instead. +export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties" +ags="$@ generate -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -g scala-akka-http -o samples/server/petstore/scala-akka-http" + +java ${JAVA_OPTS} -jar ${executable} ${ags} diff --git a/bin/windows/scala-akka-http-server-petstore.bat b/bin/windows/scala-akka-http-server-petstore.bat new file mode 100644 index 000000000000..f9c73f7d7aec --- /dev/null +++ b/bin/windows/scala-akka-http-server-petstore.bat @@ -0,0 +1,10 @@ +set executable=.\modules\openapi-generator-cli\target\openapi-generator-cli.jar + +If Not Exist %executable% ( + mvn clean package +) + +REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -DloggerPath=conf/log4j.properties +set ags=generate --artifact-id "scala-akka-http-petstore-server" -i modules\openapi-generator\src\test\resources\2_0\petstore.yaml -g scala-akka-http -o samples\server\petstore\scala-akka-http + +java %JAVA_OPTS% -jar %executable% %ags% diff --git a/docs/generators.md b/docs/generators.md index 8e4995658a89..555373e4a690 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -120,6 +120,7 @@ The following generators are available: * [ruby-on-rails](generators/ruby-on-rails.md) * [ruby-sinatra](generators/ruby-sinatra.md) * [rust-server](generators/rust-server.md) +* [scala-akka-http](generators/scala-akka-http.md) * [scala-finch](generators/scala-finch.md) * [scala-lagom-server](generators/scala-lagom-server.md) * [scala-play-server](generators/scala-play-server.md) diff --git a/docs/generators/scala-akka-http.md b/docs/generators/scala-akka-http.md new file mode 100644 index 000000000000..88f96cb531fd --- /dev/null +++ b/docs/generators/scala-akka-http.md @@ -0,0 +1,221 @@ +--- +title: Config Options for scala-akka-http +sidebar_label: scala-akka-http +--- + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|akkaHttpVersion|The version of akka-http| |10.1.10| +|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|apiPackage|package for generated api classes| |null| +|artifactId|artifactId| |openapi-scala-akka-http-server| +|artifactVersion|artifact version in generated pom.xml. This also becomes part of the generated library's filename| |1.0.0| +|dateLibrary|Option. Date library to use|
**joda**
Joda (for legacy app)
**java8**
Java 8 native JSR310 (prefered for JDK 1.8+)
|java8| +|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|groupId|groupId in generated pom.xml| |org.openapitools| +|invokerPackage|root package for generated code| |org.openapitools.server| +|modelPackage|package for generated models| |null| +|modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase| +|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| +|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| +|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| +|sourceFolder|source folder for generated code| |null| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | +|Array|java.util.List| +|ArrayList|java.util.ArrayList| +|Date|java.util.Date| +|DateTime|org.joda.time.*| +|File|java.io.File| +|HashMap|java.util.HashMap| +|ListBuffer|scala.collection.mutable.ListBuffer| +|ListSet|scala.collection.immutable.ListSet| +|LocalDate|org.joda.time.*| +|LocalDateTime|org.joda.time.*| +|LocalTime|org.joda.time.*| +|Timestamp|java.sql.Timestamp| +|URI|java.net.URI| +|UUID|java.util.UUID| + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | +|array|ListBuffer| +|map|Map| +|set|Set| + + +## LANGUAGE PRIMITIVES + + + +## RESERVED WORDS + + + +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✗|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Array|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✓|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✗|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✗|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✗|OAS2,OAS3 +|Union|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✓|OAS3 +|OAuth2_Implicit|✗|OAS2,OAS3 +|OAuth2_Password|✗|OAS2,OAS3 +|OAuth2_ClientCredentials|✗|OAS2,OAS3 +|OAuth2_AuthorizationCode|✗|OAS2,OAS3 + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✓|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✓|OAS2,OAS3 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java new file mode 100644 index 000000000000..5e1ae3dfc7dd --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaAkkaHttpServerCodegen.java @@ -0,0 +1,486 @@ +package org.openapitools.codegen.languages; + +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.servers.Server; +import org.openapitools.codegen.*; + +import java.io.File; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.openapitools.codegen.meta.features.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScalaAkkaHttpServerCodegen extends AbstractScalaCodegen implements CodegenConfig { + protected String groupId; + protected String artifactId; + protected String artifactVersion; + protected String invokerPackage; + + protected String akkaHttpVersion; + + 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"; + + static Logger LOGGER = LoggerFactory.getLogger(ScalaAkkaHttpServerCodegen.class); + + public CodegenType getTag() { + return CodegenType.SERVER; + } + + public String getName() { + return "scala-akka-http"; + } + + public String getHelp() { + return "Generates a scala-akka-http server."; + } + + public ScalaAkkaHttpServerCodegen() { + super(); + + modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML, WireFormatFeature.Custom)) + .securityFeatures(EnumSet.of( + SecurityFeature.BasicAuth, + SecurityFeature.ApiKey, + SecurityFeature.BearerToken + )) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling + ) + .excludeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism + ) + .excludeParameterFeatures( + ParameterFeature.Cookie + ) + ); + + outputFolder = "generated-code" + File.separator + "scala-akka-http"; + modelTemplateFiles.put("model.mustache", ".scala"); + apiTemplateFiles.put("api.mustache", ".scala"); + embeddedTemplateDir = templateDir = "scala-akka-http-server"; + + groupId = "org.openapitools"; + artifactId = "openapi-scala-akka-http-server"; + artifactVersion = "1.0.0"; + apiPackage = "org.openapitools.server.api"; + modelPackage = "org.openapitools.server.model"; + invokerPackage = "org.openapitools.server"; + akkaHttpVersion = DEFAULT_AKKA_HTTP_VERSION; + + setReservedWordsLowerCase( + Arrays.asList( + "abstract", "case", "catch", "class", "def", "do", "else", "extends", + "false", "final", "finally", "for", "forSome", "if", "implicit", + "import", "lazy", "match", "new", "null", "object", "override", "package", + "private", "protected", "return", "sealed", "super", "this", "throw", + "trait", "try", "true", "type", "val", "var", "while", "with", "yield") + ); + + cliOptions.add(CliOption.newString(CodegenConstants.INVOKER_PACKAGE, CodegenConstants.INVOKER_PACKAGE_DESC).defaultValue(invokerPackage)); + cliOptions.add(CliOption.newString(CodegenConstants.GROUP_ID, CodegenConstants.GROUP_ID_DESC).defaultValue(groupId)); + cliOptions.add(CliOption.newString(CodegenConstants.ARTIFACT_ID, CodegenConstants.ARTIFACT_ID).defaultValue(artifactId)); + 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)); + + importMapping.remove("Seq"); + importMapping.remove("List"); + importMapping.remove("Set"); + importMapping.remove("Map"); + + typeMapping = new HashMap<>(); + typeMapping.put("array", "Seq"); + typeMapping.put("set", "Set"); + typeMapping.put("boolean", "Boolean"); + typeMapping.put("string", "String"); + typeMapping.put("int", "Int"); + typeMapping.put("integer", "Int"); + typeMapping.put("long", "Long"); + typeMapping.put("float", "Float"); + typeMapping.put("byte", "Byte"); + typeMapping.put("short", "Short"); + typeMapping.put("char", "Char"); + typeMapping.put("double", "Double"); + typeMapping.put("object", "Any"); + typeMapping.put("file", "File"); + typeMapping.put("binary", "File"); + typeMapping.put("number", "Double"); + + instantiationTypes.put("array", "ListBuffer"); + instantiationTypes.put("map", "Map"); + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + invokerPackage = (String) additionalProperties.get(CodegenConstants.INVOKER_PACKAGE); + } else { + additionalProperties.put(CodegenConstants.INVOKER_PACKAGE, invokerPackage); + } + + if (additionalProperties.containsKey(CodegenConstants.GROUP_ID)) { + groupId = (String) additionalProperties.get(CodegenConstants.GROUP_ID); + } else { + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); + } + + if (additionalProperties.containsKey(CodegenConstants.ARTIFACT_ID)) { + artifactId = (String) additionalProperties.get(CodegenConstants.ARTIFACT_ID); + } else { + additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); + } + + if (additionalProperties.containsKey(CodegenConstants.ARTIFACT_VERSION)) { + artifactVersion = (String) additionalProperties.get(CodegenConstants.ARTIFACT_VERSION); + } else { + additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); + } + + if (additionalProperties.containsKey(AKKA_HTTP_VERSION)) { + akkaHttpVersion = (String) additionalProperties.get(AKKA_HTTP_VERSION); + } else { + additionalProperties.put(AKKA_HTTP_VERSION, akkaHttpVersion); + } + + parseAkkaHttpVersion(); + + supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt")); + supportingFiles.add(new SupportingFile("controller.mustache", + (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), "Controller.scala")); + supportingFiles.add(new SupportingFile("helper.mustache", + (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), "AkkaHttpHelper.scala")); + supportingFiles.add(new SupportingFile("stringDirectives.mustache", + (sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator), "StringDirectives.scala")); + supportingFiles.add(new SupportingFile("multipartDirectives.mustache", + (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() { + Matcher matcher = akkaVersionPattern.matcher(akkaHttpVersion); + if (matcher.matches()) { + String majorS = matcher.group(1); + String minorS = matcher.group(3); + String patchS = matcher.group(5); + boolean andAbove = matcher.group(6) != null; + int major = -1, minor = -1, patch = -1; + try { + if (majorS != null) { + major = Integer.parseInt(majorS); + if (minorS != null) { + minor = Integer.parseInt(minorS); + if (patchS != null) { + patch = Integer.parseInt(patchS); + } + } + } + + + if (major > 10 || major == -1 && andAbove) { + is10_1_10AndAbove = true; + } else if (major == 10) { + if (minor > 1 || minor == -1 && andAbove) { + is10_1_10AndAbove = true; + } else if (minor == 1) { + if (patch >= 10 || patch == -1 && andAbove) { + is10_1_10AndAbove = true; + } + } + } + + } catch (NumberFormatException e) { + LOGGER.warn("Unable to parse " + AKKA_HTTP_VERSION + ": " + akkaHttpVersion + ", fallback to " + DEFAULT_AKKA_HTTP_VERSION); + akkaHttpVersion = DEFAULT_AKKA_HTTP_VERSION; + is10_1_10AndAbove = true; + } + } + + additionalProperties.put(IS_10_1_10_PLUS, is10_1_10AndAbove); + } + + @Override + public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) { + CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, servers); + addPathMatcher(codegenOperation); + return codegenOperation; + } + + @Override + public CodegenParameter fromParameter(Parameter parameter, Set imports) { + CodegenParameter param = super.fromParameter(parameter, imports); + // Removing unhandled types + if(!primitiveParamTypes.contains(param.dataType)) { + param.dataType = "String"; + } + if (!param.required) { + param.vendorExtensions.put("hasDefaultValue", param.defaultValue != null); + // Escaping default string values + if (param.defaultValue != null && param.dataType.equals("String")) { + param.defaultValue = String.format(Locale.ROOT, "\"%s\"", param.defaultValue); + } + } + return param; + } + + + + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + Map baseObjs = super.postProcessOperationsWithModels(objs, allModels); + pathMatcherPatternsPostProcessor(baseObjs); + marshallingPostProcessor(baseObjs); + return baseObjs; + } + + private static Set primitiveParamTypes = new HashSet(){{ + addAll(Arrays.asList( + "Int", + "Long", + "Float", + "Double", + "Boolean", + "String" + )); + }}; + + private static Map pathTypeToMatcher = new HashMap(){{ + put("Int", "IntNumber"); + put("Long", "LongNumber"); + put("Float","FloatNumber"); + put("Double","DoubleNumber"); + put("Boolean","Boolean"); + put("String", "Segment"); + }}; + + protected static void addPathMatcher(CodegenOperation codegenOperation) { + LinkedList allPaths = new LinkedList<>(Arrays.asList(codegenOperation.path.split("/"))); + allPaths.removeIf(""::equals); + + LinkedList pathMatchers = new LinkedList<>(); + for(String path: allPaths){ + TextOrMatcher textOrMatcher = new TextOrMatcher("", true, true); + if(path.startsWith("{") && path.endsWith("}")) { + String parameterName = path.substring(1, path.length()-1); + for(CodegenParameter pathParam: codegenOperation.pathParams){ + if(pathParam.baseName.equals(parameterName)) { + String matcher = pathTypeToMatcher.get(pathParam.dataType); + if(matcher == null) { + LOGGER.warn("The path parameter " + pathParam.baseName + + " with the datatype " + pathParam.dataType + + " could not be translated to a corresponding path matcher of akka http" + + " and therefore has been translated to string."); + matcher = pathTypeToMatcher.get("String"); + } + if (pathParam.pattern != null && !pathParam.pattern.isEmpty()) { + matcher = pathMatcherPatternName(pathParam); + } + textOrMatcher.value = matcher; + textOrMatcher.isText = false; + pathMatchers.add(textOrMatcher); + } + } + } else { + textOrMatcher.value = path; + textOrMatcher.isText = true; + pathMatchers.add(textOrMatcher); + } + } + pathMatchers.getLast().hasMore = false; + + codegenOperation.vendorExtensions.put("paths", pathMatchers); + } + + public static String PATH_MATCHER_PATTERNS_KEY = "pathMatcherPatterns"; + + @SuppressWarnings("unchecked") + private static void pathMatcherPatternsPostProcessor(Map objs) { + if (objs != null) { + HashMap patternMap = new HashMap<>(); + Map operations = (Map) objs.get("operations"); + if (operations != null) { + List ops = (List) operations.get("operation"); + for (CodegenOperation operation: ops) { + for (CodegenParameter parameter: operation.pathParams) { + if (parameter.pattern != null && !parameter.pattern.isEmpty()) { + String name = pathMatcherPatternName(parameter); + if (!patternMap.containsKey(name)) { + patternMap.put(name, new PathMatcherPattern(name, parameter.pattern.substring(1, parameter.pattern.length() - 1))); + } + } + } + } + } + objs.put(PATH_MATCHER_PATTERNS_KEY, new ArrayList<>(patternMap.values())); + } + } + + private static String pathMatcherPatternName(CodegenParameter parameter) { + return parameter.paramName + "Pattern"; + } + + // Responsible for setting up Marshallers/Unmarshallers + @SuppressWarnings("unchecked") + public static void marshallingPostProcessor(Map objs) { + + if (objs == null) { + return; + } + + Set entityUnmarshallerTypes = new HashSet<>(); + Set entityMarshallerTypes = new HashSet<>(); + Set stringUnmarshallerTypes = new HashSet<>(); + boolean hasCookieParams = false; + boolean hasMultipart = false; + + Map operations = (Map) objs.get("operations"); + if (operations != null) { + List operationList = (List) operations.get("operation"); + + for (CodegenOperation op : operationList) { + boolean isMultipart = op.isMultipart; + hasMultipart |= isMultipart; + hasCookieParams |= op.getHasCookieParams(); + ArrayList fileParams = new ArrayList<>(); + ArrayList nonFileParams = new ArrayList<>(); + for (CodegenParameter parameter : op.allParams) { + if (parameter.isBodyParam || parameter.isFormParam) { + if (parameter.isFile) { + fileParams.add(parameter.copy()); + } else { + nonFileParams.add(parameter.copy()); + } + if (!parameter.isPrimitiveType) { + if (isMultipart) { + stringUnmarshallerTypes.add(new Marshaller(parameter)); + } else { + entityUnmarshallerTypes.add(new Marshaller(parameter)); + } + } + } + } + for (int i = 0, size = fileParams.size(); i < size; ++i) { + fileParams.get(i).hasMore = i < size - 1; + } + for (int i = 0, size = nonFileParams.size(); i < size; ++i) { + nonFileParams.get(i).hasMore = i < size - 1; + } + + HashSet operationSpecificMarshallers = new HashSet<>(); + for (CodegenResponse response : op.responses) { + if (!response.primitiveType) { + Marshaller marshaller = new Marshaller(response); + entityMarshallerTypes.add(marshaller); + operationSpecificMarshallers.add(marshaller); + } + response.vendorExtensions.put("isDefault", response.code.equals("0")); + } + op.vendorExtensions.put("specificMarshallers", operationSpecificMarshallers); + op.vendorExtensions.put("fileParams", fileParams); + op.vendorExtensions.put("nonFileParams", nonFileParams); + } + } + + objs.put("hasCookieParams", hasCookieParams); + objs.put("entityMarshallers", entityMarshallerTypes); + objs.put("entityUnmarshallers", entityUnmarshallerTypes); + objs.put("stringUnmarshallers", stringUnmarshallerTypes); + objs.put("hasMarshalling", !entityMarshallerTypes.isEmpty() || !entityUnmarshallerTypes.isEmpty() || !stringUnmarshallerTypes.isEmpty()); + objs.put("hasMultipart", hasMultipart); + } + +} + +class Marshaller { + String varName; + String dataType; + + public Marshaller(CodegenResponse response) { + if (response.containerType != null) { + this.varName = response.baseType + response.containerType; + } else { + this.varName = response.baseType; + } + this.dataType = response.dataType; + } + + public Marshaller(CodegenParameter parameter) { + if (parameter.isListContainer) { + this.varName = parameter.baseType + "List"; + } else if (parameter.isMapContainer) { + this.varName = parameter.baseType + "Map"; + } else if (parameter.isContainer) { + this.varName = parameter.baseType + "Container"; + } else { + this.varName = parameter.baseType; + } + this.dataType = parameter.dataType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Marshaller that = (Marshaller) o; + return varName.equals(that.varName) && + dataType.equals(that.dataType); + } + + @Override + public int hashCode() { + return Objects.hash(varName, dataType); + } +} + +class PathMatcherPattern { + String pathMatcherVarName; + String pattern; + + public PathMatcherPattern(String pathMatcherVarName, String pattern) { + this.pathMatcherVarName = pathMatcherVarName; + this.pattern = pattern; + } +} + +class TextOrMatcher { + String value; + boolean isText; + boolean hasMore; + + public TextOrMatcher(String value, boolean isText, boolean hasMore) { + this.value = value; + this.isText = isText; + this.hasMore = hasMore; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TextOrMatcher that = (TextOrMatcher) o; + return isText == that.isText && + hasMore == that.hasMore && + value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value, isText, hasMore); + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index cac33205e17f..8e2db93db510 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -129,3 +129,5 @@ org.openapitools.codegen.languages.FsharpFunctionsServerCodegen org.openapitools.codegen.languages.MarkdownDocumentationCodegen org.openapitools.codegen.languages.ScalaSttpClientCodegen + +org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/README.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/README.mustache new file mode 100644 index 000000000000..64c7b16ced18 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/README.mustache @@ -0,0 +1,32 @@ +# {{&appName}} + +{{&appDescription}} + +{{^hideGenerationTimestamp}} + This Scala akka-http framework project was generated by the OpenAPI generator tool at {{generatedDate}}. +{{/hideGenerationTimestamp}} + +{{#generateApis}} + ## API + + {{#apiInfo}} + {{#apis}} + ### {{baseName}} + + |Name|Role| + |----|----| + |`{{importPath}}Controller`|akka-http API controller| + |`{{importPath}}Api`|Representing trait| + {{^skipStubs}} + |`{{importPath}}ApiImpl`|Default implementation| + {{/skipStubs}} + + {{#operations}} + {{#operation}} + * `{{httpMethod}} {{contextPath}}{{path}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{paramName}}=[value]{{#hasMore}}&{{/hasMore}}{{/queryParams}}` - {{summary}} + {{/operation}} + {{/operations}} + + {{/apis}} + {{/apiInfo}} +{{/generateApis}} 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 new file mode 100644 index 000000000000..0fcceddbbfc4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/api.mustache @@ -0,0 +1,95 @@ +package {{package}} + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +{{^pathMatcherPatterns.isEmpty}}import akka.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}} +{{#hasCookieParams}}import akka.http.scaladsl.model.headers.HttpCookiePair +{{/hasCookieParams}} +import {{invokerPackage}}.AkkaHttpHelper._ +{{#hasMultipart}}import {{invokerPackage}}.StringDirectives +import {{invokerPackage}}.MultipartDirectives +import {{invokerPackage}}.FileField +import {{invokerPackage}}.PartsAndFiles +{{/hasMultipart}} +{{#imports}}import {{import}} +{{/imports}} +{{#hasMultipart}}import scala.util.Try +import akka.http.scaladsl.server.MalformedRequestContentRejection +import akka.http.scaladsl.server.directives.FileInfo +{{/hasMultipart}} + + +{{#operations}} +class {{classname}}( + {{classVarName}}Service: {{classname}}Service{{#hasMarshalling}}, + {{classVarName}}Marshaller: {{classname}}Marshaller{{/hasMarshalling}} +) {{#hasMultipart}} extends MultipartDirectives with StringDirectives {{/hasMultipart}}{ + + {{#pathMatcherPatterns}}import {{classname}}Patterns.{{pathMatcherVarName}} + {{/pathMatcherPatterns}} + + {{#hasMarshalling}}import {{classVarName}}Marshaller._ + {{/hasMarshalling}} + + lazy val route: Route = + {{#operation}} + path({{#vendorExtensions.paths}}{{#isText}}"{{/isText}}{{value}}{{#isText}}"{{/isText}}{{#hasMore}} / {{/hasMore}}{{/vendorExtensions.paths}}) { {{^pathParams.isEmpty}}({{#pathParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/pathParams}}) => {{/pathParams.isEmpty}} + {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { {{^queryParams.isEmpty}} + parameters({{#queryParams}}"{{baseName}}".as[{{dataType}}]{{^required}}.?{{#vendorExtensions.hasDefaultValue}}({{{defaultValue}}}){{/vendorExtensions.hasDefaultValue}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/queryParams}}) { ({{#queryParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/queryParams}}) =>{{/queryParams.isEmpty}} {{^headerParams.isEmpty}} + {{#headerParams}}{{#required}}headerValueByName{{/required}}{{^required}}optionalHeaderValueByName{{/required}}("{{baseName}}") { {{paramName}} => {{/headerParams}}{{/headerParams.isEmpty}}{{^cookieParams.isEmpty}} + {{#cookieParams}}{{#required}}cookie({{/required}}{{^required}}optionalCookie({{/required}}"{{baseName}}"){ {{paramName}} => {{/cookieParams}}{{/cookieParams.isEmpty}}{{#isMultipart}} +{{> multipart}}{{/isMultipart}}{{^isMultipart}}{{> noMultipart}}{{/isMultipart}}{{^cookieParams.isEmpty}} + }{{/cookieParams.isEmpty}}{{^headerParams.isEmpty}} + }{{/headerParams.isEmpty}}{{^queryParams.isEmpty}} + }{{/queryParams.isEmpty}} + } + }{{^-last}} ~{{/-last}} + {{/operation}} +} + +{{^pathMatcherPatterns.isEmpty}} +object {{classname}}Patterns { + + {{#pathMatcherPatterns}}val {{pathMatcherVarName}}: PathMatcher1[String] = PathMatcher("{{pattern}}".r) + {{/pathMatcherPatterns}} +} +{{/pathMatcherPatterns.isEmpty}} + +trait {{classname}}Service { + +{{#operation}} +{{#responses}} def {{operationId}}{{#vendorExtensions.isDefault}}Default{{/vendorExtensions.isDefault}}{{^vendorExtensions.isDefault}}{{code}}{{/vendorExtensions.isDefault}}{{#baseType}}({{#vendorExtensions.isDefault}}statusCode: Int, {{/vendorExtensions.isDefault}}response{{baseType}}{{containerType}}: {{dataType}}){{^isPrimitiveType}}(implicit toEntityMarshaller{{baseType}}{{containerType}}: ToEntityMarshaller[{{dataType}}]){{/isPrimitiveType}}{{/baseType}}{{^baseType}}{{#vendorExtensions.isDefault}}(statusCode: Int){{/vendorExtensions.isDefault}}{{/baseType}}: Route = + complete(({{#vendorExtensions.isDefault}}statusCode{{/vendorExtensions.isDefault}}{{^vendorExtensions.isDefault}}{{code}}{{/vendorExtensions.isDefault}}, {{#baseType}}response{{baseType}}{{containerType}}{{/baseType}}{{^baseType}}"{{message}}"{{/baseType}})) +{{/responses}} + /** +{{#responses}} * {{#code}}Code: {{.}}{{/code}}{{#message}}, Message: {{.}}{{/message}}{{#dataType}}, DataType: {{.}}{{/dataType}} + {{/responses}} + */ + def {{operationId}}({{> operationParam}}){{^vendorExtensions.specificMarshallers.isEmpty}} + (implicit {{#vendorExtensions.specificMarshallers}}toEntityMarshaller{{varName}}: ToEntityMarshaller[{{dataType}}]{{^-last}}, {{/-last}}{{/vendorExtensions.specificMarshallers}}){{/vendorExtensions.specificMarshallers.isEmpty}}: Route + +{{/operation}} +} + +{{#hasMarshalling}} +trait {{classname}}Marshaller { +{{#entityUnmarshallers}} implicit def fromEntityUnmarshaller{{varName}}: FromEntityUnmarshaller[{{dataType}}] + +{{/entityUnmarshallers}} + +{{#stringUnmarshallers}} implicit def fromStringUnmarshaller{{varName}}: FromStringUnmarshaller[{{dataType}}] + +{{/stringUnmarshallers}} + +{{#entityMarshallers}} implicit def toEntityMarshaller{{varName}}: ToEntityMarshaller[{{dataType}}] + +{{/entityMarshallers}} +} +{{/hasMarshalling}} + +{{/operations}} 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 new file mode 100644 index 000000000000..d4b57b676140 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/build.sbt.mustache @@ -0,0 +1,9 @@ +version := "{{artifactVersion}}" +name := "{{artifactId}}" +organization := "{{groupId}}" +scalaVersion := "2.12.8" + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-stream" % "2.5.21", + "com.typesafe.akka" %% "akka-http" % "{{akkaHttpVersion}}" +) 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 new file mode 100644 index 000000000000..ec9fe871c753 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/controller.mustache @@ -0,0 +1,16 @@ +package {{invokerPackage}} + +import akka.http.scaladsl.Http +import akka.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 + +class Controller({{#apiInfo}}{{#apis}}{{#operations}}{{classVarName}}: {{classname}}{{#hasMore}}, {{/hasMore}}{{/operations}}{{/apis}}{{/apiInfo}})(implicit system: ActorSystem, materializer: ActorMaterializer) { + + lazy val routes: Route = {{#apiInfo}}{{#apis}}{{#operations}}{{classVarName}}.route {{#hasMore}}~ {{/hasMore}}{{/operations}}{{/apis}}{{/apiInfo}} + + Http().bindAndHandle(routes, "0.0.0.0", 9000) +} \ No newline at end of file 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 new file mode 100644 index 000000000000..8aa3c0f8c269 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/helper.mustache @@ -0,0 +1,34 @@ +package {{invokerPackage}} + +import akka.http.scaladsl.server.Directives._ +import akka.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/modules/openapi-generator/src/main/resources/scala-akka-http-server/model.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/model.mustache new file mode 100644 index 000000000000..85ab81e74bfa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/model.mustache @@ -0,0 +1,27 @@ +package {{package}} + +{{#imports}} +import {{import}} +{{/imports}} + +{{#models}} +{{#model}} +/** +{{#title}} * = {{{title}}} = + * +{{/title}} +{{#description}} * {{{description}}} + * +{{/description}} +{{#vars}} + * @param {{{name}}} {{#description}}{{{description}}}{{/description}}{{#example}} for example: ''{{{example}}}''{{/example}} +{{/vars}} +*/ +final case class {{classname}} ( + {{#vars}} + {{{name}}}: {{^required}}Option[{{/required}}{{datatype}}{{^required}}]{{/required}}{{#hasMore}},{{/hasMore}} + {{/vars}} +) + +{{/model}} +{{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipart.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipart.mustache new file mode 100644 index 000000000000..6f8d2355b2ed --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipart.mustache @@ -0,0 +1,12 @@ + formAndFiles({{#vendorExtensions.fileParams}}FileField("{{baseName}}")){{/vendorExtensions.fileParams}}{{#hasMore}}, {{/hasMore}} { partsAndFiles => {{^vendorExtensions.fileParams.isEmpty}} + val _____ : Try[Route] = for { + {{#vendorExtensions.fileParams}}{{baseName}} <- optToTry(partsAndFiles.files.get("{{baseName}}"), s"File {{baseName}} missing") + {{/vendorExtensions.fileParams}} + } yield { {{/vendorExtensions.fileParams.isEmpty}} + implicit val vp: StringValueProvider = partsAndFiles.form{{^vendorExtensions.nonFileParams.isEmpty}} + stringFields({{#vendorExtensions.nonFileParams}}"{{baseName}}".as[{{dataType}}]{{^required}}.?{{#vendorExtensions.hasDefaultValue}}({{defaultValue}}){{/vendorExtensions.hasDefaultValue}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.nonFileParams}}) { ({{#vendorExtensions.nonFileParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/vendorExtensions.nonFileParams}}) =>{{/vendorExtensions.nonFileParams.isEmpty}} + {{classVarName}}Service.{{operationId}}({{#allParams}}{{paramName}} = {{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}}){{^vendorExtensions.nonFileFormParams.isEmpty}} + }{{/vendorExtensions.nonFileFormParams.isEmpty}}{{^vendorExtensions.fileParams.isEmpty}} + } + _____.fold[Route](t => reject(MalformedRequestContentRejection("Missing file.", t)), identity){{/vendorExtensions.fileParams.isEmpty}} + } \ No newline at end of file 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 new file mode 100644 index 000000000000..98a2186fd2e7 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/multipartDirectives.mustache @@ -0,0 +1,88 @@ +package {{invokerPackage}} + +import java.io.File + +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 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._ + + @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 => File.createTempFile(file.fileName, ".tmp") + } +} + +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/modules/openapi-generator/src/main/resources/scala-akka-http-server/noMultipart.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/noMultipart.mustache new file mode 100644 index 000000000000..a4c25bb27d2d --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/noMultipart.mustache @@ -0,0 +1,7 @@ +{{^formParams.isEmpty}} + + formFields({{#formParams}}"{{baseName}}".as[{{#isPrimitiveType}}{{dataType}}{{/isPrimitiveType}}{{^isPrimitiveType}}String{{/isPrimitiveType}}]{{^required}}.?{{#vendorExtensions.hasDefaultValue}}({{defaultValue}}){{/vendorExtensions.hasDefaultValue}}{{/required}}{{#hasMore}}, {{/hasMore}}{{/formParams}}) { ({{#formParams}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{/formParams}}) =>{{/formParams.isEmpty}} + {{#bodyParam}}{{^isPrimitiveType}}entity(as[{{dataType}}]){ {{paramName}} => + {{/isPrimitiveType}}{{/bodyParam}}{{classVarName}}Service.{{operationId}}({{#allParams}}{{paramName}} = {{paramName}}{{#hasMore}}, {{/hasMore}}{{/allParams}}){{#bodyParam}}{{^isPrimitiveType}} + }{{/isPrimitiveType}}{{/bodyParam}}{{^formParams.isEmpty}} + }{{/formParams.isEmpty}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-akka-http-server/operationParam.mustache b/modules/openapi-generator/src/main/resources/scala-akka-http-server/operationParam.mustache new file mode 100644 index 000000000000..7e5846f3b882 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/operationParam.mustache @@ -0,0 +1 @@ +{{#allParams}}{{paramName}}: {{#isFile}}(FileInfo, File){{/isFile}}{{^isFile}}{{^required}}{{^vendorExtensions.hasDefaultValue}}Option[{{/vendorExtensions.hasDefaultValue}}{{/required}}{{dataType}}{{^required}}{{^vendorExtensions.hasDefaultValue}}]{{/vendorExtensions.hasDefaultValue}}{{/required}}{{/isFile}}{{#hasMore}}, {{/hasMore}}{{/allParams}} \ No newline at end of file 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 new file mode 100644 index 000000000000..5640a9d45490 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-akka-http-server/stringDirectives.mustache @@ -0,0 +1,127 @@ +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 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 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._ + + 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)){{#akkaHttp10_1_10_plus}} + case Failure(x: UnsupportedContentTypeException) => reject(UnsupportedRequestContentTypeRejection(x.supported, x.actualContentType)){{/akkaHttp10_1_10_plus}}{{^akkaHttp10_1_10_plus}} + case Failure(x: UnsupportedContentTypeException) => reject(UnsupportedRequestContentTypeRejection(x.supported)){{/akkaHttp10_1_10_plus}} + 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 akka.http.scaladsl.server.util.BinaryPolyFunc + import akka.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-akka-http/.openapi-generator-ignore b/samples/server/petstore/scala-akka-http/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/.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-akka-http/.openapi-generator/VERSION b/samples/server/petstore/scala-akka-http/.openapi-generator/VERSION new file mode 100644 index 000000000000..b5d898602c2c --- /dev/null +++ b/samples/server/petstore/scala-akka-http/.openapi-generator/VERSION @@ -0,0 +1 @@ +4.3.1-SNAPSHOT \ No newline at end of file diff --git a/samples/server/petstore/scala-akka-http/README.md b/samples/server/petstore/scala-akka-http/README.md new file mode 100644 index 000000000000..a1c3b50c09b2 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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-akka-http/build.sbt b/samples/server/petstore/scala-akka-http/build.sbt new file mode 100644 index 000000000000..1099fe43c457 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/build.sbt @@ -0,0 +1,9 @@ +version := "1.0.0" +name := "scala-akka-http-petstore-server" +organization := "org.openapitools" +scalaVersion := "2.12.8" + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-stream" % "2.5.21", + "com.typesafe.akka" %% "akka-http" % "10.1.10" +) diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala new file mode 100644 index 000000000000..b35110e91f5c --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/AkkaHttpHelper.scala @@ -0,0 +1,34 @@ +package org.openapitools.server + +import akka.http.scaladsl.server.Directives._ +import akka.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-akka-http/src/main/scala/org/openapitools/server/Controller.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/Controller.scala new file mode 100644 index 000000000000..8cfc986a0aa4 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/Controller.scala @@ -0,0 +1,18 @@ +package org.openapitools.server + +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Route +import org.openapitools.server.api.PetApi +import org.openapitools.server.api.StoreApi +import org.openapitools.server.api.UserApi + +import akka.http.scaladsl.server.Directives._ +import akka.actor.ActorSystem +import akka.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-akka-http/src/main/scala/org/openapitools/server/MultipartDirectives.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/MultipartDirectives.scala new file mode 100644 index 000000000000..79891d7095a8 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/MultipartDirectives.scala @@ -0,0 +1,88 @@ +package org.openapitools.server + +import java.io.File + +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 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._ + + @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 => File.createTempFile(file.fileName, ".tmp") + } +} + +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-akka-http/src/main/scala/org/openapitools/server/StringDirectives.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/StringDirectives.scala new file mode 100644 index 000000000000..2d115849056e --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/StringDirectives.scala @@ -0,0 +1,126 @@ +package org.openapitools.server + +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 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 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._ + + 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 akka.http.scaladsl.server.util.BinaryPolyFunc + import akka.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-akka-http/src/main/scala/org/openapitools/server/api/PetApi.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/PetApi.scala new file mode 100644 index 000000000000..a78bb32e9a58 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/PetApi.scala @@ -0,0 +1,189 @@ +package org.openapitools.server.api + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import akka.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 akka.http.scaladsl.server.MalformedRequestContentRejection +import akka.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]){ body => + petService.addPet(body = body) + } + } + } ~ + 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]){ body => + petService.updatePet(body = body) + } + } + } ~ + 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 addPet405: Route = + complete((405, "Invalid input")) + /** + * Code: 405, Message: Invalid input + */ + def addPet(body: 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 updatePet400: Route = + complete((400, "Invalid ID supplied")) + def updatePet404: Route = + complete((404, "Pet not found")) + def updatePet405: Route = + complete((405, "Validation exception")) + /** + * Code: 400, Message: Invalid ID supplied + * Code: 404, Message: Pet not found + * Code: 405, Message: Validation exception + */ + def updatePet(body: 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-akka-http/src/main/scala/org/openapitools/server/api/StoreApi.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/StoreApi.scala new file mode 100644 index 000000000000..a7bfdc650129 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/StoreApi.scala @@ -0,0 +1,100 @@ +package org.openapitools.server.api + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import akka.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]){ body => + storeService.placeOrder(body = body) + } + } + } +} + + +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(body: 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-akka-http/src/main/scala/org/openapitools/server/api/UserApi.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/UserApi.scala new file mode 100644 index 000000000000..0d8cdff76944 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/api/UserApi.scala @@ -0,0 +1,160 @@ +package org.openapitools.server.api + +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.marshalling.ToEntityMarshaller +import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller +import akka.http.scaladsl.unmarshalling.FromStringUnmarshaller +import org.openapitools.server.AkkaHttpHelper._ +import org.openapitools.server.model.User + + +class UserApi( + userService: UserApiService, + userMarshaller: UserApiMarshaller +) { + + + import userMarshaller._ + + lazy val route: Route = + path("user") { + post { + entity(as[User]){ body => + userService.createUser(body = body) + } + } + } ~ + path("user" / "createWithArray") { + post { + entity(as[Seq[User]]){ body => + userService.createUsersWithArrayInput(body = body) + } + } + } ~ + path("user" / "createWithList") { + post { + entity(as[Seq[User]]){ body => + userService.createUsersWithListInput(body = body) + } + } + } ~ + 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]){ body => + userService.updateUser(username = username, body = body) + } + } + } +} + + +trait UserApiService { + + def createUserDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUser(body: User): Route + + def createUsersWithArrayInputDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUsersWithArrayInput(body: Seq[User]): Route + + def createUsersWithListInputDefault(statusCode: Int): Route = + complete((statusCode, "successful operation")) + /** + * Code: 0, Message: successful operation + */ + def createUsersWithListInput(body: 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, body: 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-akka-http/src/main/scala/org/openapitools/server/model/ApiResponse.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/ApiResponse.scala new file mode 100644 index 000000000000..9091fd61fbc4 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + `type`: Option[String], + message: Option[String] +) + diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Category.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Category.scala new file mode 100644 index 000000000000..cbde3f530168 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + name: Option[String] +) + diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Order.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Order.scala new file mode 100644 index 000000000000..669df7946c43 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + petId: Option[Long], + quantity: Option[Int], + shipDate: Option[OffsetDateTime], + status: Option[String], + complete: Option[Boolean] +) + diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Pet.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Pet.scala new file mode 100644 index 000000000000..4e929dbccb88 --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + category: Option[Category], + name: String, + photoUrls: Seq[String], + tags: Option[Seq[Tag]], + status: Option[String] +) + diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Tag.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/Tag.scala new file mode 100644 index 000000000000..3e62ea28016e --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + name: Option[String] +) + diff --git a/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/User.scala b/samples/server/petstore/scala-akka-http/src/main/scala/org/openapitools/server/model/User.scala new file mode 100644 index 000000000000..315a86efd65a --- /dev/null +++ b/samples/server/petstore/scala-akka-http/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], + username: Option[String], + firstName: Option[String], + lastName: Option[String], + email: Option[String], + password: Option[String], + phone: Option[String], + userStatus: Option[Int] +) +