From 323cd38b5c7085321dcad0f65cb3a293603bae13 Mon Sep 17 00:00:00 2001 From: Kasper Kondzielski Date: Wed, 1 Jul 2020 03:14:34 +0200 Subject: [PATCH] Improve sttpOpenApiClient generator (#6684) Co-authored-by: eugeniyk --- docs/generators/scala-sttp.md | 8 +- .../languages/ScalaSttpClientCodegen.java | 442 +++++++++++++++++- .../main/resources/scala-sttp/api.mustache | 30 +- .../resources/scala-sttp/apiInvoker.mustache | 50 -- .../resources/scala-sttp/build.sbt.mustache | 26 +- .../scala-sttp/dateSerializers.mustache | 76 +++ .../scala-sttp/enumsSerializers.mustache | 42 -- .../resources/scala-sttp/jsonSupport.mustache | 53 +++ .../scala-sttp/methodParameters.mustache | 2 +- .../main/resources/scala-sttp/model.mustache | 3 +- .../paramMultipartCreation.mustache | 16 + .../project/build.properties.mustache | 1 + .../resources/scala-sttp/requests.mustache | 48 -- .../resources/scala-sttp/serializers.mustache | 57 --- .../scala/SttpBooleanPropertyTest.java | 29 ++ .../scala/SttpJsonLibraryPropertyTest.java | 39 ++ .../scala/SttpPackagePropertyTest.java | 59 +++ .../codegen/scala/SttpStringPropertyTest.java | 30 ++ .../scala-sttp/.openapi-generator/FILES | 7 +- samples/client/petstore/scala-sttp/build.sbt | 16 +- .../scala-sttp/project/build.properties | 1 + .../org/openapitools/client/api/PetApi.scala | 51 +- .../openapitools/client/api/StoreApi.scala | 25 +- .../org/openapitools/client/api/UserApi.scala | 47 +- .../openapitools/client/core/ApiInvoker.scala | 60 --- .../client/core/DateSerializers.scala | 29 ++ .../JsonSupport.scala} | 25 +- .../client/core/Serializers.scala | 31 -- .../client/core/credentials.scala | 18 + .../openapitools/client/core/requests.scala | 58 --- .../client/model/ApiResponse.scala | 3 +- .../openapitools/client/model/Category.scala | 3 +- .../client/model/InlineObject.scala | 3 +- .../client/model/InlineObject1.scala | 3 +- .../org/openapitools/client/model/Order.scala | 3 +- .../org/openapitools/client/model/Pet.scala | 3 +- .../org/openapitools/client/model/Tag.scala | 3 +- .../org/openapitools/client/model/User.scala | 3 +- 38 files changed, 901 insertions(+), 502 deletions(-) delete mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/apiInvoker.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache delete mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/enumsSerializers.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/paramMultipartCreation.mustache create mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/project/build.properties.mustache delete mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/requests.mustache delete mode 100644 modules/openapi-generator/src/main/resources/scala-sttp/serializers.mustache create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpBooleanPropertyTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpJsonLibraryPropertyTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpPackagePropertyTest.java create mode 100644 modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpStringPropertyTest.java create mode 100644 samples/client/petstore/scala-sttp/project/build.properties delete mode 100644 samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/ApiInvoker.scala create mode 100644 samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala rename samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/{api/EnumsSerializers.scala => core/JsonSupport.scala} (62%) delete mode 100644 samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/Serializers.scala create mode 100644 samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/credentials.scala delete mode 100644 samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/requests.scala diff --git a/docs/generators/scala-sttp.md b/docs/generators/scala-sttp.md index 7561501ce2e..420682ca082 100644 --- a/docs/generators/scala-sttp.md +++ b/docs/generators/scala-sttp.md @@ -7,17 +7,23 @@ sidebar_label: scala-sttp | ------ | ----------- | ------ | ------- | |allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| |apiPackage|package for generated api classes| |null| +|circeVersion|The version of circe library| |0.13.0| |dateLibrary|Option. Date library to use|
**joda**
Joda (for legacy app)
**java8**
Java 8 native JSR310 (prefered for JDK 1.8+)
|java8| |disallowAdditionalPropertiesIfNotPresent|Specify the behavior when the 'additionalProperties' keyword is not present in the OAS document. If false: the 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications. If true: when the 'additionalProperties' keyword is not present in a schema, the value of 'additionalProperties' is set to false, i.e. no additional properties are allowed. Note: this mode is not compliant with the JSON schema specification. This is the original openapi-generator behavior.This setting is currently ignored for OAS 2.0 documents: 1) When the 'additionalProperties' keyword is not present in a 2.0 schema, additional properties are NOT allowed. 2) Boolean values of the 'additionalProperties' keyword are ignored. It's as if additional properties are NOT allowed.Note: the root cause are issues #1369 and #1371, which must be resolved in the swagger-parser project.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
when the 'additionalProperties' keyword is not present in a schema, the value of 'additionalProperties' is automatically set to false, i.e. no additional properties are allowed. Note: this mode is not compliant with the JSON schema specification. This is the original openapi-generator behavior.
|true| |ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|jodaTimeVersion|The version of joda-time library| |2.10.6| +|json4sVersion|The version of json4s library| |3.6.8| +|jsonLibrary|Json library to use. Possible values are: json4s and circe.| |json4s| |legacyDiscriminatorBehavior|This flag is used by OpenAPITools codegen to influence the processing of the discriminator attribute in OpenAPI documents. This flag has no impact if the OAS document does not use the discriminator attribute. The default value of this flag is set in each language-specific code generator (e.g. Python, Java, go...)using the method toModelName. Note to developers supporting a language generator in OpenAPITools; to fully support the discriminator attribute as defined in the OAS specification 3.x, language generators should set this flag to true by default; however this requires updating the mustache templates to generate a language-specific discriminator lookup function that iterates over {{#mappedModels}} and does not iterate over {{children}}, {{#anyOf}}, or {{#oneOf}}.|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| |mainPackage|Top-level package name, which defines 'apiPackage', 'modelPackage', 'invokerPackage'| |org.openapitools.client| |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| +|separateErrorChannel|Whether to return response as F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten response's error raising them through enclosing monad (F[ReturnType]).| |true| |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| +|sttpClientVersion|The version of sttp client| |2.2.0| ## IMPORT MAPPING @@ -81,7 +87,7 @@ sidebar_label: scala-sttp
  • final
  • finally
  • for
  • -
  • forsome
  • +
  • forSome
  • if
  • implicit
  • import
  • diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java index 43b48cb3114..4fc1efdd507 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttpClientCodegen.java @@ -16,50 +16,154 @@ package org.openapitools.codegen.languages; +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; -import org.openapitools.codegen.CodegenConfig; -import org.openapitools.codegen.CodegenOperation; -import org.openapitools.codegen.SupportingFile; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.*; import org.openapitools.codegen.meta.GeneratorMetadata; import org.openapitools.codegen.meta.Stability; +import org.openapitools.codegen.meta.features.*; +import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; -import java.util.List; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.*; + +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public class ScalaSttpClientCodegen extends AbstractScalaCodegen implements CodegenConfig { + private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " + + "sttp client", "2.2.0"); + private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel", + "Whether to return response as " + + "F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " + + "response's error raising them through enclosing monad (F[ReturnType]).", true); + private static final StringProperty JODA_TIME_VERSION = new StringProperty("jodaTimeVersion", "The version of " + + "joda-time library", "2.10.6"); + private static final StringProperty JSON4S_VERSION = new StringProperty("json4sVersion", "The version of json4s " + + "library", "3.6.8"); + private static final StringProperty CIRCE_VERSION = new StringProperty("circeVersion", "The version of circe " + + "library", "0.13.0"); + private static final JsonLibraryProperty JSON_LIBRARY_PROPERTY = new JsonLibraryProperty(); + + public static final String DEFAULT_PACKAGE_NAME = "org.openapitools.client"; + private static final PackageProperty PACKAGE_PROPERTY = new PackageProperty(); + + private static final List> properties = Arrays.asList( + STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JODA_TIME_VERSION, + JSON4S_VERSION, CIRCE_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY); + + private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttpClientCodegen.class); + + protected String groupId = "org.openapitools"; + protected String artifactId = "openapi-client"; + protected String artifactVersion = "1.0.0"; + protected boolean registerNonStandardStatusCodes = true; + protected boolean renderJavadoc = true; + protected boolean removeOAuthSecurities = true; -public class ScalaSttpClientCodegen extends ScalaAkkaClientCodegen implements CodegenConfig { public ScalaSttpClientCodegen() { super(); generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) .stability(Stability.BETA) .build(); - embeddedTemplateDir = templateDir = "scala-sttp"; + 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 + ) + .includeClientModificationFeatures( + ClientModificationFeature.BasePath, + ClientModificationFeature.UserAgent + ) + ); + outputFolder = "generated-code/scala-sttp"; + modelTemplateFiles.put("model.mustache", ".scala"); + apiTemplateFiles.put("api.mustache", ".scala"); + embeddedTemplateDir = templateDir = "scala-sttp"; + + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); + additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); + additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); + if (renderJavadoc) { + additionalProperties.put("javadocRenderer", new JavadocLambda()); + } + additionalProperties.put("fnCapitalize", new CapitalizeLambda()); + additionalProperties.put("fnCamelize", new CamelizeLambda(false)); + additionalProperties.put("fnEnumEntry", new EnumEntryLambda()); + + 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"); + + properties.stream() + .map(Property::toCliOptions) + .flatMap(Collection::stream) + .forEach(option -> cliOptions.add(option)); } @Override public void processOpts() { super.processOpts(); - if (additionalProperties.containsKey("mainPackage")) { - setMainPackage((String) additionalProperties.get("mainPackage")); - additionalProperties.replace("configKeyPath", this.configKeyPath); - apiPackage = mainPackage + ".api"; - modelPackage = mainPackage + ".model"; - invokerPackage = mainPackage + ".core"; - additionalProperties.put("apiPackage", apiPackage); - additionalProperties.put("modelPackage", modelPackage); - } + properties.forEach(p -> p.updateAdditionalProperties(additionalProperties)); + invokerPackage = PACKAGE_PROPERTY.getInvokerPackage(additionalProperties); + apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties); + modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties); - supportingFiles.clear(); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); supportingFiles.add(new SupportingFile("build.sbt.mustache", "", "build.sbt")); final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator); - supportingFiles.add(new SupportingFile("requests.mustache", invokerFolder, "requests.scala")); - supportingFiles.add(new SupportingFile("apiInvoker.mustache", invokerFolder, "ApiInvoker.scala")); - final String apiFolder = (sourceFolder + File.separator + apiPackage).replace(".", File.separator); - supportingFiles.add(new SupportingFile("enumsSerializers.mustache", apiFolder, "EnumsSerializers.scala")); - supportingFiles.add(new SupportingFile("serializers.mustache", invokerFolder, "Serializers.scala")); + supportingFiles.add(new SupportingFile("jsonSupport.mustache", invokerFolder, "JsonSupport.scala")); + supportingFiles.add(new SupportingFile("project/build.properties.mustache", "project", "build.properties")); + supportingFiles.add(new SupportingFile("dateSerializers.mustache", invokerFolder, "DateSerializers.scala")); } @Override @@ -87,4 +191,300 @@ public class ScalaSttpClientCodegen extends ScalaAkkaClientCodegen implements Co op.path = encodePath(path); return op; } + + @Override + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + @Override + public String escapeReservedWord(String name) { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "`" + name + "`"; + } + + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + if (registerNonStandardStatusCodes) { + try { + @SuppressWarnings("unchecked") + Map> opsMap = (Map>) objs.get("operations"); + HashSet unknownCodes = new HashSet(); + for (CodegenOperation operation : opsMap.get("operation")) { + for (CodegenResponse response : operation.responses) { + if ("default".equals(response.code)) { + continue; + } + try { + int code = Integer.parseInt(response.code); + if (code >= 600) { + unknownCodes.add(code); + } + } catch (NumberFormatException e) { + LOGGER.error("Status code is not an integer : response.code", e); + } + } + } + if (!unknownCodes.isEmpty()) { + additionalProperties.put("unknownStatusCodes", unknownCodes); + } + } catch (Exception e) { + LOGGER.error("Unable to find operations List", e); + } + } + return super.postProcessOperationsWithModels(objs, allModels); + } + + @Override + public List fromSecurity(Map schemes) { + final List codegenSecurities = super.fromSecurity(schemes); + if (!removeOAuthSecurities) { + return codegenSecurities; + } + + // Remove OAuth securities + Iterator it = codegenSecurities.iterator(); + while (it.hasNext()) { + final CodegenSecurity security = it.next(); + if (security.isOAuth) { + it.remove(); + } + } + // Adapt 'hasMore' + it = codegenSecurities.iterator(); + while (it.hasNext()) { + final CodegenSecurity security = it.next(); + security.hasMore = it.hasNext(); + } + + if (codegenSecurities.isEmpty()) { + return null; + } + return codegenSecurities; + } + + @Override + public String toParamName(String name) { + return formatIdentifier(name, false); + } + + @Override + public String toEnumName(CodegenProperty property) { + return formatIdentifier(property.baseName, true); + } + + @Override + public String toDefaultValue(Schema p) { + if (p.getRequired() != null && p.getRequired().contains(p.getName())) { + return "None"; + } + + if (ModelUtils.isBooleanSchema(p)) { + return null; + } else if (ModelUtils.isDateSchema(p)) { + return null; + } else if (ModelUtils.isDateTimeSchema(p)) { + return null; + } else if (ModelUtils.isNumberSchema(p)) { + return null; + } else if (ModelUtils.isIntegerSchema(p)) { + return null; + } else if (ModelUtils.isMapSchema(p)) { + String inner = getSchemaType(getAdditionalProperties(p)); + return "Map[String, " + inner + "].empty "; + } else if (ModelUtils.isArraySchema(p)) { + ArraySchema ap = (ArraySchema) p; + String inner = getSchemaType(ap.getItems()); + if (ModelUtils.isSet(ap)) { + return "Set[" + inner + "].empty "; + } + return "Seq[" + inner + "].empty "; + } else if (ModelUtils.isStringSchema(p)) { + return null; + } else { + return null; + } + } + + public static abstract class Property { + final String name; + final String description; + final T defaultValue; + + public Property(String name, String description, T defaultValue) { + this.name = name; + this.description = description; + this.defaultValue = defaultValue; + } + + public abstract List toCliOptions(); + + public abstract void updateAdditionalProperties(Map additionalProperties); + + public abstract T getValue(Map additionalProperties); + + public void setValue(Map additionalProperties, T value) { + additionalProperties.put(name, value); + } + } + + public static class StringProperty extends Property { + public StringProperty(String name, String description, String defaultValue) { + super(name, description, defaultValue); + } + + @Override + public List toCliOptions() { + return Collections.singletonList(CliOption.newString(name, description).defaultValue(defaultValue)); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + if (!additionalProperties.containsKey(name)) { + additionalProperties.put(name, defaultValue); + } + } + + @Override + public String getValue(Map additionalProperties) { + return additionalProperties.getOrDefault(name, defaultValue).toString(); + } + } + + public static class BooleanProperty extends Property { + public BooleanProperty(String name, String description, Boolean defaultValue) { + super(name, description, defaultValue); + } + + @Override + public List toCliOptions() { + return Collections.singletonList(CliOption.newBoolean(name, description, defaultValue)); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + Boolean value = getValue(additionalProperties); + additionalProperties.put(name, value); + } + + @Override + public Boolean getValue(Map additionalProperties) { + return Boolean.valueOf(additionalProperties.getOrDefault(name, defaultValue.toString()).toString()); + } + } + + public static class JsonLibraryProperty extends StringProperty { + private static final String JSON4S = "json4s"; + private static final String CIRCE = "circe"; + + public JsonLibraryProperty() { + super("jsonLibrary", "Json library to use. Possible values are: json4s and circe.", JSON4S); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + String value = getValue(additionalProperties); + if (value.equals(CIRCE) || value.equals(JSON4S)) { + additionalProperties.put(CIRCE, value.equals(CIRCE)); + additionalProperties.put(JSON4S, value.equals(JSON4S)); + } else { + IllegalArgumentException exception = + new IllegalArgumentException("Invalid json library: " + value + ". Must be " + CIRCE + " " + + "or " + JSON4S); + throw exception; + } + } + } + + public static class PackageProperty extends StringProperty { + + public PackageProperty() { + super("mainPackage", "Top-level package name, which defines 'apiPackage', 'modelPackage', " + + "'invokerPackage'", DEFAULT_PACKAGE_NAME); + } + + @Override + public void updateAdditionalProperties(Map additionalProperties) { + String mainPackage = getValue(additionalProperties); + if (!additionalProperties.containsKey(CodegenConstants.API_PACKAGE)) { + String apiPackage = mainPackage + ".api"; + additionalProperties.put(CodegenConstants.API_PACKAGE, apiPackage); + } + if (!additionalProperties.containsKey(CodegenConstants.MODEL_PACKAGE)) { + String modelPackage = mainPackage + ".model"; + additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage); + } + if (!additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + String invokerPackage = mainPackage + ".core"; + additionalProperties.put(CodegenConstants.INVOKER_PACKAGE, invokerPackage); + } + } + + public String getApiPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.API_PACKAGE, DEFAULT_PACKAGE_NAME + ".api").toString(); + } + + public String getModelPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.MODEL_PACKAGE, DEFAULT_PACKAGE_NAME + ".model").toString(); + } + + public String getInvokerPackage(Map additionalProperties) { + return additionalProperties.getOrDefault(CodegenConstants.INVOKER_PACKAGE, DEFAULT_PACKAGE_NAME + ".core").toString(); + } + } + + private static abstract class CustomLambda implements Mustache.Lambda { + @Override + public void execute(Template.Fragment frag, Writer out) throws IOException { + final StringWriter tempWriter = new StringWriter(); + frag.execute(tempWriter); + out.write(formatFragment(tempWriter.toString())); + } + + public abstract String formatFragment(String fragment); + } + + private static class JavadocLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + final String[] lines = fragment.split("\\r?\\n"); + final StringBuilder sb = new StringBuilder(); + sb.append(" /**\n"); + for (String line : lines) { + sb.append(" * ").append(line).append("\n"); + } + sb.append(" */\n"); + return sb.toString(); + } + } + + private static class CapitalizeLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return StringUtils.capitalize(fragment); + } + } + + private static class CamelizeLambda extends CustomLambda { + private final boolean capitalizeFirst; + + public CamelizeLambda(boolean capitalizeFirst) { + this.capitalizeFirst = capitalizeFirst; + } + + @Override + public String formatFragment(String fragment) { + return camelize(fragment, !capitalizeFirst); + } + } + + private class EnumEntryLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return formatIdentifier(fragment, true); + } + } + } diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/api.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/api.mustache index 196a51a96f0..e67b67018c5 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/api.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/api.mustache @@ -4,42 +4,40 @@ package {{package}} {{#imports}} import {{import}} {{/imports}} -import {{invokerPackage}}._ -import alias._ +import {{invokerPackage}}.JsonSupport._ import sttp.client._ import sttp.model.Method {{#operations}} object {{classname}} { - def apply(baseUrl: String = "{{{basePath}}}")(implicit serializer: SttpSerializer) = new {{classname}}(baseUrl) +def apply(baseUrl: String = "{{{basePath}}}") = new {{classname}}(baseUrl) } -class {{classname}}(baseUrl: String)(implicit serializer: SttpSerializer) { - - import Helpers._ - import serializer._ +class {{classname}}(baseUrl: String) { {{#operation}} {{#javadocRenderer}} {{>javadoc}} {{/javadocRenderer}} - def {{operationId}}({{>methodParameters}}): ApiRequestT[{{>operationReturnType}}] = + def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseError[Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}, Nothing] = basicRequest .method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{#queryParams}}{{baseName}}=${{{paramName}}}{{^-last}}&{{/-last}}{{/queryParams}}{{/queryParams.0}}{{#isApiKey}}{{#isKeyInQuery}}{{^queryParams.0}}?{{/queryParams.0}}{{#queryParams.0}}&{{/queryParams.0}}{{keyParamName}}=${apiKey.value}&{{/isKeyInQuery}}{{/isApiKey}}") .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} .header({{>paramCreation}}){{/headerParams}}{{#authMethods}}{{#isBasic}}{{#isBasicBasic}} - .auth.withCredentials(basicAuth.user, basicAuth.password){{/isBasicBasic}}{{#isBasicBearer}} - .auth.bearer(bearerToken.token){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}} - .header("{{keyParamName}}", apiKey.value){{/isKeyInHeader}}{{#isKeyInCookie}} - .cookie("{{keyParamName}}", apiKey.value){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}} + .auth.withCredentials(username, password){{/isBasicBasic}}{{#isBasicBearer}} + .auth.bearer(bearerToken){{/isBasicBearer}}{{/isBasic}}{{#isApiKey}}{{#isKeyInHeader}} + .header("{{keyParamName}}", apiKey){{/isKeyInHeader}}{{#isKeyInCookie}} + .cookie("{{keyParamName}}", apiKey){{/isKeyInCookie}}{{/isApiKey}}{{/authMethods}}{{#formParams.0}}{{^isMultipart}} .body(Map({{#formParams}} - {{>paramFormCreation}},{{/formParams}} - )){{/formParams.0}}{{#bodyParam}} + {{>paramFormCreation}}{{#hasMore}}, {{/hasMore}}{{/formParams}} + )){{/isMultipart}}{{#isMultipart}} + .multipartBody(Seq({{#formParams}} + {{>paramMultipartCreation}}{{#hasMore}}, {{/hasMore}}{{/formParams}} + ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} .body({{paramName}}){{/bodyParam}} - .response(asJson[{{>operationReturnType}}]) + .response({{#separateErrorChannel}}asJson{{/separateErrorChannel}}{{^separateErrorChannel}}asJsonAlwaysUnsafe{{/separateErrorChannel}}[{{>operationReturnType}}]) {{/operation}} } - {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/apiInvoker.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/apiInvoker.mustache deleted file mode 100644 index af26ef8f2bb..00000000000 --- a/modules/openapi-generator/src/main/resources/scala-sttp/apiInvoker.mustache +++ /dev/null @@ -1,50 +0,0 @@ -{{>licenseInfo}} -package {{invokerPackage}} - -import org.json4s._ -import sttp.client._ -import sttp.model.StatusCode -import {{{apiPackage}}}.EnumsSerializers -import sttp.client.json4s.SttpJson4sApi -import sttp.client.monad.MonadError - -class SttpSerializer(implicit val format: Formats = DefaultFormats ++ EnumsSerializers.all ++ Serializers.all, - implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization) extends SttpJson4sApi - -class HttpException(val statusCode: StatusCode, val statusText: String, val message: String) extends Exception(s"[$statusCode] $statusText: $message") - -object Helpers { - - // Helper to handle Optional header parameters - implicit class optionalParams(val request: RequestT[Identity, Either[String, String], Nothing]) extends AnyVal { - def header( header: String, optValue: Option[Any]): RequestT[Identity, Either[String, String], Nothing] = { - optValue.map( value => request.header(header, value.toString)).getOrElse(request) - } - } - -} - -object ApiInvoker { - - /** - * Allows request execution without calling apiInvoker.execute(request) - * request.result can be used to get a monad wrapped content. - * - * @param request the apiRequest to be executed - */ - implicit class ApiRequestImprovements[R[_], T](request: RequestT[Identity, Either[ResponseError[Exception], T], Nothing]) { - - def result(implicit backend: SttpBackend[R, Nothing, Nothing]): R[T] = { - val responseT = request.send() - val ME: MonadError[R] = backend.responseMonad - ME.flatMap(responseT) { - response => - response.body match { - case Left(ex) => ME.error[T](new HttpException(response.code, response.statusText, ex.body)) - case Right(value) => ME.unit(value) - } - } - } - } - -} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/build.sbt.mustache index 00fe48b731d..a20ba532f44 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/build.sbt.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/build.sbt.mustache @@ -2,20 +2,24 @@ version := "{{artifactVersion}}" name := "{{artifactId}}" organization := "{{groupId}}" -scalaVersion := "2.13.0" - -crossScalaVersions := Seq(scalaVersion.value, "2.12.10", "2.11.12") +scalaVersion := "2.13.2" +crossScalaVersions := Seq(scalaVersion.value, "2.12.10") libraryDependencies ++= Seq( - "com.softwaremill.sttp.client" %% "core" % "2.0.0", - "com.softwaremill.sttp.client" %% "json4s" % "2.0.0", + "com.softwaremill.sttp.client" %% "core" % "{{sttpClientVersion}}", {{#joda}} - "joda-time" % "joda-time" % "2.10.1", + "joda-time" % "joda-time" % "{{jodaTimeVersion}}", {{/joda}} - "org.json4s" %% "json4s-jackson" % "3.6.7", - // test dependencies - "org.scalatest" %% "scalatest" % "3.0.8" % Test, - "junit" % "junit" % "4.13" % "test" +{{#json4s}} + "com.softwaremill.sttp.client" %% "json4s" % "{{sttpClientVersion}}", + "org.json4s" %% "json4s-jackson" % "{{json4sVersion}}" +{{/json4s}} +{{#circe}} + "com.softwaremill.sttp.client" %% "circe" % "{{sttpClientVersion}}", + "io.circe" %% "circe-core" % "{{circeVersion}}", + "io.circe" %% "circe-generic" % "{{circeVersion}}", + "io.circe" %% "circe-parser" % "{{circeVersion}}" +{{/circe}} ) scalacOptions := Seq( @@ -23,5 +27,3 @@ scalacOptions := Seq( "-deprecation", "-feature" ) - -publishArtifact in (Compile, packageDoc) := false \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache new file mode 100644 index 00000000000..779e7a428de --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp/dateSerializers.mustache @@ -0,0 +1,76 @@ +package {{invokerPackage}} + +{{#java8}} +import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} +import java.time.format.DateTimeFormatter +import scala.util.Try +{{/java8}} +{{#joda}} +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +{{/joda}} + +{{#json4s}} +object DateSerializers { + import org.json4s.{Serializer, CustomSerializer, JNull} + import org.json4s.JsonAST.JString + {{#java8}} + case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { + case JString(s) => + Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse + Try(LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toOffsetDateTime) getOrElse (null) + case JNull => null + }, { + case d: OffsetDateTime => + JString(d.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + })) + + case object LocalDateSerializer extends CustomSerializer[LocalDate]( _ => ( { + case JString(s) => LocalDate.parse(s) + case JNull => null + }, { + case d: LocalDate => + JString(d.format(DateTimeFormatter.ISO_LOCAL_DATE)) + })) + {{/java8}} + {{#joda}} + case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( { + case JString(s) => + ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s) + case JNull => null + }, { + case d: org.joda.time.DateTime => + JString(ISODateTimeFormat.dateTime().print(d)) + }) + ) + + case object LocalDateSerializer extends CustomSerializer[org.joda.time.LocalDate](_ => ( { + case JString(s) => org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(s) + case JNull => null + }, { + case d: org.joda.time.LocalDate => JString(d.toString("yyyy-MM-dd")) + })) + {{/joda}} + + def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ LocalDateSerializer :+ DateTimeSerializer +} +{{/json4s}} +{{#circe}} +trait DateSerializers { + import io.circe.{Decoder, Encoder} + {{#java8}} + implicit val isoOffsetDateTimeDecoder: Decoder[OffsetDateTime] = Decoder.decodeOffsetDateTimeWithFormatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + implicit val isoOffsetDateTimeEncoder: Encoder[OffsetDateTime] = Encoder.encodeOffsetDateTimeWithFormatter(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + + implicit val localDateDecoder: Decoder[LocalDate] = Decoder.decodeLocalDateWithFormatter(DateTimeFormatter.ISO_LOCAL_DATE) + implicit val localDateEncoder: Encoder[LocalDate] = Encoder.encodeLocalDateWithFormatter(DateTimeFormatter.ISO_LOCAL_DATE) + {{/java8}} + {{#joda}} + implicit val dateTimeDecoder: Decoder[DateTime] = Decoder.decodeString.map(ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(_)) + implicit val dateTimeEncoder: Encoder[DateTime] = Encoder.encodeString.contramap(ISODateTimeFormat.dateTime().print(_)) + + implicit val localDateDecoder: Decoder[org.joda.time.LocalDate] = Decoder.decodeString.map(org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(_)) + implicit val localDateEncoder: Encoder[org.joda.time.LocalDate] = Encoder.encodeString.contramap(_.toString("yyyy-MM-dd")) + {{/joda}} +} +{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/enumsSerializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/enumsSerializers.mustache deleted file mode 100644 index 8c7e6f2e41e..00000000000 --- a/modules/openapi-generator/src/main/resources/scala-sttp/enumsSerializers.mustache +++ /dev/null @@ -1,42 +0,0 @@ -{{>licenseInfo}} -package {{apiPackage}} - -{{#models.0}} -import {{modelPackage}}._ -{{/models.0}} -import org.json4s._ -import scala.reflect.ClassTag - -object EnumsSerializers { - - def all: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ - new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} - - private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) - extends Serializer[E#Value] { - import JsonDSL._ - - val EnumerationClass: Class[E#Value] = classOf[E#Value] - - def deserialize(implicit format: Formats): - PartialFunction[(TypeInfo, JValue), E#Value] = { - case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => - json match { - case JString(value) => - enum.withName(value) - case value => - throw new MappingException(s"Can't convert $value to $EnumerationClass") - } - } - - private[this] def isValid(json: JValue) = json match { - case JString(value) if enum.values.exists(_.toString == value) => true - case _ => false - } - - def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { - case i: E#Value => i.toString - } - } - -} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache new file mode 100644 index 00000000000..57c3ab4204c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp/jsonSupport.mustache @@ -0,0 +1,53 @@ +{{>licenseInfo}} +package {{invokerPackage}} + +{{#models.0}} +import {{modelPackage}}._ +{{/models.0}} +{{#json4s}} +import org.json4s._ +import sttp.client.json4s.SttpJson4sApi +import scala.reflect.ClassTag + +object JsonSupport extends SttpJson4sApi { + def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]](){{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ + new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} + + private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] { + import JsonDSL._ + val EnumerationClass: Class[E#Value] = classOf[E#Value] + + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { + case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => + json match { + case JString(value) => enum.withName(value) + case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") + } + } + + private[this] def isValid(json: JValue) = json match { + case JString(value) if enum.values.exists(_.toString == value) => true + case _ => false + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case i: E#Value => i.toString + } + } + + implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all + implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization +} +{{/json4s}} +{{#circe}} +import io.circe.{Decoder, Encoder} +import io.circe.generic.AutoDerivation +import sttp.client.circe.SttpCirceApi + +object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers { +{{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} + implicit val {{classname}}{{datatypeWithEnum}}Decoder: Decoder[{{classname}}Enums.{{datatypeWithEnum}}] = Decoder.decodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) + implicit val {{classname}}{{datatypeWithEnum}}Encoder: Encoder[{{classname}}Enums.{{datatypeWithEnum}}] = Encoder.encodeEnumeration({{classname}}Enums.{{datatypeWithEnum}}) +{{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} +} +{{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/methodParameters.mustache index 54dc2f92a51..84bfa581861 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/methodParameters.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/methodParameters.mustache @@ -1 +1 @@ -{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{#hasMore}}, {{/hasMore}}{{/allParams}}{{#authMethods.0}})(implicit {{#authMethods}}{{#isApiKey}}apiKey: ApiKeyValue{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}basicAuth: BasicCredentials{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: BearerToken{{/isBasicBearer}}{{/isBasic}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}{{/authMethods.0}} \ No newline at end of file +{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}apiKey: String{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{#hasMore}}, {{/hasMore}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{#hasMore}}, {{/hasMore}}{{/allParams}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache index 1f4da5283e3..f2cf2796234 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache @@ -4,7 +4,6 @@ package {{package}} {{#imports}} import {{import}} {{/imports}} -import {{invokerPackage}}.ApiModel {{#models}} {{#model}} @@ -23,7 +22,7 @@ case class {{classname}}( {{/description}} {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{classname}}Enums.{{datatypeWithEnum}}{{/isEnum}}{{^required}}] = None{{/required}}{{#hasMore}},{{/hasMore}} {{/vars}} -) extends ApiModel +) {{#hasEnums}} object {{classname}}Enums { diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/paramMultipartCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/paramMultipartCreation.mustache new file mode 100644 index 00000000000..edfb7b0766b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp/paramMultipartCreation.mustache @@ -0,0 +1,16 @@ +{{#required}} + {{#isFile}} + multipartFile("{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}) + {{/isFile}} + {{^isFile}} + multipart("{{baseName}}", {{paramName}}) + {{/isFile}} +{{/required}} +{{^required}} + {{#isFile}} + {{paramName}}.map(multipartFile("{{baseName}}", _)) + {{/isFile}} + {{^isFile}} + {{paramName}}.map(multipart("{{baseName}}", {{#isContainer}}ArrayValues(_{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}_{{/isContainer}})) + {{/isFile}} +{{/required}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/project/build.properties.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/project/build.properties.mustache new file mode 100644 index 00000000000..654fe70c42c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/scala-sttp/project/build.properties.mustache @@ -0,0 +1 @@ +sbt.version=1.3.12 diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/requests.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/requests.mustache deleted file mode 100644 index a39706e43f4..00000000000 --- a/modules/openapi-generator/src/main/resources/scala-sttp/requests.mustache +++ /dev/null @@ -1,48 +0,0 @@ -{{>licenseInfo}} -package {{invokerPackage}} - -import sttp.client.{Identity, RequestT, ResponseError} - -/** - * This trait needs to be added to any model defined by the api. - */ -trait ApiModel - -/** - * Sttp type aliases - */ -object alias { - type ApiRequestT[T] = RequestT[Identity, Either[ResponseError[Exception], T], Nothing] -} - -/** - * Single trait defining a credential that can be transformed to a paramName / paramValue tupple - */ -sealed trait Credentials { - def asQueryParam: Option[(String, String)] = None -} - -sealed case class BasicCredentials(user: String, password: String) extends Credentials - -sealed case class BearerToken(token: String) extends Credentials - -sealed case class ApiKeyCredentials(key: ApiKeyValue, keyName: String, location: ApiKeyLocation) extends Credentials { - override def asQueryParam: Option[(String, String)] = location match { - case ApiKeyLocations.QUERY => Some((keyName, key.value)) - case _ => None - } -} - -sealed case class ApiKeyValue(value: String) - -sealed trait ApiKeyLocation - -object ApiKeyLocations { - - case object QUERY extends ApiKeyLocation - - case object HEADER extends ApiKeyLocation - - case object COOKIE extends ApiKeyLocation - -} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp/serializers.mustache b/modules/openapi-generator/src/main/resources/scala-sttp/serializers.mustache deleted file mode 100644 index a17d7706915..00000000000 --- a/modules/openapi-generator/src/main/resources/scala-sttp/serializers.mustache +++ /dev/null @@ -1,57 +0,0 @@ -package {{invokerPackage}} - -{{#java8}} -import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} -import java.time.format.DateTimeFormatter -import scala.util.Try -{{/java8}} -{{#joda}} -import org.joda.time.DateTime -import org.joda.time.format.ISODateTimeFormat -{{/joda}} -import org.json4s.{Serializer, CustomSerializer, JNull} -import org.json4s.JsonAST.JString - -object Serializers { - -{{#java8}} - case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { - case JString(s) => - Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse - Try(LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toOffsetDateTime) getOrElse (null) - case JNull => null - }, { - case d: OffsetDateTime => - JString(d.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - })) - - case object LocalDateSerializer extends CustomSerializer[LocalDate]( _ => ( { - case JString(s) => LocalDate.parse(s) - case JNull => null - }, { - case d: LocalDate => - JString(d.format(DateTimeFormatter.ISO_LOCAL_DATE)) - })) -{{/java8}} -{{#joda}} - case object DateTimeSerializer extends CustomSerializer[DateTime](_ => ( { - case JString(s) => - ISODateTimeFormat.dateOptionalTimeParser().parseDateTime(s) - case JNull => null - }, { - case d: org.joda.time.DateTime => - JString(ISODateTimeFormat.dateTime().print(d)) - }) - ) - - case object LocalDateSerializer extends CustomSerializer[org.joda.time.LocalDate](_ => ( { - case JString(s) => org.joda.time.format.DateTimeFormat.forPattern("yyyy-MM-dd").parseLocalDate(s) - case JNull => null - }, { - case d: org.joda.time.LocalDate => JString(d.toString("yyyy-MM-dd")) - })) -{{/joda}} - - def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ LocalDateSerializer :+ DateTimeSerializer - -} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpBooleanPropertyTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpBooleanPropertyTest.java new file mode 100644 index 00000000000..c500dda7706 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpBooleanPropertyTest.java @@ -0,0 +1,29 @@ +package org.openapitools.codegen.scala; + +import org.junit.Assert; +import org.junit.Test; +import org.openapitools.codegen.languages.ScalaSttpClientCodegen; + +import java.util.HashMap; +import java.util.Map; + +public class SttpBooleanPropertyTest { + @Test + public void shouldUseDefaultValueIfAdditionalPropertiesAreEmpty() { + ScalaSttpClientCodegen.BooleanProperty booleanProperty = new ScalaSttpClientCodegen.BooleanProperty("k1", "desc", false); + Map additionalProperties = new HashMap<>(); + booleanProperty.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals(false, additionalProperties.get("k1")); + } + + @Test + public void shouldUseGivenValueIfProvided() { + ScalaSttpClientCodegen.BooleanProperty booleanProperty = new ScalaSttpClientCodegen.BooleanProperty("k1", "desc", false); + Map additionalProperties = new HashMap<>(); + additionalProperties.put("k1", true); + booleanProperty.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals(true, additionalProperties.get("k1")); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpJsonLibraryPropertyTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpJsonLibraryPropertyTest.java new file mode 100644 index 00000000000..d8473cb8f5d --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpJsonLibraryPropertyTest.java @@ -0,0 +1,39 @@ +package org.openapitools.codegen.scala; + +import org.junit.Assert; +import org.junit.Test; +import org.openapitools.codegen.languages.ScalaSttpClientCodegen; + +import java.util.HashMap; +import java.util.Map; + +public class SttpJsonLibraryPropertyTest { + @Test + public void shouldUseJson4sByDefault() { + ScalaSttpClientCodegen.JsonLibraryProperty property = new ScalaSttpClientCodegen.JsonLibraryProperty(); + Map additionalProperties = new HashMap<>(); + property.updateAdditionalProperties(additionalProperties); + Assert.assertEquals(true, additionalProperties.get("json4s")); + Assert.assertEquals(false, additionalProperties.get("circe")); + } + + @Test + public void shouldUseJson4sIfExplicitlyAskTo() { + ScalaSttpClientCodegen.JsonLibraryProperty property = new ScalaSttpClientCodegen.JsonLibraryProperty(); + Map additionalProperties = new HashMap<>(); + additionalProperties.put("jsonLibrary", "json4s"); + property.updateAdditionalProperties(additionalProperties); + Assert.assertEquals(true, additionalProperties.get("json4s")); + Assert.assertEquals(false, additionalProperties.get("circe")); + } + + @Test + public void shouldUseCirceIfExplicitlyAskTo() { + ScalaSttpClientCodegen.JsonLibraryProperty property = new ScalaSttpClientCodegen.JsonLibraryProperty(); + Map additionalProperties = new HashMap<>(); + additionalProperties.put("jsonLibrary", "circe"); + property.updateAdditionalProperties(additionalProperties); + Assert.assertEquals(false, additionalProperties.get("json4s")); + Assert.assertEquals(true, additionalProperties.get("circe")); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpPackagePropertyTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpPackagePropertyTest.java new file mode 100644 index 00000000000..7aa45197522 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpPackagePropertyTest.java @@ -0,0 +1,59 @@ +package org.openapitools.codegen.scala; + +import org.junit.Assert; +import org.junit.Test; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.languages.ScalaSttpClientCodegen; + +import java.util.HashMap; +import java.util.Map; + +public class SttpPackagePropertyTest { + @Test + public void shouldUseDefaultPackageNameIfAdditionalPropertiesAreEmpty(){ + ScalaSttpClientCodegen.PackageProperty property = new ScalaSttpClientCodegen.PackageProperty(); + Map additionalProperties = new HashMap<>(); + property.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals(ScalaSttpClientCodegen.DEFAULT_PACKAGE_NAME + ".api", + additionalProperties.get(CodegenConstants.API_PACKAGE)); + Assert.assertEquals(ScalaSttpClientCodegen.DEFAULT_PACKAGE_NAME + ".model", + additionalProperties.get(CodegenConstants.MODEL_PACKAGE)); + Assert.assertEquals(ScalaSttpClientCodegen.DEFAULT_PACKAGE_NAME + ".core", + additionalProperties.get(CodegenConstants.INVOKER_PACKAGE)); + } + + @Test + public void shouldUseCustomMainPackageNameIfProvided(){ + ScalaSttpClientCodegen.PackageProperty property = new ScalaSttpClientCodegen.PackageProperty(); + Map additionalProperties = new HashMap<>(); + String customPackageName = "my.custom.pkg.name"; + additionalProperties.put("mainPackage", customPackageName); + property.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals(customPackageName + ".api", + additionalProperties.get(CodegenConstants.API_PACKAGE)); + Assert.assertEquals(customPackageName + ".model", + additionalProperties.get(CodegenConstants.MODEL_PACKAGE)); + Assert.assertEquals(customPackageName + ".core", + additionalProperties.get(CodegenConstants.INVOKER_PACKAGE)); + } + + @Test + public void shouldAllowToMixCustomPackages(){ + ScalaSttpClientCodegen.PackageProperty property = new ScalaSttpClientCodegen.PackageProperty(); + Map additionalProperties = new HashMap<>(); + String customPackageName = "my.custom.pkg.name"; + additionalProperties.put("mainPackage", customPackageName); + String otherCustomPackageName = "some.other.custom.pkg.api"; + additionalProperties.put(CodegenConstants.API_PACKAGE, otherCustomPackageName); + property.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals(otherCustomPackageName, + additionalProperties.get(CodegenConstants.API_PACKAGE)); + Assert.assertEquals(customPackageName + ".model", + additionalProperties.get(CodegenConstants.MODEL_PACKAGE)); + Assert.assertEquals(customPackageName + ".core", + additionalProperties.get(CodegenConstants.INVOKER_PACKAGE)); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpStringPropertyTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpStringPropertyTest.java new file mode 100644 index 00000000000..f0db3bdafd3 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpStringPropertyTest.java @@ -0,0 +1,30 @@ +package org.openapitools.codegen.scala; + +import org.junit.Assert; +import org.junit.Test; +import org.openapitools.codegen.languages.ScalaSttpClientCodegen; + +import java.util.HashMap; +import java.util.Map; + +public class SttpStringPropertyTest { + + @Test + public void shouldUseDefaultValueIfAdditionalPropertiesAreEmpty(){ + ScalaSttpClientCodegen.StringProperty property = new ScalaSttpClientCodegen.StringProperty("k1", "desc", "default"); + Map additionalProperties = new HashMap<>(); + property.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals("default", additionalProperties.get("k1")); + } + + @Test + public void shouldUseGivenValueIfProvided(){ + ScalaSttpClientCodegen.StringProperty property = new ScalaSttpClientCodegen.StringProperty("k1", "desc", "default"); + Map additionalProperties = new HashMap<>(); + additionalProperties.put("k1", "custom"); + property.updateAdditionalProperties(additionalProperties); + + Assert.assertEquals("custom", additionalProperties.get("k1")); + } +} diff --git a/samples/client/petstore/scala-sttp/.openapi-generator/FILES b/samples/client/petstore/scala-sttp/.openapi-generator/FILES index ffe27d28081..bf41f88a7b3 100644 --- a/samples/client/petstore/scala-sttp/.openapi-generator/FILES +++ b/samples/client/petstore/scala-sttp/.openapi-generator/FILES @@ -1,12 +1,11 @@ README.md build.sbt -src/main/scala/org/openapitools/client/api/EnumsSerializers.scala +project/build.properties src/main/scala/org/openapitools/client/api/PetApi.scala src/main/scala/org/openapitools/client/api/StoreApi.scala src/main/scala/org/openapitools/client/api/UserApi.scala -src/main/scala/org/openapitools/client/core/ApiInvoker.scala -src/main/scala/org/openapitools/client/core/Serializers.scala -src/main/scala/org/openapitools/client/core/requests.scala +src/main/scala/org/openapitools/client/core/DateSerializers.scala +src/main/scala/org/openapitools/client/core/JsonSupport.scala src/main/scala/org/openapitools/client/model/ApiResponse.scala src/main/scala/org/openapitools/client/model/Category.scala src/main/scala/org/openapitools/client/model/InlineObject.scala diff --git a/samples/client/petstore/scala-sttp/build.sbt b/samples/client/petstore/scala-sttp/build.sbt index b940c8b6737..e7d77cdd2db 100644 --- a/samples/client/petstore/scala-sttp/build.sbt +++ b/samples/client/petstore/scala-sttp/build.sbt @@ -2,17 +2,13 @@ version := "1.0.0" name := "scala-sttp-petstore" organization := "org.openapitools" -scalaVersion := "2.13.0" - -crossScalaVersions := Seq(scalaVersion.value, "2.12.10", "2.11.12") +scalaVersion := "2.13.2" +crossScalaVersions := Seq(scalaVersion.value, "2.12.10") libraryDependencies ++= Seq( - "com.softwaremill.sttp.client" %% "core" % "2.0.0", - "com.softwaremill.sttp.client" %% "json4s" % "2.0.0", - "org.json4s" %% "json4s-jackson" % "3.6.7", - // test dependencies - "org.scalatest" %% "scalatest" % "3.0.8" % Test, - "junit" % "junit" % "4.13" % "test" + "com.softwaremill.sttp.client" %% "core" % "2.2.0", + "com.softwaremill.sttp.client" %% "json4s" % "2.2.0", + "org.json4s" %% "json4s-jackson" % "3.6.8" ) scalacOptions := Seq( @@ -20,5 +16,3 @@ scalacOptions := Seq( "-deprecation", "-feature" ) - -publishArtifact in (Compile, packageDoc) := false \ No newline at end of file diff --git a/samples/client/petstore/scala-sttp/project/build.properties b/samples/client/petstore/scala-sttp/project/build.properties new file mode 100644 index 00000000000..654fe70c42c --- /dev/null +++ b/samples/client/petstore/scala-sttp/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.3.12 diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/PetApi.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/PetApi.scala index 5211d09c9c5..034cceb63cc 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/PetApi.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/PetApi.scala @@ -14,20 +14,16 @@ package org.openapitools.client.api import org.openapitools.client.model.ApiResponse import java.io.File import org.openapitools.client.model.Pet -import org.openapitools.client.core._ -import alias._ +import org.openapitools.client.core.JsonSupport._ import sttp.client._ import sttp.model.Method object PetApi { - def apply(baseUrl: String = "http://petstore.swagger.io/v2")(implicit serializer: SttpSerializer) = new PetApi(baseUrl) +def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new PetApi(baseUrl) } -class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { - - import Helpers._ - import serializer._ +class PetApi(baseUrl: String) { /** * Expected answers: @@ -36,7 +32,8 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param pet Pet object that needs to be added to the store */ - def addPet(pet: Pet): ApiRequestT[Pet] = + def addPet(pet: Pet +): Request[Either[ResponseError[Exception], Pet], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/pet") .contentType("application/json") @@ -50,7 +47,8 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * @param petId Pet id to delete * @param apiKey */ - def deletePet(petId: Long, apiKey: Option[String] = None): ApiRequestT[Unit] = + def deletePet(petId: Long, apiKey: Option[String] = None +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.DELETE, uri"$baseUrl/pet/${petId}") .contentType("application/json") @@ -66,7 +64,8 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param status Status values that need to be considered for filter */ - def findPetsByStatus(status: Seq[String]): ApiRequestT[Seq[Pet]] = + def findPetsByStatus(status: Seq[String] +): Request[Either[ResponseError[Exception], Seq[Pet]], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/pet/findByStatus?status=$status") .contentType("application/json") @@ -81,7 +80,8 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param tags Tags to filter by */ - def findPetsByTags(tags: Seq[String]): ApiRequestT[Seq[Pet]] = + def findPetsByTags(tags: Seq[String] +): Request[Either[ResponseError[Exception], Seq[Pet]], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/pet/findByTags?tags=$tags") .contentType("application/json") @@ -100,11 +100,12 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param petId ID of pet to return */ - def getPetById(petId: Long)(implicit apiKey: ApiKeyValue): ApiRequestT[Pet] = + def getPetById(apiKey: String)(petId: Long +): Request[Either[ResponseError[Exception], Pet], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/pet/${petId}") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .response(asJson[Pet]) /** @@ -116,7 +117,8 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param pet Pet object that needs to be added to the store */ - def updatePet(pet: Pet): ApiRequestT[Pet] = + def updatePet(pet: Pet +): Request[Either[ResponseError[Exception], Pet], Nothing] = basicRequest .method(Method.PUT, uri"$baseUrl/pet") .contentType("application/json") @@ -131,13 +133,14 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * @param name Updated name of the pet * @param status Updated status of the pet */ - def updatePetWithForm(petId: Long, name: Option[String] = None, status: Option[String] = None): ApiRequestT[Unit] = + def updatePetWithForm(petId: Long, name: Option[String] = None, status: Option[String] = None +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/pet/${petId}") .contentType("application/x-www-form-urlencoded") .body(Map( - "name" -> name, - "status" -> status, + "name" -> name, + "status" -> status )) .response(asJson[Unit]) @@ -149,15 +152,17 @@ class PetApi(baseUrl: String)(implicit serializer: SttpSerializer) { * @param additionalMetadata Additional data to pass to server * @param file file to upload */ - def uploadFile(petId: Long, additionalMetadata: Option[String] = None, file: Option[File] = None): ApiRequestT[ApiResponse] = + def uploadFile(petId: Long, additionalMetadata: Option[String] = None, file: Option[File] = None +): Request[Either[ResponseError[Exception], ApiResponse], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/pet/${petId}/uploadImage") .contentType("multipart/form-data") - .body(Map( - "additionalMetadata" -> additionalMetadata, - "file" -> file, - )) + .multipartBody(Seq( + additionalMetadata.map(multipart("additionalMetadata", _)) +, + file.map(multipartFile("file", _)) + + ).flatten) .response(asJson[ApiResponse]) } - diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/StoreApi.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/StoreApi.scala index 907cc9f42f0..e6ffcaae629 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/StoreApi.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/StoreApi.scala @@ -12,20 +12,16 @@ package org.openapitools.client.api import org.openapitools.client.model.Order -import org.openapitools.client.core._ -import alias._ +import org.openapitools.client.core.JsonSupport._ import sttp.client._ import sttp.model.Method object StoreApi { - def apply(baseUrl: String = "http://petstore.swagger.io/v2")(implicit serializer: SttpSerializer) = new StoreApi(baseUrl) +def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new StoreApi(baseUrl) } -class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { - - import Helpers._ - import serializer._ +class StoreApi(baseUrl: String) { /** * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors @@ -36,7 +32,8 @@ class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param orderId ID of the order that needs to be deleted */ - def deleteOrder(orderId: String): ApiRequestT[Unit] = + def deleteOrder(orderId: String +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.DELETE, uri"$baseUrl/store/order/${orderId}") .contentType("application/json") @@ -51,11 +48,12 @@ class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { * Available security schemes: * api_key (apiKey) */ - def getInventory()(implicit apiKey: ApiKeyValue): ApiRequestT[Map[String, Int]] = + def getInventory(apiKey: String)( +): Request[Either[ResponseError[Exception], Map[String, Int]], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/store/inventory") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .response(asJson[Map[String, Int]]) /** @@ -68,7 +66,8 @@ class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param orderId ID of pet that needs to be fetched */ - def getOrderById(orderId: Long): ApiRequestT[Order] = + def getOrderById(orderId: Long +): Request[Either[ResponseError[Exception], Order], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/store/order/${orderId}") .contentType("application/json") @@ -81,7 +80,8 @@ class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param order order placed for purchasing the pet */ - def placeOrder(order: Order): ApiRequestT[Order] = + def placeOrder(order: Order +): Request[Either[ResponseError[Exception], Order], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/store/order") .contentType("application/json") @@ -89,4 +89,3 @@ class StoreApi(baseUrl: String)(implicit serializer: SttpSerializer) { .response(asJson[Order]) } - diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/UserApi.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/UserApi.scala index 34679dea5d7..fd3b82392b8 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/UserApi.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/UserApi.scala @@ -12,20 +12,16 @@ package org.openapitools.client.api import org.openapitools.client.model.User -import org.openapitools.client.core._ -import alias._ +import org.openapitools.client.core.JsonSupport._ import sttp.client._ import sttp.model.Method object UserApi { - def apply(baseUrl: String = "http://petstore.swagger.io/v2")(implicit serializer: SttpSerializer) = new UserApi(baseUrl) +def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new UserApi(baseUrl) } -class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { - - import Helpers._ - import serializer._ +class UserApi(baseUrl: String) { /** * This can only be done by the logged in user. @@ -38,11 +34,12 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param user Created user object */ - def createUser(user: User)(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def createUser(apiKey: String)(user: User +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/user") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .body(user) .response(asJson[Unit]) @@ -55,11 +52,12 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param user List of user object */ - def createUsersWithArrayInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def createUsersWithArrayInput(apiKey: String)(user: Seq[User] +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/user/createWithArray") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .body(user) .response(asJson[Unit]) @@ -72,11 +70,12 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param user List of user object */ - def createUsersWithListInput(user: Seq[User])(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def createUsersWithListInput(apiKey: String)(user: Seq[User] +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.POST, uri"$baseUrl/user/createWithList") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .body(user) .response(asJson[Unit]) @@ -92,11 +91,12 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param username The name that needs to be deleted */ - def deleteUser(username: String)(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def deleteUser(apiKey: String)(username: String +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.DELETE, uri"$baseUrl/user/${username}") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .response(asJson[Unit]) /** @@ -107,7 +107,8 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * * @param username The name that needs to be fetched. Use user1 for testing. */ - def getUserByName(username: String): ApiRequestT[User] = + def getUserByName(username: String +): Request[Either[ResponseError[Exception], User], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/user/${username}") .contentType("application/json") @@ -125,7 +126,8 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * @param username The user name for login * @param password The password for login in clear text */ - def loginUser(username: String, password: String): ApiRequestT[String] = + def loginUser(username: String, password: String +): Request[Either[ResponseError[Exception], String], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/user/login?username=$username&password=$password") .contentType("application/json") @@ -138,11 +140,12 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * Available security schemes: * api_key (apiKey) */ - def logoutUser()(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def logoutUser(apiKey: String)( +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.GET, uri"$baseUrl/user/logout") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .response(asJson[Unit]) /** @@ -158,13 +161,13 @@ class UserApi(baseUrl: String)(implicit serializer: SttpSerializer) { * @param username name that need to be deleted * @param user Updated user object */ - def updateUser(username: String, user: User)(implicit apiKey: ApiKeyValue): ApiRequestT[Unit] = + def updateUser(apiKey: String)(username: String, user: User +): Request[Either[ResponseError[Exception], Unit], Nothing] = basicRequest .method(Method.PUT, uri"$baseUrl/user/${username}") .contentType("application/json") - .header("api_key", apiKey.value) + .header("api_key", apiKey) .body(user) .response(asJson[Unit]) } - diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/ApiInvoker.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/ApiInvoker.scala deleted file mode 100644 index dc98ff4d136..00000000000 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/ApiInvoker.scala +++ /dev/null @@ -1,60 +0,0 @@ -/** - * OpenAPI Petstore - * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -package org.openapitools.client.core - -import org.json4s._ -import sttp.client._ -import sttp.model.StatusCode -import org.openapitools.client.api.EnumsSerializers -import sttp.client.json4s.SttpJson4sApi -import sttp.client.monad.MonadError - -class SttpSerializer(implicit val format: Formats = DefaultFormats ++ EnumsSerializers.all ++ Serializers.all, - implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization) extends SttpJson4sApi - -class HttpException(val statusCode: StatusCode, val statusText: String, val message: String) extends Exception(s"[$statusCode] $statusText: $message") - -object Helpers { - - // Helper to handle Optional header parameters - implicit class optionalParams(val request: RequestT[Identity, Either[String, String], Nothing]) extends AnyVal { - def header( header: String, optValue: Option[Any]): RequestT[Identity, Either[String, String], Nothing] = { - optValue.map( value => request.header(header, value.toString)).getOrElse(request) - } - } - -} - -object ApiInvoker { - - /** - * Allows request execution without calling apiInvoker.execute(request) - * request.result can be used to get a monad wrapped content. - * - * @param request the apiRequest to be executed - */ - implicit class ApiRequestImprovements[R[_], T](request: RequestT[Identity, Either[ResponseError[Exception], T], Nothing]) { - - def result(implicit backend: SttpBackend[R, Nothing, Nothing]): R[T] = { - val responseT = request.send() - val ME: MonadError[R] = backend.responseMonad - ME.flatMap(responseT) { - response => - response.body match { - case Left(ex) => ME.error[T](new HttpException(response.code, response.statusText, ex.body)) - case Right(value) => ME.unit(value) - } - } - } - } - -} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala new file mode 100644 index 00000000000..cb2d1d2aa43 --- /dev/null +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/DateSerializers.scala @@ -0,0 +1,29 @@ +package org.openapitools.client.core + +import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} +import java.time.format.DateTimeFormatter +import scala.util.Try + +object DateSerializers { + import org.json4s.{Serializer, CustomSerializer, JNull} + import org.json4s.JsonAST.JString + case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { + case JString(s) => + Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse + Try(LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toOffsetDateTime) getOrElse (null) + case JNull => null + }, { + case d: OffsetDateTime => + JString(d.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + })) + + case object LocalDateSerializer extends CustomSerializer[LocalDate]( _ => ( { + case JString(s) => LocalDate.parse(s) + case JNull => null + }, { + case d: LocalDate => + JString(d.format(DateTimeFormatter.ISO_LOCAL_DATE)) + })) + + def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ LocalDateSerializer :+ DateTimeSerializer +} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/EnumsSerializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala similarity index 62% rename from samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/EnumsSerializers.scala rename to samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala index 71ad618e31f..51896325c2a 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/api/EnumsSerializers.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/JsonSupport.scala @@ -9,32 +9,27 @@ * https://openapi-generator.tech * Do not edit the class manually. */ -package org.openapitools.client.api +package org.openapitools.client.core import org.openapitools.client.model._ import org.json4s._ +import sttp.client.json4s.SttpJson4sApi import scala.reflect.ClassTag -object EnumsSerializers { - - def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ +object JsonSupport extends SttpJson4sApi { + def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ new EnumNameSerializer(OrderEnums.Status) :+ new EnumNameSerializer(PetEnums.Status) - private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) - extends Serializer[E#Value] { + private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] { import JsonDSL._ - val EnumerationClass: Class[E#Value] = classOf[E#Value] - def deserialize(implicit format: Formats): - PartialFunction[(TypeInfo, JValue), E#Value] = { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = { case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => json match { - case JString(value) => - enum.withName(value) - case value => - throw new MappingException(s"Can't convert $value to $EnumerationClass") + case JString(value) => enum.withName(value) + case value => throw new MappingException(s"Can't convert $value to $EnumerationClass") } } @@ -45,7 +40,9 @@ object EnumsSerializers { def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { case i: E#Value => i.toString + } } - } + implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all + implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization } diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/Serializers.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/Serializers.scala deleted file mode 100644 index dbd13545c73..00000000000 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/Serializers.scala +++ /dev/null @@ -1,31 +0,0 @@ -package org.openapitools.client.core - -import java.time.{LocalDate, LocalDateTime, OffsetDateTime, ZoneId} -import java.time.format.DateTimeFormatter -import scala.util.Try -import org.json4s.{Serializer, CustomSerializer, JNull} -import org.json4s.JsonAST.JString - -object Serializers { - - case object DateTimeSerializer extends CustomSerializer[OffsetDateTime](_ => ( { - case JString(s) => - Try(OffsetDateTime.parse(s, DateTimeFormatter.ISO_OFFSET_DATE_TIME)) orElse - Try(LocalDateTime.parse(s).atZone(ZoneId.systemDefault()).toOffsetDateTime) getOrElse (null) - case JNull => null - }, { - case d: OffsetDateTime => - JString(d.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) - })) - - case object LocalDateSerializer extends CustomSerializer[LocalDate]( _ => ( { - case JString(s) => LocalDate.parse(s) - case JNull => null - }, { - case d: LocalDate => - JString(d.format(DateTimeFormatter.ISO_LOCAL_DATE)) - })) - - def all: Seq[Serializer[_]] = Seq[Serializer[_]]() :+ LocalDateSerializer :+ DateTimeSerializer - -} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/credentials.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/credentials.scala new file mode 100644 index 00000000000..97d9ddcec81 --- /dev/null +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/credentials.scala @@ -0,0 +1,18 @@ +/** + * OpenAPI Petstore + * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +package org.openapitools.client.core + +final case class BasicCredentials(user: String, password: String) + +final case class BearerToken(token: String) + +final case class ApiKeyValue(value: String) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/requests.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/requests.scala deleted file mode 100644 index 1f45be8103e..00000000000 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/core/requests.scala +++ /dev/null @@ -1,58 +0,0 @@ -/** - * OpenAPI Petstore - * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. - * - * The version of the OpenAPI document: 1.0.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ -package org.openapitools.client.core - -import sttp.client.{Identity, RequestT, ResponseError} - -/** - * This trait needs to be added to any model defined by the api. - */ -trait ApiModel - -/** - * Sttp type aliases - */ -object alias { - type ApiRequestT[T] = RequestT[Identity, Either[ResponseError[Exception], T], Nothing] -} - -/** - * Single trait defining a credential that can be transformed to a paramName / paramValue tupple - */ -sealed trait Credentials { - def asQueryParam: Option[(String, String)] = None -} - -sealed case class BasicCredentials(user: String, password: String) extends Credentials - -sealed case class BearerToken(token: String) extends Credentials - -sealed case class ApiKeyCredentials(key: ApiKeyValue, keyName: String, location: ApiKeyLocation) extends Credentials { - override def asQueryParam: Option[(String, String)] = location match { - case ApiKeyLocations.QUERY => Some((keyName, key.value)) - case _ => None - } -} - -sealed case class ApiKeyValue(value: String) - -sealed trait ApiKeyLocation - -object ApiKeyLocations { - - case object QUERY extends ApiKeyLocation - - case object HEADER extends ApiKeyLocation - - case object COOKIE extends ApiKeyLocation - -} diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/ApiResponse.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/ApiResponse.scala index 3a3b6d6f499..aaaeae4c340 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/ApiResponse.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/ApiResponse.scala @@ -11,7 +11,6 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel /** * An uploaded response @@ -21,6 +20,6 @@ case class ApiResponse( code: Option[Int] = None, `type`: Option[String] = None, message: Option[String] = None -) extends ApiModel +) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Category.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Category.scala index 011164617cf..81d132f226e 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Category.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Category.scala @@ -11,7 +11,6 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel /** * Pet category @@ -20,6 +19,6 @@ import org.openapitools.client.core.ApiModel case class Category( id: Option[Long] = None, name: Option[String] = None -) extends ApiModel +) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject.scala index a8c5493161a..bf97d86b279 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject.scala @@ -11,13 +11,12 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel case class InlineObject( /* Updated name of the pet */ name: Option[String] = None, /* Updated status of the pet */ status: Option[String] = None -) extends ApiModel +) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject1.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject1.scala index 480cf8c2e10..73014a0bfde 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject1.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/InlineObject1.scala @@ -12,13 +12,12 @@ package org.openapitools.client.model import java.io.File -import org.openapitools.client.core.ApiModel case class InlineObject1( /* Additional data to pass to server */ additionalMetadata: Option[String] = None, /* file to upload */ file: Option[File] = None -) extends ApiModel +) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Order.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Order.scala index baa0c0cb14a..222b0564f54 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Order.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Order.scala @@ -12,7 +12,6 @@ package org.openapitools.client.model import java.time.OffsetDateTime -import org.openapitools.client.core.ApiModel /** * Pet Order @@ -26,7 +25,7 @@ case class Order( /* Order Status */ status: Option[OrderEnums.Status] = None, complete: Option[Boolean] = None -) extends ApiModel +) object OrderEnums { diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Pet.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Pet.scala index 75b528c3c0a..0e48ba900d8 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Pet.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Pet.scala @@ -11,7 +11,6 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel /** * a Pet @@ -25,7 +24,7 @@ case class Pet( tags: Option[Seq[Tag]] = None, /* pet status in the store */ status: Option[PetEnums.Status] = None -) extends ApiModel +) object PetEnums { diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Tag.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Tag.scala index 299ee5161a8..d1ce00a9a84 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Tag.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/Tag.scala @@ -11,7 +11,6 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel /** * Pet Tag @@ -20,6 +19,6 @@ import org.openapitools.client.core.ApiModel case class Tag( id: Option[Long] = None, name: Option[String] = None -) extends ApiModel +) diff --git a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/User.scala b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/User.scala index bd2e6c3ba2a..729b4bcc658 100644 --- a/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/User.scala +++ b/samples/client/petstore/scala-sttp/src/main/scala/org/openapitools/client/model/User.scala @@ -11,7 +11,6 @@ */ package org.openapitools.client.model -import org.openapitools.client.core.ApiModel /** * a User @@ -27,6 +26,6 @@ case class User( phone: Option[String] = None, /* User Status */ userStatus: Option[Int] = None -) extends ApiModel +)