diff --git a/bin/configs/scala-sttp4-circe.yaml b/bin/configs/scala-sttp4-circe.yaml new file mode 100644 index 000000000000..330a9ae1803d --- /dev/null +++ b/bin/configs/scala-sttp4-circe.yaml @@ -0,0 +1,7 @@ +generatorName: scala-sttp4 +outputDir: samples/client/petstore/scala-sttp4-circe +inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/scala-sttp4 +additionalProperties: + hideGenerationTimestamp: "true" + jsonLibrary: "circe" diff --git a/docs/generators/scala-sttp4.md b/docs/generators/scala-sttp4.md index 2ff0143b0eda..b6ed04bc3242 100644 --- a/docs/generators/scala-sttp4.md +++ b/docs/generators/scala-sttp4.md @@ -20,6 +20,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl | ------ | ----------- | ------ | ------- | |allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| |apiPackage|package for generated api classes| |null| +|circeExtrasVersion|The version of circe-generic-extras library| |0.14.4| +|circeVersion|The version of circe library| |0.14.15| |dateLibrary|Option. Date library to use|
**joda**
Joda (for legacy app)
**java8**
Java 8 native JSR310 (preferred for JDK 1.8+)
|java8| |disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| |ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| @@ -36,7 +38,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| |sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| |sourceFolder|source folder for generated code| |null| -|sttpClientVersion|The version of sttp client| |4.0.0-M1| +|sttpClientVersion|The version of sttp client| |4.0.15| ## IMPORT MAPPING diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4ClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4ClientCodegen.java index 9c54671b8634..7385f7115518 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4ClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ScalaSttp4ClientCodegen.java @@ -24,7 +24,7 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements CodegenConfig { private static final StringProperty STTP_CLIENT_VERSION = new StringProperty("sttpClientVersion", "The version of " + - "sttp client", "4.0.0-M1"); + "sttp client", "4.0.15"); private static final BooleanProperty USE_SEPARATE_ERROR_CHANNEL = new BooleanProperty("separateErrorChannel", "Whether to return response as " + "F[Either[ResponseError[ErrorType], ReturnType]]] or to flatten " + @@ -33,6 +33,10 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements Cod "joda-time library", "2.10.13"); private static final StringProperty JSON4S_VERSION = new StringProperty("json4sVersion", "The version of json4s " + "library", "4.0.6"); + private static final StringProperty CIRCE_VERSION = new StringProperty("circeVersion", "The version of circe " + + "library", "0.14.15"); + private static final StringProperty CIRCE_EXTRAS_VERSION = new StringProperty("circeExtrasVersion", + "The version of circe-generic-extras library", "0.14.4"); private static final JsonLibraryProperty JSON_LIBRARY_PROPERTY = new JsonLibraryProperty(); @@ -41,7 +45,7 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements Cod private static final List> properties = Arrays.asList( STTP_CLIENT_VERSION, USE_SEPARATE_ERROR_CHANNEL, JODA_TIME_VERSION, - JSON4S_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY); + JSON4S_VERSION, CIRCE_VERSION, CIRCE_EXTRAS_VERSION, JSON_LIBRARY_PROPERTY, PACKAGE_PROPERTY); private final Logger LOGGER = LoggerFactory.getLogger(ScalaSttp4ClientCodegen.class); @@ -86,15 +90,17 @@ public ScalaSttp4ClientCodegen() { ) ); + // Enable oneOf interface generation + useOneOfInterfaces = true; + supportsMultipleInheritance = true; + supportsInheritance = true; + addOneOfInterfaceImports = true; + outputFolder = "generated-code/scala-sttp4"; modelTemplateFiles.put("model.mustache", ".scala"); apiTemplateFiles.put("api.mustache", ".scala"); embeddedTemplateDir = templateDir = "scala-sttp4"; - String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); - - String jsonValueClass = "circe".equals(jsonLibrary) ? "io.circe.Json" : "org.json4s.JValue"; - additionalProperties.put(CodegenConstants.GROUP_ID, groupId); additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); additionalProperties.put(CodegenConstants.ARTIFACT_VERSION, artifactVersion); @@ -124,13 +130,12 @@ public ScalaSttp4ClientCodegen() { 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"); typeMapping.put("decimal", "BigDecimal"); typeMapping.put("ByteArray", "Array[Byte]"); - typeMapping.put("AnyType", jsonValueClass); + // AnyType and object mapping will be set in processOpts() based on jsonLibrary instantiationTypes.put("array", "ListBuffer"); instantiationTypes.put("map", "Map"); @@ -149,6 +154,20 @@ public void processOpts() { apiPackage = PACKAGE_PROPERTY.getApiPackage(additionalProperties); modelPackage = PACKAGE_PROPERTY.getModelPackage(additionalProperties); + // Set AnyType and object mapping based on jsonLibrary + String jsonLibrary = JSON_LIBRARY_PROPERTY.getValue(additionalProperties); + if ("circe".equals(jsonLibrary)) { + typeMapping.put("AnyType", "io.circe.Json"); + typeMapping.put("object", "io.circe.JsonObject"); + importMapping.put("io.circe.Json", "io.circe.Json"); + importMapping.put("io.circe.JsonObject", "io.circe.JsonObject"); + } else { + typeMapping.put("AnyType", "org.json4s.JValue"); + typeMapping.put("object", "org.json4s.JObject"); + importMapping.put("org.json4s.JValue", "org.json4s.JValue"); + importMapping.put("org.json4s.JObject", "org.json4s.JObject"); + } + 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); @@ -211,6 +230,16 @@ public ModelsMap postProcessModels(ModelsMap objs) { return objs; } + private void setParameterDefaults(CodegenParameter param) { + // Set default values for optional parameters + // Template will handle Option[] wrapping, so all defaults should be None + if (!param.required) { + param.defaultValue = "None"; + } + } + + + /** * Invoked by {@link DefaultGenerator} after all models have been post-processed, * allowing for a last pass of codegen-specific model cleanup. @@ -221,6 +250,104 @@ public ModelsMap postProcessModels(ModelsMap objs) { @Override public Map postProcessAllModels(Map objs) { final Map processed = super.postProcessAllModels(objs); + + // First pass: count how many oneOf parents each model has + Map oneOfMemberCount = new HashMap<>(); + for (ModelsMap mm : processed.values()) { + for (ModelMap model : mm.getModels()) { + CodegenModel cModel = model.getModel(); + if (!cModel.oneOf.isEmpty()) { + for (String childName : cModel.oneOf) { + oneOfMemberCount.put(childName, oneOfMemberCount.getOrDefault(childName, 0) + 1); + } + } + } + } + + // Second pass: process models + for (ModelsMap mm : processed.values()) { + for (ModelMap model : mm.getModels()) { + CodegenModel cModel = model.getModel(); + + if (!cModel.oneOf.isEmpty()) { + cModel.getVendorExtensions().put("x-isSealedTrait", true); + + // Collect child models for inline generation + // Only inline if they are used exclusively by this oneOf parent + List childModels = new ArrayList<>(); + + for (String childName : cModel.oneOf) { + CodegenModel childModel = ModelUtils.getModelByName(childName, processed); + if (childModel != null && oneOfMemberCount.getOrDefault(childName, 0) == 1) { + // This child is only used by this parent - can be inlined + childModel.getVendorExtensions().put("x-isOneOfMember", true); + childModel.getVendorExtensions().put("x-oneOfParent", cModel.classname); + + // Add discriminator mapping value if present + if (cModel.discriminator != null) { + String discriminatorName = cModel.discriminator.getPropertyName(); + + // Find the mapping value for this child model + String discriminatorValue = null; + if (cModel.discriminator.getMappedModels() != null) { + for (CodegenDiscriminator.MappedModel mappedModel : cModel.discriminator.getMappedModels()) { + if (mappedModel.getModelName().equals(childName)) { + discriminatorValue = mappedModel.getMappingName(); + break; + } + } + } + + if (discriminatorValue != null) { + childModel.getVendorExtensions().put("x-discriminator-value", discriminatorValue); + } + + // Remove discriminator field from child + // (circe-generic-extras adds it automatically) + childModel.vars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + childModel.allVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + childModel.requiredVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + childModel.optionalVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + } + + childModels.add(childModel); + } + } + cModel.getVendorExtensions().put("x-oneOfMembers", childModels); + } else if (cModel.isEnum) { + cModel.getVendorExtensions().put("x-isEnum", true); + } else { + cModel.getVendorExtensions().put("x-isRegularModel", true); + } + + if (cModel.discriminator != null) { + cModel.getVendorExtensions().put("x-use-discr", true); + + if (cModel.discriminator.getMapping() != null) { + cModel.getVendorExtensions().put("x-use-discr-mapping", true); + } + } + + // Remove discriminator property from models that extend a oneOf parent + // (circe-generic-extras adds it automatically) + if (cModel.parent != null && cModel.parentModel != null && cModel.parentModel.discriminator != null) { + String discriminatorName = cModel.parentModel.discriminator.getPropertyName(); + cModel.vars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + cModel.allVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + cModel.requiredVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + cModel.optionalVars.removeIf(prop -> prop.baseName.equals(discriminatorName)); + } + } + } + + // Third pass: remove oneOf members from the map to skip file generation + // (they are already inlined in their parent sealed trait) + processed.entrySet().removeIf(entry -> { + ModelsMap mm = entry.getValue(); + return mm.getModels().stream() + .anyMatch(model -> model.getModel().getVendorExtensions().containsKey("x-isOneOfMember")); + }); + postProcessUpdateImports(processed); return processed; } @@ -349,6 +476,14 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, Listjavadoc}} {{/javadocRenderer}} - def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String, Exception], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = + def {{operationId}}({{>methodParameters}}): Request[{{#separateErrorChannel}}Either[ResponseException[String], {{>operationReturnType}}]{{/separateErrorChannel}}{{^separateErrorChannel}}{{>operationReturnType}}{{/separateErrorChannel}}] = basicRequest .method(Method.{{httpMethod.toUpperCase}}, uri"$baseUrl{{{path}}}{{#queryParams.0}}?{{/queryParams.0}}{{#queryParams}}{{baseName}}=${ {{paramName}} }{{^-last}}&{{/-last}}{{/queryParams}}{{#authMethods}}{{#isApiKey}}{{#isKeyInQuery}}{{#queryParams.0}}&{{/queryParams.0}}{{^queryParams.0}}?{{/queryParams.0}}{{keyParamName}}=${apiKeyQuery}{{/isKeyInQuery}}{{/isApiKey}}{{/authMethods}}") .contentType({{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}){{#headerParams}} @@ -35,7 +35,7 @@ class {{classname}}(baseUrl: String) { .multipartBody(Seq({{#formParams}} {{>paramMultipartCreation}}{{^-last}}, {{/-last}}{{/formParams}} ).flatten){{/isMultipart}}{{/formParams.0}}{{#bodyParam}} - .body({{paramName}}){{/bodyParam}} + .body(asJson({{paramName}})){{/bodyParam}} .response({{#separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))){{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}]{{/returnType}}{{/separateErrorChannel}}{{^separateErrorChannel}}{{^returnType}}asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(()))).getRight{{/returnType}}{{#returnType}}asJson[{{>operationReturnType}}].getRight{{/returnType}}{{/separateErrorChannel}}) {{/operation}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4/build.sbt.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4/build.sbt.mustache index d915432ab27a..0047155c9fde 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/build.sbt.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/build.sbt.mustache @@ -15,7 +15,9 @@ libraryDependencies ++= Seq( "org.json4s" %% "json4s-jackson" % "{{json4sVersion}}" {{/json4s}} {{#circe}} - "com.softwaremill.sttp.client4" %% "circe" % "{{sttpClientVersion}}" + "com.softwaremill.sttp.client4" %% "circe" % "{{sttpClientVersion}}", + "io.circe" %% "circe-generic" % "{{circeVersion}}", + "io.circe" %% "circe-generic-extras" % "{{circeExtrasVersion}}", {{/circe}} ) diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4/jsonSupport.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4/jsonSupport.mustache index d8f929c43225..c0ba583b2771 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/jsonSupport.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/jsonSupport.mustache @@ -43,17 +43,9 @@ object JsonSupport extends SttpJson4sApi { {{#circe}} import io.circe.{Decoder, Encoder} import io.circe.generic.AutoDerivation -import sttp.client3.circe.SttpCirceApi +import sttp.client4.circe.SttpCirceApi object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers { - -{{#models}} -{{#model}} -{{#isEnum}} - implicit val {{classname}}Decoder: Decoder[{{classname}}.{{classname}}] = Decoder.decodeEnumeration({{classname}}) - implicit val {{classname}}Encoder: Encoder[{{classname}}.{{classname}}] = Encoder.encodeEnumeration({{classname}}) -{{/isEnum}} -{{/model}} -{{/models}} + // Enum encoders/decoders are defined in their respective companion objects } {{/circe}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4/methodParameters.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4/methodParameters.mustache index 0f941eefee32..b735a87b18ce 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/methodParameters.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/methodParameters.mustache @@ -1 +1 @@ -{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}} \ No newline at end of file +{{#authMethods.0}}{{#authMethods}}{{#isApiKey}}{{#isKeyInHeader}}apiKeyHeader: String{{/isKeyInHeader}}{{#isKeyInQuery}}apiKeyQuery: String{{/isKeyInQuery}}{{#isKeyInCookie}}apiKeyCookie: String{{/isKeyInCookie}}{{/isApiKey}}{{#isBasic}}{{#isBasicBasic}}username: String, password: String{{/isBasicBasic}}{{#isBasicBearer}}bearerToken: String{{/isBasicBearer}}{{/isBasic}}{{^-last}}, {{/-last}}{{/authMethods}})({{/authMethods.0}}{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}Option[{{dataType}}]{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{^-last}}, {{/-last}}{{/allParams}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache index ecece27a59a5..b4483ab3b31b 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache @@ -7,6 +7,11 @@ import {{import}} {{#models}} {{#model}} +{{#vendorExtensions.x-isOneOfMember}} +// This case class is defined inline in {{vendorExtensions.x-oneOfParent}}.scala +// This file is intentionally minimal to avoid duplication. +{{/vendorExtensions.x-isOneOfMember}} +{{^vendorExtensions.x-isOneOfMember}} {{#description}} {{#javadocRenderer}} {{#title}} @@ -15,6 +20,169 @@ import {{import}} {{{description}}} {{/javadocRenderer}} {{/description}} +{{#vendorExtensions.x-isSealedTrait}} +sealed trait {{classname}} + +{{! Generate inline case classes for oneOf members }} +{{#vendorExtensions.x-oneOfMembers}} +case class {{classname}}( + {{#allVars}} + {{#description}} + /* {{{.}}} */ + {{/description}} + {{{name}}}: {{^required}}Option[{{/required}}{{dataType}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} + {{/allVars}} + {{^allVars}} + {{! Empty case class for models with no properties }} + {{/allVars}} +) extends {{vendorExtensions.x-oneOfParent}} +{{#circe}} +object {{classname}} { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[{{classname}}] = deriveEncoder + implicit val decoder: Decoder[{{classname}}] = deriveDecoder +} +{{/circe}} + +{{/vendorExtensions.x-oneOfMembers}} +object {{classname}} { +{{#json4s}} + import org.json4s._ + +{{^vendorExtensions.x-use-discr}} + // oneOf without discriminator - json4s custom serializer + implicit object {{classname}}Serializer extends Serializer[{{classname}}] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{classname}}] = { + case (TypeInfo(clazz, _), json) if classOf[{{classname}}].isAssignableFrom(clazz) => + // Try each oneOf type in order + {{#oneOf}} + Extraction.extract[{{.}}](json) match { + case x: {{.}} => return x + case _ => // continue + } + {{/oneOf}} + throw new MappingException(s"Can't convert $json to {{classname}}") + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + {{#oneOf}} + case x: {{.}} => Extraction.decompose(x) + {{/oneOf}} + } + } +{{/vendorExtensions.x-use-discr}} +{{#vendorExtensions.x-use-discr}} + // oneOf with discriminator + implicit object {{classname}}Serializer extends Serializer[{{classname}}] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{classname}}] = { + case (TypeInfo(clazz, _), json) if classOf[{{classname}}].isAssignableFrom(clazz) => + (json \ "{{discriminator.propertyName}}") match { + {{#oneOf}} + case JString("{{.}}") => Extraction.extract[{{.}}](json) + {{/oneOf}} + case _ => throw new MappingException(s"Unknown discriminator value in $json") + } + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + {{#oneOf}} + case x: {{.}} => Extraction.decompose(x).merge(JObject("{{discriminator.propertyName}}" -> JString("{{.}}"))) + {{/oneOf}} + } + } +{{/vendorExtensions.x-use-discr}} +{{/json4s}} +{{#circe}} +{{^vendorExtensions.x-use-discr}} + // oneOf without discriminator - using semiauto derivation + import io.circe.{Encoder, Decoder} + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[{{classname}}] = deriveEncoder + implicit val decoder: Decoder[{{classname}}] = deriveDecoder +{{/vendorExtensions.x-use-discr}} +{{#vendorExtensions.x-use-discr}} + // oneOf with discriminator - using semiauto derivation with Configuration + import io.circe.{Encoder, Decoder} + import io.circe.generic.extras._ + import io.circe.generic.extras.semiauto._ + + private implicit val config: Configuration = Configuration.default.withDiscriminator("{{discriminator.propertyName}}") + .copy( + transformConstructorNames = { +{{#vendorExtensions.x-oneOfMembers}} + case "{{classname}}" => "{{vendorExtensions.x-discriminator-value}}" +{{/vendorExtensions.x-oneOfMembers}} + case other => sys.error(s"Invalid {{classname}} discriminant: ${other}") + } + ) + implicit val encoder: Encoder[{{classname}}] = deriveConfiguredEncoder + implicit val decoder: Decoder[{{classname}}] = deriveConfiguredDecoder +{{/vendorExtensions.x-use-discr}} +{{/circe}} +} +{{/vendorExtensions.x-isSealedTrait}} +{{#vendorExtensions.x-isEnum}} +sealed trait {{classname}} + +object {{classname}} { +{{#allowableValues}} + {{#values}} + case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{classname}} + {{/values}} +{{/allowableValues}} + +{{#circe}} + import io.circe.{Encoder, Decoder} + + implicit val encoder: Encoder[{{classname}}] = Encoder.encodeString.contramap[{{classname}}] { +{{#allowableValues}} + {{#values}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} => "{{.}}" + {{/values}} +{{/allowableValues}} + } + + implicit val decoder: Decoder[{{classname}}] = Decoder.decodeString.emap { +{{#allowableValues}} + {{#values}} + case "{{.}}" => Right({{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}) + {{/values}} +{{/allowableValues}} + case other => Left(s"Invalid {{classname}}: $other") + } +{{/circe}} +{{#json4s}} + import org.json4s._ + + implicit object {{classname}}Serializer extends Serializer[{{classname}}] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{classname}}] = { + case (TypeInfo(clazz, _), json) if classOf[{{classname}}].isAssignableFrom(clazz) => + json match { +{{#allowableValues}} + {{#values}} + case JString("{{.}}") => {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} + {{/values}} +{{/allowableValues}} + case other => throw new MappingException(s"Invalid {{classname}}: $other") + } + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { +{{#allowableValues}} + {{#values}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} => JString("{{.}}") + {{/values}} +{{/allowableValues}} + } + } +{{/json4s}} +} +{{/vendorExtensions.x-isEnum}} +{{#vendorExtensions.x-isRegularModel}} {{^isEnum}} case class {{classname}}( {{#vars}} @@ -23,38 +191,72 @@ case class {{classname}}( {{/description}} {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}] = None{{/required}}{{^-last}},{{/-last}} {{/vars}} -) +){{#parent}} extends {{parent}}{{/parent}} {{/isEnum}} +{{#circe}} +object {{classname}} { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ -{{#isEnum}} -object {{classname}} extends Enumeration { - type {{classname}} = {{classname}}.Value -{{#allowableValues}} - {{#values}} - val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") - {{/values}} -{{/allowableValues}} + implicit val encoder: Encoder[{{classname}}] = deriveEncoder + implicit val decoder: Decoder[{{classname}}] = deriveDecoder } -{{/isEnum}} +{{/circe}} {{#hasEnums}} object {{classname}}Enums { - {{#vars}} {{#isEnum}} - type {{datatypeWithEnum}} = {{datatypeWithEnum}}.Value - {{/isEnum}} - {{/vars}} - {{#vars}} - {{#isEnum}} - object {{datatypeWithEnum}} extends Enumeration { + + sealed trait {{datatypeWithEnum}} + object {{datatypeWithEnum}} { {{#_enum}} - val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") + case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{datatypeWithEnum}} {{/_enum}} - } +{{#circe}} + import io.circe.{Encoder, Decoder} + + implicit val encoder: Encoder[{{datatypeWithEnum}}] = Encoder.encodeString.contramap[{{datatypeWithEnum}}] { +{{#_enum}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} => "{{.}}" +{{/_enum}} + } + + implicit val decoder: Decoder[{{datatypeWithEnum}}] = Decoder.decodeString.emap { +{{#_enum}} + case "{{.}}" => Right({{#fnEnumEntry}}{{.}}{{/fnEnumEntry}}) +{{/_enum}} + case other => Left(s"Invalid {{datatypeWithEnum}}: $other") + } +{{/circe}} +{{#json4s}} + import org.json4s._ + + implicit object {{datatypeWithEnum}}Serializer extends Serializer[{{datatypeWithEnum}}] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), {{datatypeWithEnum}}] = { + case (TypeInfo(clazz, _), json) if classOf[{{datatypeWithEnum}}].isAssignableFrom(clazz) => + json match { +{{#_enum}} + case JString("{{.}}") => {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} +{{/_enum}} + case other => throw new MappingException(s"Invalid {{datatypeWithEnum}}: $other") + } + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { +{{#_enum}} + case {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} => JString("{{.}}") +{{/_enum}} + } + } +{{/json4s}} + } {{/isEnum}} {{/vars}} } {{/hasEnums}} +{{/vendorExtensions.x-isRegularModel}} +{{/vendorExtensions.x-isOneOfMember}} {{/model}} {{/models}} diff --git a/modules/openapi-generator/src/main/resources/scala-sttp4/paramCreation.mustache b/modules/openapi-generator/src/main/resources/scala-sttp4/paramCreation.mustache index 25ec73e8d5e1..be9104b3995d 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/paramCreation.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/paramCreation.mustache @@ -1 +1 @@ -"{{baseName}}", {{#isContainer}}ArrayValues({{{paramName}}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{{paramName}}}{{/isContainer}}.toString \ No newline at end of file +"{{baseName}}", {{#isContainer}}{{#required}}{{{paramName}}}.mkString(","){{/required}}{{^required}}{{{paramName}}}.filter(_.nonEmpty).map(_.mkString(",")){{/required}}{{/isContainer}}{{^isContainer}}{{#required}}{{{paramName}}}.toString{{/required}}{{^required}}{{{paramName}}}{{/required}}{{/isContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/Sttp4CodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/Sttp4CodegenTest.java index 74f7f43ae1e1..94ff72f40ed7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/Sttp4CodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/Sttp4CodegenTest.java @@ -53,4 +53,159 @@ public void verifyApiKeyLocations() throws IOException { assertFileContains(path, ".header(\"X-Api-Key\", apiKeyHeader)"); assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)"); } + + @Test + public void verifyOneOfSupportWithCirce() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/scala/sttp4-oneOf.yaml", null, new ParseOptions()).getOpenAPI(); + + ScalaSttp4ClientCodegen codegen = new ScalaSttp4ClientCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put("jsonLibrary", "circe"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.opts(input).generate(); + + // Test oneOf without discriminator generates sealed trait with semiauto + Path petPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Pet.scala"); + assertFileContains(petPath, "sealed trait Pet"); + assertFileContains(petPath, "object Pet {"); + assertFileContains(petPath, "import io.circe.generic.semiauto._"); + assertFileContains(petPath, "// oneOf without discriminator - using semiauto derivation"); + assertFileContains(petPath, "implicit val encoder: Encoder[Pet] = deriveEncoder"); + assertFileContains(petPath, "implicit val decoder: Decoder[Pet] = deriveDecoder"); + + // Test oneOf with discriminator uses semiauto with Configuration + Path animalPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Animal.scala"); + assertFileContains(animalPath, "sealed trait Animal"); + assertFileContains(animalPath, "object Animal {"); + assertFileContains(animalPath, "import io.circe.generic.extras.semiauto._"); + assertFileContains(animalPath, "// oneOf with discriminator - using semiauto derivation with Configuration"); + assertFileContains(animalPath, + "private implicit val config: Configuration = Configuration.default.withDiscriminator(\"petType\")"); + assertFileContains(animalPath, "implicit val encoder: Encoder[Animal] = deriveConfiguredEncoder"); + assertFileContains(animalPath, "implicit val decoder: Decoder[Animal] = deriveConfiguredDecoder"); + + // Test oneOf with discriminator mapping + Path vehiclePath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Vehicle.scala"); + assertFileContains(vehiclePath, "sealed trait Vehicle"); + assertFileContains(vehiclePath, "object Vehicle {"); + assertFileContains(vehiclePath, "// oneOf with discriminator - using semiauto derivation with Configuration"); + assertFileContains(vehiclePath, + "private implicit val config: Configuration = Configuration.default.withDiscriminator(\"vehicleType\")"); + assertFileContains(vehiclePath, "\"Car\" => \"car\""); + assertFileContains(vehiclePath, "\"Truck\" => \"truck\""); + + // Verify regular models are still case classes + Path dogPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Dog.scala"); + assertFileContains(dogPath, "case class Dog("); + assertFileContains(dogPath, "name: String"); + assertFileContains(dogPath, "breed: String"); + } + + @Test + public void verifyOneOfSupportWithJson4s() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/scala/sttp4-oneOf.yaml", null, new ParseOptions()).getOpenAPI(); + + ScalaSttp4ClientCodegen codegen = new ScalaSttp4ClientCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put("jsonLibrary", "json4s"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.opts(input).generate(); + + // Test oneOf without discriminator generates sealed trait with json4s + Path petPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Pet.scala"); + assertFileContains(petPath, "sealed trait Pet"); + assertFileContains(petPath, "object Pet {"); + assertFileContains(petPath, "import org.json4s._"); + assertFileContains(petPath, "// oneOf without discriminator - json4s custom serializer"); + assertFileContains(petPath, "implicit object PetSerializer extends Serializer[Pet]"); + assertFileContains(petPath, "Extraction.extract[Dog](json)"); + assertFileContains(petPath, "Extraction.extract[Cat](json)"); + + // Test oneOf with discriminator + Path animalPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Animal.scala"); + assertFileContains(animalPath, "sealed trait Animal"); + assertFileContains(animalPath, "// oneOf with discriminator"); + assertFileContains(animalPath, "petType"); + } + + @Test + public void verifyOneOfWithEmptyMembers() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + OpenAPI openAPI = new OpenAPIParser() + .readLocation("src/test/resources/3_0/scala/sttp4-oneOf-empty-members.yaml", null, new ParseOptions()) + .getOpenAPI(); + + ScalaSttp4ClientCodegen codegen = new ScalaSttp4ClientCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put("jsonLibrary", "circe"); + + ClientOptInput input = new ClientOptInput(); + input.openAPI(openAPI); + input.config(codegen); + + DefaultGenerator generator = new DefaultGenerator(); + + generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.opts(input).generate(); + + // Test sealed trait is generated correctly + Path eventPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Event.scala"); + assertFileContains(eventPath, "sealed trait Event"); + + // Test empty case classes (no properties except discriminator which was + // removed) + assertFileContains(eventPath, "case class ClickEvent(\n) extends Event"); + assertFileContains(eventPath, "case class ViewEvent(\n) extends Event"); + + // Test case class with properties (PurchaseEvent has amount) + assertFileContains(eventPath, "case class PurchaseEvent("); + assertFileContains(eventPath, "amount: Double"); + + // Verify discriminator is configured + assertFileContains(eventPath, "Configuration.default.withDiscriminator(\"eventType\")"); + + // Verify the discriminator property was removed from inline members + // ClickEvent and ViewEvent should have NO properties at all + assertFileContains(eventPath, "case class ClickEvent(\n) extends Event"); + assertFileContains(eventPath, "case class ViewEvent(\n) extends Event"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf-empty-members.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf-empty-members.yaml new file mode 100644 index 000000000000..3d2be6e556db --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf-empty-members.yaml @@ -0,0 +1,61 @@ +openapi: 3.0.0 +info: + title: OneOf Empty Members Test + version: 1.0.0 +paths: + /event: + post: + operationId: sendEvent + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + responses: + '200': + description: Success +components: + schemas: + Event: + oneOf: + - $ref: '#/components/schemas/ClickEvent' + - $ref: '#/components/schemas/ViewEvent' + - $ref: '#/components/schemas/PurchaseEvent' + discriminator: + propertyName: eventType + mapping: + click: '#/components/schemas/ClickEvent' + view: '#/components/schemas/ViewEvent' + purchase: '#/components/schemas/PurchaseEvent' + + ClickEvent: + type: object + required: + - eventType + properties: + eventType: + type: string + const: click + + ViewEvent: + type: object + required: + - eventType + properties: + eventType: + type: string + const: view + + PurchaseEvent: + type: object + required: + - eventType + - amount + properties: + eventType: + type: string + const: purchase + amount: + type: number + description: Purchase amount diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf.yaml new file mode 100644 index 000000000000..480996ce2d06 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-oneOf.yaml @@ -0,0 +1,107 @@ +openapi: 3.0.0 +info: + title: OneOf Test API + version: 1.0.0 +paths: + /pets: + get: + operationId: getPet + responses: + '200': + description: A pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' +components: + schemas: + Dog: + type: object + required: + - petType + - name + - breed + properties: + petType: + type: string + name: + type: string + breed: + type: string + barkVolume: + type: integer + + Cat: + type: object + required: + - petType + - name + - age + properties: + petType: + type: string + name: + type: string + age: + type: integer + indoor: + type: boolean + + # OneOf without discriminator + Pet: + oneOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + + # OneOf with discriminator + Animal: + oneOf: + - $ref: '#/components/schemas/Dog' + - $ref: '#/components/schemas/Cat' + discriminator: + propertyName: petType + + # OneOf with discriminator and mapping + Vehicle: + oneOf: + - $ref: '#/components/schemas/Car' + - $ref: '#/components/schemas/Truck' + discriminator: + propertyName: vehicleType + mapping: + car: '#/components/schemas/Car' + truck: '#/components/schemas/Truck' + + Car: + type: object + required: + - vehicleType + - make + - model + properties: + vehicleType: + type: string + const: car + make: + type: string + model: + type: string + doors: + type: integer + + Truck: + type: object + required: + - vehicleType + - make + - model + properties: + vehicleType: + type: string + const: truck + make: + type: string + model: + type: string + bedLength: + type: number diff --git a/samples/client/petstore/scala-sttp4-circe/.openapi-generator-ignore b/samples/client/petstore/scala-sttp4-circe/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/.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/client/petstore/scala-sttp4-circe/.openapi-generator/FILES b/samples/client/petstore/scala-sttp4-circe/.openapi-generator/FILES new file mode 100644 index 000000000000..93bed4b07d22 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/.openapi-generator/FILES @@ -0,0 +1,15 @@ +README.md +build.sbt +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/AdditionalTypeSerializers.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/Order.scala +src/main/scala/org/openapitools/client/model/Pet.scala +src/main/scala/org/openapitools/client/model/Tag.scala +src/main/scala/org/openapitools/client/model/User.scala diff --git a/samples/client/petstore/scala-sttp4-circe/.openapi-generator/VERSION b/samples/client/petstore/scala-sttp4-circe/.openapi-generator/VERSION new file mode 100644 index 000000000000..193a12d6e891 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.20.0-SNAPSHOT diff --git a/samples/client/petstore/scala-sttp4-circe/README.md b/samples/client/petstore/scala-sttp4-circe/README.md new file mode 100644 index 000000000000..e2d5251a5750 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/README.md @@ -0,0 +1,117 @@ +# openapi-client + +OpenAPI Petstore +- API version: 1.0.0 + - Generator version: 7.20.0-SNAPSHOT + +This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + + +*Automatically generated by the [OpenAPI Generator](https://openapi-generator.tech)* + +## Requirements + +Building the API client library requires: +1. Java 1.7+ +2. Maven/Gradle/SBT + +## Installation + +To install the API client library to your local Maven repository, simply execute: + +```shell +mvn clean install +``` + +To deploy it to a remote Maven repository instead, configure the settings of the repository and execute: + +```shell +mvn clean deploy +``` + +Refer to the [OSSRH Guide](http://central.sonatype.org/pages/ossrh-guide.html) for more information. + +### Maven users + +Add this dependency to your project's POM: + +```xml + + org.openapitools + openapi-client + 1.0.0 + compile + +``` + +### Gradle users + +Add this dependency to your project's build file: + +```groovy +compile "org.openapitools:openapi-client:1.0.0" +``` + +### SBT users + +```scala +libraryDependencies += "org.openapitools" % "openapi-client" % "1.0.0" +``` + +## Getting Started + +## Documentation for API Endpoints + +All URIs are relative to *http://petstore.swagger.io/v2* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*PetApi* | **addPet** | **POST** /pet | Add a new pet to the store +*PetApi* | **deletePet** | **DELETE** /pet/${petId} | Deletes a pet +*PetApi* | **findPetsByStatus** | **GET** /pet/findByStatus | Finds Pets by status +*PetApi* | **findPetsByTags** | **GET** /pet/findByTags | Finds Pets by tags +*PetApi* | **getPetById** | **GET** /pet/${petId} | Find pet by ID +*PetApi* | **updatePet** | **PUT** /pet | Update an existing pet +*PetApi* | **updatePetWithForm** | **POST** /pet/${petId} | Updates a pet in the store with form data +*PetApi* | **uploadFile** | **POST** /pet/${petId}/uploadImage | uploads an image +*StoreApi* | **deleteOrder** | **DELETE** /store/order/${orderId} | Delete purchase order by ID +*StoreApi* | **getInventory** | **GET** /store/inventory | Returns pet inventories by status +*StoreApi* | **getOrderById** | **GET** /store/order/${orderId} | Find purchase order by ID +*StoreApi* | **placeOrder** | **POST** /store/order | Place an order for a pet +*UserApi* | **createUser** | **POST** /user | Create user +*UserApi* | **createUsersWithArrayInput** | **POST** /user/createWithArray | Creates list of users with given input array +*UserApi* | **createUsersWithListInput** | **POST** /user/createWithList | Creates list of users with given input array +*UserApi* | **deleteUser** | **DELETE** /user/${username} | Delete user +*UserApi* | **getUserByName** | **GET** /user/${username} | Get user by user name +*UserApi* | **loginUser** | **GET** /user/login | Logs user into the system +*UserApi* | **logoutUser** | **GET** /user/logout | Logs out current logged in user session +*UserApi* | **updateUser** | **PUT** /user/${username} | Updated user + + +## Documentation for Models + + - [ApiResponse](ApiResponse.md) + - [Category](Category.md) + - [Order](Order.md) + - [Pet](Pet.md) + - [Tag](Tag.md) + - [User](User.md) + + + +## Documentation for Authorization + + +Authentication schemes defined for the API: + + ### api_key + + - **Type**: API key + - **API key parameter name**: api_key + - **Location**: HTTP header + + +## Author + + + diff --git a/samples/client/petstore/scala-sttp4-circe/build.sbt b/samples/client/petstore/scala-sttp4-circe/build.sbt new file mode 100644 index 000000000000..7d3f14401015 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/build.sbt @@ -0,0 +1,19 @@ +version := "1.0.0" +name := "openapi-client" +organization := "org.openapitools" + +scalaVersion := "2.13.16" +crossScalaVersions := Seq(scalaVersion.value, "2.12.20") + +libraryDependencies ++= Seq( + "com.softwaremill.sttp.client4" %% "core" % "4.0.15", + "com.softwaremill.sttp.client4" %% "circe" % "4.0.15", + "io.circe" %% "circe-generic" % "0.14.15", + "io.circe" %% "circe-generic-extras" % "0.14.4", +) + +scalacOptions := Seq( + "-unchecked", + "-deprecation", + "-feature" +) diff --git a/samples/client/petstore/scala-sttp4-circe/project/build.properties b/samples/client/petstore/scala-sttp4-circe/project/build.properties new file mode 100644 index 000000000000..cc68b53f1a30 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.11 diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/PetApi.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/PetApi.scala new file mode 100644 index 000000000000..3744397ca2d1 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/PetApi.scala @@ -0,0 +1,169 @@ +/** + * 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.api + +import org.openapitools.client.model.ApiResponse +import java.io.File +import org.openapitools.client.model.Pet +import org.openapitools.client.core.JsonSupport._ +import sttp.client4._ +import sttp.model.Method + +object PetApi { + def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new PetApi(baseUrl) +} + +class PetApi(baseUrl: String) { + + /** + * + * + * Expected answers: + * code 200 : Pet (successful operation) + * code 405 : (Invalid input) + * + * @param pet Pet object that needs to be added to the store + */ + def addPet(pet: Pet): Request[Either[ResponseException[String], Pet]] = + basicRequest + .method(Method.POST, uri"$baseUrl/pet") + .contentType("application/json") + .body(asJson(pet)) + .response(asJson[Pet]) + + /** + * + * + * Expected answers: + * code 400 : (Invalid pet value) + * + * @param petId Pet id to delete + * @param apiKey + */ + def deletePet(petId: Long, apiKey: Option[String] = None): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.DELETE, uri"$baseUrl/pet/${petId}") + .contentType("application/json") + .header("api_key", apiKey) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * Multiple status values can be provided with comma separated strings + * + * Expected answers: + * code 200 : Seq[Pet] (successful operation) + * code 400 : (Invalid status value) + * + * @param status Status values that need to be considered for filter + */ + def findPetsByStatus(status: Seq[String] = Seq.empty): Request[Either[ResponseException[String], Seq[Pet]]] = + basicRequest + .method(Method.GET, uri"$baseUrl/pet/findByStatus?status=${ status }") + .contentType("application/json") + .response(asJson[Seq[Pet]]) + + /** + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + * + * Expected answers: + * code 200 : Seq[Pet] (successful operation) + * code 400 : (Invalid tag value) + * + * @param tags Tags to filter by + */ + def findPetsByTags(tags: Seq[String] = Seq.empty): Request[Either[ResponseException[String], Seq[Pet]]] = + basicRequest + .method(Method.GET, uri"$baseUrl/pet/findByTags?tags=${ tags }") + .contentType("application/json") + .response(asJson[Seq[Pet]]) + + /** + * Returns a single pet + * + * Expected answers: + * code 200 : Pet (successful operation) + * code 400 : (Invalid ID supplied) + * code 404 : (Pet not found) + * + * Available security schemes: + * api_key (apiKey) + * + * @param petId ID of pet to return + */ + def getPetById(apiKeyHeader: String)(petId: Long): Request[Either[ResponseException[String], Pet]] = + basicRequest + .method(Method.GET, uri"$baseUrl/pet/${petId}") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .response(asJson[Pet]) + + /** + * + * + * Expected answers: + * code 200 : Pet (successful operation) + * code 400 : (Invalid ID supplied) + * code 404 : (Pet not found) + * code 405 : (Validation exception) + * + * @param pet Pet object that needs to be added to the store + */ + def updatePet(pet: Pet): Request[Either[ResponseException[String], Pet]] = + basicRequest + .method(Method.PUT, uri"$baseUrl/pet") + .contentType("application/json") + .body(asJson(pet)) + .response(asJson[Pet]) + + /** + * + * + * Expected answers: + * code 405 : (Invalid input) + * + * @param petId ID of pet that needs to be updated + * @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): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.POST, uri"$baseUrl/pet/${petId}") + .contentType("application/x-www-form-urlencoded") + .body(Map( + "name" -> name, + "status" -> status + )) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * + * + * Expected answers: + * code 200 : ApiResponse (successful operation) + * + * @param petId ID of pet to update + * @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): Request[Either[ResponseException[String], ApiResponse]] = + basicRequest + .method(Method.POST, uri"$baseUrl/pet/${petId}/uploadImage") + .contentType("multipart/form-data") + .multipartBody(Seq( + additionalMetadata.map(multipart("additionalMetadata", _)) +, + file.map(multipartFile("file", _)) + + ).flatten) + .response(asJson[ApiResponse]) + +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/StoreApi.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/StoreApi.scala new file mode 100644 index 000000000000..27efb05281b1 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/StoreApi.scala @@ -0,0 +1,88 @@ +/** + * 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.api + +import org.openapitools.client.model.Order +import org.openapitools.client.core.JsonSupport._ +import sttp.client4._ +import sttp.model.Method + +object StoreApi { + def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new StoreApi(baseUrl) +} + +class StoreApi(baseUrl: String) { + + /** + * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + * + * Expected answers: + * code 400 : (Invalid ID supplied) + * code 404 : (Order not found) + * + * @param orderId ID of the order that needs to be deleted + */ + def deleteOrder(orderId: String): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.DELETE, uri"$baseUrl/store/order/${orderId}") + .contentType("application/json") + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * Returns a map of status codes to quantities + * + * Expected answers: + * code 200 : Map[String, Int] (successful operation) + * + * Available security schemes: + * api_key (apiKey) + */ + def getInventory(apiKeyHeader: String)(): Request[Either[ResponseException[String], Map[String, Int]]] = + basicRequest + .method(Method.GET, uri"$baseUrl/store/inventory") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .response(asJson[Map[String, Int]]) + + /** + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions + * + * Expected answers: + * code 200 : Order (successful operation) + * code 400 : (Invalid ID supplied) + * code 404 : (Order not found) + * + * @param orderId ID of pet that needs to be fetched + */ + def getOrderById(orderId: Long): Request[Either[ResponseException[String], Order]] = + basicRequest + .method(Method.GET, uri"$baseUrl/store/order/${orderId}") + .contentType("application/json") + .response(asJson[Order]) + + /** + * + * + * Expected answers: + * code 200 : Order (successful operation) + * code 400 : (Invalid Order) + * + * @param order order placed for purchasing the pet + */ + def placeOrder(order: Order): Request[Either[ResponseException[String], Order]] = + basicRequest + .method(Method.POST, uri"$baseUrl/store/order") + .contentType("application/json") + .body(asJson(order)) + .response(asJson[Order]) + +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/UserApi.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/UserApi.scala new file mode 100644 index 000000000000..176c21e2b316 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/api/UserApi.scala @@ -0,0 +1,175 @@ +/** + * 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.api + +import java.time.OffsetDateTime +import org.openapitools.client.model.User +import org.openapitools.client.core.JsonSupport._ +import sttp.client4._ +import sttp.model.Method + +object UserApi { + def apply(baseUrl: String = "http://petstore.swagger.io/v2") = new UserApi(baseUrl) +} + +class UserApi(baseUrl: String) { + + /** + * This can only be done by the logged in user. + * + * Expected answers: + * code 0 : (successful operation) + * + * Available security schemes: + * api_key (apiKey) + * + * @param user Created user object + */ + def createUser(apiKeyHeader: String)(user: User): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.POST, uri"$baseUrl/user") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .body(asJson(user)) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * + * + * Expected answers: + * code 0 : (successful operation) + * + * Available security schemes: + * api_key (apiKey) + * + * @param user List of user object + */ + def createUsersWithArrayInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.POST, uri"$baseUrl/user/createWithArray") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .body(asJson(user)) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * + * + * Expected answers: + * code 0 : (successful operation) + * + * Available security schemes: + * api_key (apiKey) + * + * @param user List of user object + */ + def createUsersWithListInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.POST, uri"$baseUrl/user/createWithList") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .body(asJson(user)) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * This can only be done by the logged in user. + * + * Expected answers: + * code 400 : (Invalid username supplied) + * code 404 : (User not found) + * + * Available security schemes: + * api_key (apiKey) + * + * @param username The name that needs to be deleted + */ + def deleteUser(apiKeyHeader: String)(username: String): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.DELETE, uri"$baseUrl/user/${username}") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * + * + * Expected answers: + * code 200 : User (successful operation) + * code 400 : (Invalid username supplied) + * code 404 : (User not found) + * + * @param username The name that needs to be fetched. Use user1 for testing. + */ + def getUserByName(username: String): Request[Either[ResponseException[String], User]] = + basicRequest + .method(Method.GET, uri"$baseUrl/user/${username}") + .contentType("application/json") + .response(asJson[User]) + + /** + * + * + * Expected answers: + * code 200 : String (successful operation) + * Headers : + * Set-Cookie - Cookie authentication key for use with the `api_key` apiKey authentication. + * X-Rate-Limit - calls per hour allowed by the user + * X-Expires-After - date in UTC when token expires + * code 400 : (Invalid username/password supplied) + * + * @param username The user name for login + * @param password The password for login in clear text + */ + def loginUser(username: String, password: String): Request[Either[ResponseException[String], String]] = + basicRequest + .method(Method.GET, uri"$baseUrl/user/login?username=${ username }&password=${ password }") + .contentType("application/json") + .response(asJson[String]) + + /** + * + * + * Expected answers: + * code 0 : (successful operation) + * + * Available security schemes: + * api_key (apiKey) + */ + def logoutUser(apiKeyHeader: String)(): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.GET, uri"$baseUrl/user/logout") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + + /** + * This can only be done by the logged in user. + * + * Expected answers: + * code 400 : (Invalid user supplied) + * code 404 : (User not found) + * + * Available security schemes: + * api_key (apiKey) + * + * @param username name that need to be deleted + * @param user Updated user object + */ + def updateUser(apiKeyHeader: String)(username: String, user: User): Request[Either[ResponseException[String], Unit]] = + basicRequest + .method(Method.PUT, uri"$baseUrl/user/${username}") + .contentType("application/json") + .header("api_key", apiKeyHeader) + .body(asJson(user)) + .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) + +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala new file mode 100644 index 000000000000..137dbc248fad --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala @@ -0,0 +1,21 @@ +package org.openapitools.client.core + +import java.net.{ URI, URISyntaxException } + +trait AdditionalTypeSerializers { + import io.circe._ + + implicit final lazy val URIDecoder: Decoder[URI] = Decoder.decodeString.emap(string => + try Right(new URI(string)) + catch { + case _: URISyntaxException => + Left("String could not be parsed as a URI reference, it violates RFC 2396.") + case _: NullPointerException => + Left("String is null.") + } + ) + + implicit final lazy val URIEncoder: Encoder[URI] = new Encoder[URI] { + final def apply(a: URI): Json = Json.fromString(a.toString) + } +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/DateSerializers.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/DateSerializers.scala new file mode 100644 index 000000000000..1bed2914651f --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/DateSerializers.scala @@ -0,0 +1,13 @@ +package org.openapitools.client.core + +import java.time.{LocalDate, OffsetDateTime} +import java.time.format.DateTimeFormatter + +trait DateSerializers { + import io.circe.{Decoder, Encoder} + 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) +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala new file mode 100644 index 000000000000..dd7cd56e15e0 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/core/JsonSupport.scala @@ -0,0 +1,21 @@ +/** + * 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.openapitools.client.model._ +import io.circe.{Decoder, Encoder} +import io.circe.generic.AutoDerivation +import sttp.client4.circe.SttpCirceApi + +object JsonSupport extends SttpCirceApi with AutoDerivation with DateSerializers with AdditionalTypeSerializers { + // Enum encoders/decoders are defined in their respective companion objects +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala new file mode 100644 index 000000000000..b1b1c90495a6 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala @@ -0,0 +1,31 @@ +/** + * 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.model + + + /** + * An uploaded response + * Describes the result of uploading an image resource + */ +case class ApiResponse( + code: Option[Int] = None, + `type`: Option[String] = None, + message: Option[String] = None +) +object ApiResponse { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[ApiResponse] = deriveEncoder + implicit val decoder: Decoder[ApiResponse] = deriveDecoder +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Category.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Category.scala new file mode 100644 index 000000000000..def67cc526f2 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Category.scala @@ -0,0 +1,30 @@ +/** + * 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.model + + + /** + * Pet category + * A category for a pet + */ +case class Category( + id: Option[Long] = None, + name: Option[String] = None +) +object Category { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[Category] = deriveEncoder + implicit val decoder: Decoder[Category] = deriveDecoder +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Order.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Order.scala new file mode 100644 index 000000000000..6cb20a7a07cd --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Order.scala @@ -0,0 +1,60 @@ +/** + * 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.model + +import java.time.OffsetDateTime + + /** + * Pet Order + * An order for a pets from the pet store + */ +case class Order( + id: Option[Long] = None, + petId: Option[Long] = None, + quantity: Option[Int] = None, + shipDate: Option[OffsetDateTime] = None, + /* Order Status */ + status: Option[OrderEnums.Status] = None, + complete: Option[Boolean] = None +) +object Order { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[Order] = deriveEncoder + implicit val decoder: Decoder[Order] = deriveDecoder +} +object OrderEnums { + + sealed trait Status + object Status { + case object Placed extends Status + case object Approved extends Status + case object Delivered extends Status + + import io.circe.{Encoder, Decoder} + + implicit val encoder: Encoder[Status] = Encoder.encodeString.contramap[Status] { + case Placed => "placed" + case Approved => "approved" + case Delivered => "delivered" + } + + implicit val decoder: Decoder[Status] = Decoder.decodeString.emap { + case "placed" => Right(Placed) + case "approved" => Right(Approved) + case "delivered" => Right(Delivered) + case other => Left(s"Invalid Status: $other") + } + } +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Pet.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Pet.scala new file mode 100644 index 000000000000..88f0ce279b25 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Pet.scala @@ -0,0 +1,59 @@ +/** + * 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.model + + + /** + * a Pet + * A pet for sale in the pet store + */ +case class Pet( + id: Option[Long] = None, + category: Option[Category] = None, + name: String, + photoUrls: Seq[String], + tags: Option[Seq[Tag]] = None, + /* pet status in the store */ + status: Option[PetEnums.Status] = None +) +object Pet { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[Pet] = deriveEncoder + implicit val decoder: Decoder[Pet] = deriveDecoder +} +object PetEnums { + + sealed trait Status + object Status { + case object Available extends Status + case object Pending extends Status + case object Sold extends Status + + import io.circe.{Encoder, Decoder} + + implicit val encoder: Encoder[Status] = Encoder.encodeString.contramap[Status] { + case Available => "available" + case Pending => "pending" + case Sold => "sold" + } + + implicit val decoder: Decoder[Status] = Decoder.decodeString.emap { + case "available" => Right(Available) + case "pending" => Right(Pending) + case "sold" => Right(Sold) + case other => Left(s"Invalid Status: $other") + } + } +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Tag.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Tag.scala new file mode 100644 index 000000000000..4672a3316f36 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/Tag.scala @@ -0,0 +1,30 @@ +/** + * 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.model + + + /** + * Pet Tag + * A tag for a pet + */ +case class Tag( + id: Option[Long] = None, + name: Option[String] = None +) +object Tag { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[Tag] = deriveEncoder + implicit val decoder: Decoder[Tag] = deriveDecoder +} diff --git a/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/User.scala b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/User.scala new file mode 100644 index 000000000000..f488d72e97d1 --- /dev/null +++ b/samples/client/petstore/scala-sttp4-circe/src/main/scala/org/openapitools/client/model/User.scala @@ -0,0 +1,37 @@ +/** + * 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.model + + + /** + * a User + * A User who is purchasing from the pet store + */ +case class User( + id: Option[Long] = None, + username: Option[String] = None, + firstName: Option[String] = None, + lastName: Option[String] = None, + email: Option[String] = None, + password: Option[String] = None, + phone: Option[String] = None, + /* User Status */ + userStatus: Option[Int] = None +) +object User { + import io.circe._ + import io.circe.syntax._ + import io.circe.generic.semiauto._ + + implicit val encoder: Encoder[User] = deriveEncoder + implicit val decoder: Decoder[User] = deriveDecoder +} diff --git a/samples/client/petstore/scala-sttp4/build.sbt b/samples/client/petstore/scala-sttp4/build.sbt index 9ad901170074..a002d012cd30 100644 --- a/samples/client/petstore/scala-sttp4/build.sbt +++ b/samples/client/petstore/scala-sttp4/build.sbt @@ -6,8 +6,8 @@ scalaVersion := "2.13.16" crossScalaVersions := Seq(scalaVersion.value, "2.12.20") libraryDependencies ++= Seq( - "com.softwaremill.sttp.client4" %% "core" % "4.0.0-M1", - "com.softwaremill.sttp.client4" %% "json4s" % "4.0.0-M1", + "com.softwaremill.sttp.client4" %% "core" % "4.0.15", + "com.softwaremill.sttp.client4" %% "json4s" % "4.0.15", "org.json4s" %% "json4s-jackson" % "4.0.6" ) diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/PetApi.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/PetApi.scala index 89bc472e65f6..3744397ca2d1 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/PetApi.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/PetApi.scala @@ -33,11 +33,11 @@ class PetApi(baseUrl: String) { * * @param pet Pet object that needs to be added to the store */ - def addPet(pet: Pet): Request[Either[ResponseException[String, Exception], Pet]] = + def addPet(pet: Pet): Request[Either[ResponseException[String], Pet]] = basicRequest .method(Method.POST, uri"$baseUrl/pet") .contentType("application/json") - .body(pet) + .body(asJson(pet)) .response(asJson[Pet]) /** @@ -49,11 +49,11 @@ class PetApi(baseUrl: String) { * @param petId Pet id to delete * @param apiKey */ - def deletePet(petId: Long, apiKey: Option[String] = None): Request[Either[ResponseException[String, Exception], Unit]] = + def deletePet(petId: Long, apiKey: Option[String] = None): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.DELETE, uri"$baseUrl/pet/${petId}") .contentType("application/json") - .header("api_key", apiKey.toString) + .header("api_key", apiKey) .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) /** @@ -65,7 +65,7 @@ class PetApi(baseUrl: String) { * * @param status Status values that need to be considered for filter */ - def findPetsByStatus(status: Seq[String]): Request[Either[ResponseException[String, Exception], Seq[Pet]]] = + def findPetsByStatus(status: Seq[String] = Seq.empty): Request[Either[ResponseException[String], Seq[Pet]]] = basicRequest .method(Method.GET, uri"$baseUrl/pet/findByStatus?status=${ status }") .contentType("application/json") @@ -80,7 +80,7 @@ class PetApi(baseUrl: String) { * * @param tags Tags to filter by */ - def findPetsByTags(tags: Seq[String]): Request[Either[ResponseException[String, Exception], Seq[Pet]]] = + def findPetsByTags(tags: Seq[String] = Seq.empty): Request[Either[ResponseException[String], Seq[Pet]]] = basicRequest .method(Method.GET, uri"$baseUrl/pet/findByTags?tags=${ tags }") .contentType("application/json") @@ -99,7 +99,7 @@ class PetApi(baseUrl: String) { * * @param petId ID of pet to return */ - def getPetById(apiKeyHeader: String)(petId: Long): Request[Either[ResponseException[String, Exception], Pet]] = + def getPetById(apiKeyHeader: String)(petId: Long): Request[Either[ResponseException[String], Pet]] = basicRequest .method(Method.GET, uri"$baseUrl/pet/${petId}") .contentType("application/json") @@ -117,11 +117,11 @@ class PetApi(baseUrl: String) { * * @param pet Pet object that needs to be added to the store */ - def updatePet(pet: Pet): Request[Either[ResponseException[String, Exception], Pet]] = + def updatePet(pet: Pet): Request[Either[ResponseException[String], Pet]] = basicRequest .method(Method.PUT, uri"$baseUrl/pet") .contentType("application/json") - .body(pet) + .body(asJson(pet)) .response(asJson[Pet]) /** @@ -134,7 +134,7 @@ class PetApi(baseUrl: String) { * @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): Request[Either[ResponseException[String, Exception], Unit]] = + def updatePetWithForm(petId: Long, name: Option[String] = None, status: Option[String] = None): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.POST, uri"$baseUrl/pet/${petId}") .contentType("application/x-www-form-urlencoded") @@ -154,7 +154,7 @@ class PetApi(baseUrl: String) { * @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): Request[Either[ResponseException[String, Exception], ApiResponse]] = + def uploadFile(petId: Long, additionalMetadata: Option[String] = None, file: Option[File] = None): Request[Either[ResponseException[String], ApiResponse]] = basicRequest .method(Method.POST, uri"$baseUrl/pet/${petId}/uploadImage") .contentType("multipart/form-data") diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/StoreApi.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/StoreApi.scala index 507611de4d13..27efb05281b1 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/StoreApi.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/StoreApi.scala @@ -31,7 +31,7 @@ class StoreApi(baseUrl: String) { * * @param orderId ID of the order that needs to be deleted */ - def deleteOrder(orderId: String): Request[Either[ResponseException[String, Exception], Unit]] = + def deleteOrder(orderId: String): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.DELETE, uri"$baseUrl/store/order/${orderId}") .contentType("application/json") @@ -46,7 +46,7 @@ class StoreApi(baseUrl: String) { * Available security schemes: * api_key (apiKey) */ - def getInventory(apiKeyHeader: String)(): Request[Either[ResponseException[String, Exception], Map[String, Int]]] = + def getInventory(apiKeyHeader: String)(): Request[Either[ResponseException[String], Map[String, Int]]] = basicRequest .method(Method.GET, uri"$baseUrl/store/inventory") .contentType("application/json") @@ -63,7 +63,7 @@ class StoreApi(baseUrl: String) { * * @param orderId ID of pet that needs to be fetched */ - def getOrderById(orderId: Long): Request[Either[ResponseException[String, Exception], Order]] = + def getOrderById(orderId: Long): Request[Either[ResponseException[String], Order]] = basicRequest .method(Method.GET, uri"$baseUrl/store/order/${orderId}") .contentType("application/json") @@ -78,11 +78,11 @@ class StoreApi(baseUrl: String) { * * @param order order placed for purchasing the pet */ - def placeOrder(order: Order): Request[Either[ResponseException[String, Exception], Order]] = + def placeOrder(order: Order): Request[Either[ResponseException[String], Order]] = basicRequest .method(Method.POST, uri"$baseUrl/store/order") .contentType("application/json") - .body(order) + .body(asJson(order)) .response(asJson[Order]) } diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/UserApi.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/UserApi.scala index 67a17958468e..176c21e2b316 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/UserApi.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/api/UserApi.scala @@ -34,12 +34,12 @@ class UserApi(baseUrl: String) { * * @param user Created user object */ - def createUser(apiKeyHeader: String)(user: User): Request[Either[ResponseException[String, Exception], Unit]] = + def createUser(apiKeyHeader: String)(user: User): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.POST, uri"$baseUrl/user") .contentType("application/json") .header("api_key", apiKeyHeader) - .body(user) + .body(asJson(user)) .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) /** @@ -53,12 +53,12 @@ class UserApi(baseUrl: String) { * * @param user List of user object */ - def createUsersWithArrayInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String, Exception], Unit]] = + def createUsersWithArrayInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.POST, uri"$baseUrl/user/createWithArray") .contentType("application/json") .header("api_key", apiKeyHeader) - .body(user) + .body(asJson(user)) .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) /** @@ -72,12 +72,12 @@ class UserApi(baseUrl: String) { * * @param user List of user object */ - def createUsersWithListInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String, Exception], Unit]] = + def createUsersWithListInput(apiKeyHeader: String)(user: Seq[User]): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.POST, uri"$baseUrl/user/createWithList") .contentType("application/json") .header("api_key", apiKeyHeader) - .body(user) + .body(asJson(user)) .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) /** @@ -92,7 +92,7 @@ class UserApi(baseUrl: String) { * * @param username The name that needs to be deleted */ - def deleteUser(apiKeyHeader: String)(username: String): Request[Either[ResponseException[String, Exception], Unit]] = + def deleteUser(apiKeyHeader: String)(username: String): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.DELETE, uri"$baseUrl/user/${username}") .contentType("application/json") @@ -109,7 +109,7 @@ class UserApi(baseUrl: String) { * * @param username The name that needs to be fetched. Use user1 for testing. */ - def getUserByName(username: String): Request[Either[ResponseException[String, Exception], User]] = + def getUserByName(username: String): Request[Either[ResponseException[String], User]] = basicRequest .method(Method.GET, uri"$baseUrl/user/${username}") .contentType("application/json") @@ -129,7 +129,7 @@ class UserApi(baseUrl: String) { * @param username The user name for login * @param password The password for login in clear text */ - def loginUser(username: String, password: String): Request[Either[ResponseException[String, Exception], String]] = + def loginUser(username: String, password: String): Request[Either[ResponseException[String], String]] = basicRequest .method(Method.GET, uri"$baseUrl/user/login?username=${ username }&password=${ password }") .contentType("application/json") @@ -144,7 +144,7 @@ class UserApi(baseUrl: String) { * Available security schemes: * api_key (apiKey) */ - def logoutUser(apiKeyHeader: String)(): Request[Either[ResponseException[String, Exception], Unit]] = + def logoutUser(apiKeyHeader: String)(): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.GET, uri"$baseUrl/user/logout") .contentType("application/json") @@ -164,12 +164,12 @@ class UserApi(baseUrl: String) { * @param username name that need to be deleted * @param user Updated user object */ - def updateUser(apiKeyHeader: String)(username: String, user: User): Request[Either[ResponseException[String, Exception], Unit]] = + def updateUser(apiKeyHeader: String)(username: String, user: User): Request[Either[ResponseException[String], Unit]] = basicRequest .method(Method.PUT, uri"$baseUrl/user/${username}") .contentType("application/json") .header("api_key", apiKeyHeader) - .body(user) + .body(asJson(user)) .response(asString.mapWithMetadata(ResponseAs.deserializeRightWithError(_ => Right(())))) } diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/ApiResponse.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/ApiResponse.scala index b0abb512265e..9d09300eae38 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/ApiResponse.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/ApiResponse.scala @@ -21,4 +21,3 @@ case class ApiResponse( `type`: Option[String] = None, message: Option[String] = None ) - diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Category.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Category.scala index 169acc49cefd..c0c51c5e53eb 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Category.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Category.scala @@ -20,4 +20,3 @@ case class Category( id: Option[Long] = None, name: Option[String] = None ) - diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Order.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Order.scala index 9e805e256b21..893cad11d288 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Order.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Order.scala @@ -26,14 +26,32 @@ case class Order( status: Option[OrderEnums.Status] = None, complete: Option[Boolean] = None ) - object OrderEnums { - type Status = Status.Value - object Status extends Enumeration { - val Placed = Value("placed") - val Approved = Value("approved") - val Delivered = Value("delivered") - } + sealed trait Status + object Status { + case object Placed extends Status + case object Approved extends Status + case object Delivered extends Status + + import org.json4s._ + implicit object StatusSerializer extends Serializer[Status] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), Status] = { + case (TypeInfo(clazz, _), json) if classOf[Status].isAssignableFrom(clazz) => + json match { + case JString("placed") => Placed + case JString("approved") => Approved + case JString("delivered") => Delivered + case other => throw new MappingException(s"Invalid Status: $other") + } + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case Placed => JString("placed") + case Approved => JString("approved") + case Delivered => JString("delivered") + } + } + } } diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Pet.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Pet.scala index 3cbab6051284..d5805371fac8 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Pet.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Pet.scala @@ -25,14 +25,32 @@ case class Pet( /* pet status in the store */ status: Option[PetEnums.Status] = None ) - object PetEnums { - type Status = Status.Value - object Status extends Enumeration { - val Available = Value("available") - val Pending = Value("pending") - val Sold = Value("sold") - } + sealed trait Status + object Status { + case object Available extends Status + case object Pending extends Status + case object Sold extends Status + + import org.json4s._ + implicit object StatusSerializer extends Serializer[Status] { + def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), Status] = { + case (TypeInfo(clazz, _), json) if classOf[Status].isAssignableFrom(clazz) => + json match { + case JString("available") => Available + case JString("pending") => Pending + case JString("sold") => Sold + case other => throw new MappingException(s"Invalid Status: $other") + } + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case Available => JString("available") + case Pending => JString("pending") + case Sold => JString("sold") + } + } + } } diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Tag.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Tag.scala index c2020246658a..9af834f41b9e 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Tag.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/Tag.scala @@ -20,4 +20,3 @@ case class Tag( id: Option[Long] = None, name: Option[String] = None ) - diff --git a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/User.scala b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/User.scala index 6977180bccee..c48dd41458b4 100644 --- a/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/User.scala +++ b/samples/client/petstore/scala-sttp4/src/main/scala/org/openapitools/client/model/User.scala @@ -27,4 +27,3 @@ case class User( /* User Status */ userStatus: Option[Int] = None ) -