diff --git a/bin/configs/scala-sttp-circe.yaml b/bin/configs/scala-sttp-circe.yaml index 37771342b0fd..f48d2e47d295 100644 --- a/bin/configs/scala-sttp-circe.yaml +++ b/bin/configs/scala-sttp-circe.yaml @@ -1,6 +1,6 @@ generatorName: scala-sttp outputDir: samples/client/petstore/scala-sttp-circe -inputSpec: modules/openapi-generator/src/test/resources/3_0/scala/petstore.yaml +inputSpec: modules/openapi-generator/src/test/resources/3_0/scala-sttp-circe/petstore.yaml templateDir: modules/openapi-generator/src/main/resources/scala-sttp nameMappings: _type: "`underscoreType`" diff --git a/docs/generators/scala-sttp.md b/docs/generators/scala-sttp.md index 798740d25668..0160442c0abb 100644 --- a/docs/generators/scala-sttp.md +++ b/docs/generators/scala-sttp.md @@ -226,9 +226,9 @@ These options may be applied as additional-properties (cli) or configOptions (pl |Composite|✓|OAS2,OAS3 |Polymorphism|✗|OAS2,OAS3 |Union|✗|OAS3 -|allOf|✗|OAS2,OAS3 +|allOf|✓|OAS2,OAS3 |anyOf|✗|OAS3 -|oneOf|✗|OAS3 +|oneOf|✓|OAS3 |not|✗|OAS3 ### Security Feature 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 c3266f72bb86..fb25eda08f71 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 @@ -94,6 +94,10 @@ public ScalaSttpClientCodegen() { .excludeSchemaSupportFeatures( SchemaSupportFeature.Polymorphism ) + .includeSchemaSupportFeatures( + SchemaSupportFeature.oneOf, + SchemaSupportFeature.allOf + ) .excludeParameterFeatures( ParameterFeature.Cookie ) @@ -240,9 +244,207 @@ public ModelsMap postProcessModels(ModelsMap objs) { */ @Override public Map postProcessAllModels(Map objs) { - final Map processed = super.postProcessAllModels(objs); - postProcessUpdateImports(processed); - return processed; + Map modelsMap = super.postProcessAllModels(objs); + + Map allModels = collectAllModels(modelsMap); + synthesizeOneOfFromDiscriminator(allModels); + Map refCounts = countModelReferences(allModels); + markOneOfTraits(modelsMap, allModels, refCounts); + removeInlinedModels(modelsMap); + + postProcessUpdateImports(modelsMap); + return modelsMap; + } + + /** + * Collect all CodegenModels by classname for lookup. + */ + private Map collectAllModels(Map modelsMap) { + return modelsMap.values().stream() + .flatMap(mm -> mm.getModels().stream()) + .map(ModelMap::getModel) + .collect(java.util.stream.Collectors.toMap(m -> m.classname, m -> m, (a, b) -> a)); + } + + /** + * For specs that use allOf+discriminator (children reference parent via allOf, parent has + * discriminator.mapping but no oneOf), synthesize the oneOf set from the discriminator mapping. + * This allows the standard oneOf processing logic to handle both patterns uniformly. + */ + private void synthesizeOneOfFromDiscriminator(Map allModels) { + for (CodegenModel model : allModels.values()) { + if (!model.oneOf.isEmpty() || model.discriminator == null) { + continue; + } + + if (model.discriminator.getMappedModels() != null + && !model.discriminator.getMappedModels().isEmpty()) { + for (CodegenDiscriminator.MappedModel mapped : model.discriminator.getMappedModels()) { + model.oneOf.add(mapped.getModelName()); + } + } else if (model.discriminator.getMapping() != null) { + for (String ref : model.discriminator.getMapping().values()) { + String modelName = ref.contains("/") ? ref.substring(ref.lastIndexOf('/') + 1) : ref; + if (allModels.containsKey(modelName)) { + model.oneOf.add(modelName); + } + } + } + + if (!model.oneOf.isEmpty()) { + model.getVendorExtensions().put("x-synthesized-oneOf", true); + } + } + } + + /** + * Count how many times each model is referenced - both as a oneOf member and as a + * property type. A child can only be inlined if it's referenced exactly once (by its + * oneOf parent) and not used as a property type elsewhere. + */ + private Map countModelReferences(Map allModels) { + Map counts = new HashMap<>(); + + // Count oneOf parent references + allModels.values().stream() + .flatMap(m -> m.oneOf.stream()) + .forEach(name -> counts.merge(name, 1, Integer::sum)); + + // Count property-type references (prevents inlining models used as field types). + // Check both dataType and complexType + allModels.values().stream() + .flatMap(m -> m.vars.stream()) + .forEach(prop -> { + if (prop.dataType != null && allModels.containsKey(prop.dataType)) { + counts.merge(prop.dataType, 1, Integer::sum); + } + if (prop.complexType != null && allModels.containsKey(prop.complexType)) { + counts.merge(prop.complexType, 1, Integer::sum); + } + }); + + return counts; + } + + /** + * Mark oneOf parents as sealed/regular traits with discriminator vendor extensions, + * and configure child models for inlining. + */ + private void markOneOfTraits( + Map modelsMap, + Map allModels, + Map refCounts) { + for (ModelsMap mm : modelsMap.values()) { + for (ModelMap modelMap : mm.getModels()) { + CodegenModel model = modelMap.getModel(); + + if (!model.oneOf.isEmpty()) { + configureOneOfModel(model, allModels, refCounts); + } + + if (model.discriminator != null) { + model.getVendorExtensions().put("x-use-discr", true); + if (model.discriminator.getMapping() != null) { + model.getVendorExtensions().put("x-use-discr-mapping", true); + } + } + } + } + } + + private void configureOneOfModel( + CodegenModel parent, + Map allModels, + Map refCounts) { + List inlineableMembers = new ArrayList<>(); + Set childImports = new HashSet<>(); + + for (String childName : parent.oneOf) { + CodegenModel child = allModels.get(childName); + if (child == null) continue; + + // All children extend the parent trait + child.getVendorExtensions().put("x-oneOfParent", parent.classname); + if (parent.discriminator != null) { + child.getVendorExtensions().put("x-parentDiscriminatorName", + parent.discriminator.getPropertyName()); + } + + if (isInlineable(child, refCounts)) { + child.getVendorExtensions().put("x-isOneOfMember", true); + inlineableMembers.add(child); + if (child.imports != null) { + childImports.addAll(child.imports); + } + } + } + + buildDiscriminatorEntries(parent, allModels); + + if (!inlineableMembers.isEmpty() && inlineableMembers.size() == parent.oneOf.size()) { + markAsSealedTrait(parent, inlineableMembers, childImports); + } else { + markAsRegularTrait(parent, inlineableMembers); + } + } + + private boolean isInlineable(CodegenModel child, Map refCounts) { + return (child.oneOf == null || child.oneOf.isEmpty()) + && refCounts.getOrDefault(child.classname, 0) == 1; + } + + private void buildDiscriminatorEntries(CodegenModel parent, Map allModels) { + List> entries = parent.oneOf.stream() + .map(allModels::get) + .filter(Objects::nonNull) + .map(child -> Map.of("classname", child.classname, "schemaName", child.name)) + .collect(java.util.stream.Collectors.toList()); + parent.getVendorExtensions().put("x-discriminator-entries", entries); + } + + private void markAsSealedTrait( + CodegenModel parent, + List members, + Set childImports) { + parent.getVendorExtensions().put("x-isSealedTrait", true); + parent.getVendorExtensions().put("x-oneOfMembers", members); + + if (parent.getVendorExtensions().containsKey("x-synthesized-oneOf") + && parent.vars != null && !parent.vars.isEmpty()) { + parent.getVendorExtensions().put("x-hasOwnVars", true); + } + + mergeChildImports(parent, childImports); + } + + private void markAsRegularTrait(CodegenModel parent, List partialMembers) { + parent.getVendorExtensions().put("x-isRegularTrait", true); + for (CodegenModel member : partialMembers) { + member.getVendorExtensions().remove("x-isOneOfMember"); + } + } + + private void mergeChildImports(CodegenModel parent, Set childImports) { + if (childImports.isEmpty()) return; + Set existing = parent.imports != null ? new HashSet<>(parent.imports) : new HashSet<>(); + childImports.removeAll(existing); + if (!childImports.isEmpty()) { + if (parent.imports == null) { + parent.imports = new HashSet<>(); + } + parent.imports.addAll(childImports); + } + } + + /** + * Remove models that were inlined into their parent sealed trait - + * they don't need separate files. + */ + private void removeInlinedModels(Map modelsMap) { + modelsMap.entrySet().removeIf(entry -> + entry.getValue().getModels().stream() + .anyMatch(m -> m.getModel().getVendorExtensions().containsKey("x-isOneOfMember")) + ); } /** 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 e584c94da3b1..3168fe30de98 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp/model.mustache @@ -5,7 +5,7 @@ package {{package}} import {{import}} {{/imports}} {{#circe}} -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import {{invokerPackage}}.JsonSupport._ {{/circe}} @@ -20,6 +20,148 @@ import {{invokerPackage}}.JsonSupport._ {{{description}}} {{/javadocRenderer}} {{/description}} +{{#vendorExtensions.x-isSealedTrait}} +sealed trait {{classname}}{{#vendorExtensions.x-hasOwnVars}} { +{{#vars}} + def {{{name}}}: {{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}} +{{/vars}} +}{{/vendorExtensions.x-hasOwnVars}} +object {{classname}} { +{{#circe}} +{{#vendorExtensions.x-use-discr-mapping}} +{{#discriminator}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#mappedModels}} + case obj: {{model.classname}} => obj.asJson.mapObject(("{{propertyName}}" -> "{{mappingName}}".asJson) +: _) +{{/mappedModels}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + c.downField("{{propertyName}}").as[String].flatMap { +{{#mappedModels}} + case "{{mappingName}}" => c.as[{{model.classname}}] +{{/mappedModels}} + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +{{/discriminator}} +{{/vendorExtensions.x-use-discr-mapping}} +{{^vendorExtensions.x-use-discr-mapping}} +{{#vendorExtensions.x-use-discr}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#vendorExtensions.x-discriminator-entries}} + case obj: {{classname}} => obj.asJson.mapObject(("{{discriminator.propertyName}}" -> "{{schemaName}}".asJson) +: _) +{{/vendorExtensions.x-discriminator-entries}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + c.downField("{{discriminator.propertyName}}").as[String].flatMap { +{{#vendorExtensions.x-discriminator-entries}} + case "{{schemaName}}" => c.as[{{classname}}] +{{/vendorExtensions.x-discriminator-entries}} + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +{{/vendorExtensions.x-use-discr}} +{{^vendorExtensions.x-use-discr}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#oneOf}} + case obj: {{.}} => obj.asJson +{{/oneOf}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = + List[Decoder[{{classname}}]]({{#oneOf}}Decoder[{{.}}].map(x => x: {{classname}}){{^-last}}, {{/-last}}{{/oneOf}}).reduceLeft(_ or _) +{{/vendorExtensions.x-use-discr}} +{{/vendorExtensions.x-use-discr-mapping}} +{{/circe}} +} + +{{#vendorExtensions.x-oneOfMembers}} +case class {{classname}}( + {{#vars}} + {{#description}} + /* {{{.}}} */ + {{/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}} +) extends {{vendorExtensions.x-oneOfParent}} +{{#circe}} +object {{classname}} { + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { t => + Json.fromFields{ + Seq( + {{#vars}} + {{#required}}Some("{{baseName}}" -> t.{{{name}}}.asJson){{/required}}{{^required}}t.{{{name}}}.map(v => "{{baseName}}" -> v.asJson){{/required}}{{^-last}},{{/-last}} + {{/vars}} + ).flatten + } + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + for { + {{#vars}} + {{{name}}} <- c.downField("{{baseName}}").as[{{^required}}Option[{{/required}}{{^isEnum}}{{dataType}}{{/isEnum}}{{#isEnum}}{{^isArray}}{{classname}}Enums.{{datatypeWithEnum}}{{/isArray}}{{#isArray}}Seq[{{classname}}Enums.{{datatypeWithEnum}}]{{/isArray}}{{/isEnum}}{{^required}}]{{/required}}] + {{/vars}} + } yield {{classname}}( + {{#vars}} + {{{name}}} = {{{name}}}{{^-last}},{{/-last}} + {{/vars}} + ) + } +} +{{/circe}} + +{{/vendorExtensions.x-oneOfMembers}} +{{/vendorExtensions.x-isSealedTrait}} +{{#vendorExtensions.x-isRegularTrait}} +trait {{classname}} +object {{classname}} { +{{#circe}} +{{#vendorExtensions.x-use-discr-mapping}} +{{#discriminator}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#mappedModels}} + case obj: {{model.classname}} => obj.asJson.mapObject(("{{propertyName}}" -> "{{mappingName}}".asJson) +: _) +{{/mappedModels}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + c.downField("{{propertyName}}").as[String].flatMap { +{{#mappedModels}} + case "{{mappingName}}" => c.as[{{model.classname}}] +{{/mappedModels}} + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +{{/discriminator}} +{{/vendorExtensions.x-use-discr-mapping}} +{{^vendorExtensions.x-use-discr-mapping}} +{{#vendorExtensions.x-use-discr}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#vendorExtensions.x-discriminator-entries}} + case obj: {{classname}} => obj.asJson.mapObject(("{{discriminator.propertyName}}" -> "{{schemaName}}".asJson) +: _) +{{/vendorExtensions.x-discriminator-entries}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = Decoder.instance { c => + c.downField("{{discriminator.propertyName}}").as[String].flatMap { +{{#vendorExtensions.x-discriminator-entries}} + case "{{schemaName}}" => c.as[{{classname}}] +{{/vendorExtensions.x-discriminator-entries}} + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +{{/vendorExtensions.x-use-discr}} +{{^vendorExtensions.x-use-discr}} + implicit val encoder{{classname}}: Encoder[{{classname}}] = Encoder.instance { +{{#oneOf}} + case obj: {{.}} => obj.asJson +{{/oneOf}} + } + implicit val decoder{{classname}}: Decoder[{{classname}}] = + List[Decoder[{{classname}}]]({{#oneOf}}Decoder[{{.}}].map(x => x: {{classname}}){{^-last}}, {{/-last}}{{/oneOf}}).reduceLeft(_ or _) +{{/vendorExtensions.x-use-discr}} +{{/vendorExtensions.x-use-discr-mapping}} +{{/circe}} +} +{{/vendorExtensions.x-isRegularTrait}} +{{^vendorExtensions.x-isSealedTrait}} +{{^vendorExtensions.x-isRegularTrait}} {{^isEnum}} case class {{classname}}( {{#vars}} @@ -28,7 +170,7 @@ 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}} -) +){{#vendorExtensions.x-oneOfParent}} extends {{vendorExtensions.x-oneOfParent}}{{/vendorExtensions.x-oneOfParent}} {{#circe}} object {{classname}} { {{#hasVars}} @@ -64,6 +206,8 @@ object {{classname}} { } {{/circe}} {{/isEnum}} +{{/vendorExtensions.x-isRegularTrait}} +{{/vendorExtensions.x-isSealedTrait}} {{#isEnum}} object {{classname}} extends Enumeration { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaSttpCirceCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaSttpCirceCodegenTest.java new file mode 100644 index 000000000000..950987bfe911 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/ScalaSttpCirceCodegenTest.java @@ -0,0 +1,195 @@ +package org.openapitools.codegen.scala; + +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.openapitools.codegen.ClientOptInput; +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.languages.ScalaSttpClientCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.openapitools.codegen.TestUtils.assertFileContains; +import static org.openapitools.codegen.TestUtils.assertFileNotContains; + +/** + * Tests for scala-sttp generator with circe JSON library. + * Covers baseName field mapping, discriminator/polymorphism, and special type handling. + */ +public class ScalaSttpCirceCodegenTest { + + private DefaultGenerator generateFromSpec(String specPath, File output) { + OpenAPI openAPI = new OpenAPIParser() + .readLocation(specPath, null, new ParseOptions()).getOpenAPI(); + + ScalaSttpClientCodegen codegen = new ScalaSttpClientCodegen(); + 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, "true"); + generator.opts(input).generate(); + return generator; + } + + @Test(description = "circe encoder/decoder uses baseName for JSON field names") + public void verifyBaseNameFieldMapping() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + generateFromSpec("src/test/resources/3_0/scala/mixed-case-fields.yaml", output); + + // MixedCaseModel: verify baseName is used in encoder/decoder + Path modelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala"); + assertFileContains(modelPath, "\"first-name\""); + assertFileContains(modelPath, "\"phone_number\""); + assertFileContains(modelPath, "\"lastName\""); + assertFileContains(modelPath, "\"ZipCode\""); + assertFileContains(modelPath, "c.downField(\"first-name\")"); + assertFileContains(modelPath, "c.downField(\"phone_number\")"); + assertFileContains(modelPath, "c.downField(\"ZipCode\")"); + assertFileContains(modelPath, "implicit val encoderMixedCaseModel"); + assertFileContains(modelPath, "implicit val decoderMixedCaseModel"); + + // BinaryPayload: File and untyped object fields + Path binaryPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/BinaryPayload.scala"); + assertFileContains(binaryPath, "data: Option[File]"); + assertFileContains(binaryPath, "implicit val encoderBinaryPayload"); + assertFileContains(binaryPath, "implicit val decoderBinaryPayload"); + + // AdditionalTypeSerializers: File and Any codecs + Path serializersPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala"); + assertFileContains(serializersPath, "FileDecoder"); + assertFileContains(serializersPath, "FileEncoder"); + assertFileContains(serializersPath, "AnyDecoder"); + assertFileContains(serializersPath, "AnyEncoder"); + + // JsonSupport should NOT use AutoDerivation + Path jsonSupportPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/JsonSupport.scala"); + assertFileNotContains(jsonSupportPath, "AutoDerivation"); + } + + @Test(description = "allOf + discriminator generates sealed trait with discriminator-based circe codecs") + public void verifyAllOfDiscriminator() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + generateFromSpec("src/test/resources/3_0/scala/mixed-case-fields.yaml", output); + + // Animal should be a sealed trait with base fields as abstract defs + Path animalPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Animal.scala"); + assertFileContains(animalPath, "sealed trait Animal"); + assertFileContains(animalPath, "def className: String"); + assertFileContains(animalPath, "def color: Option[String]"); + + // Discriminator-based encoder/decoder + assertFileContains(animalPath, "implicit val encoderAnimal"); + assertFileContains(animalPath, "implicit val decoderAnimal"); + assertFileContains(animalPath, "\"DOG\""); + assertFileContains(animalPath, "\"CAT\""); + assertFileContains(animalPath, "c.downField(\"className\")"); + assertFileContains(animalPath, "DecodingFailure"); + + // Cat and Dog inlined in Animal.scala, extending Animal + assertFileContains(animalPath, "case class Cat"); + assertFileContains(animalPath, "case class Dog"); + assertFileContains(animalPath, "extends Animal"); + assertFileContains(animalPath, "declawed"); + assertFileContains(animalPath, "breed"); + + // Cat/Dog should have their own encoder/decoder (for the discriminator to delegate to) + assertFileContains(animalPath, "implicit val encoderCat"); + assertFileContains(animalPath, "implicit val decoderCat"); + assertFileContains(animalPath, "implicit val encoderDog"); + assertFileContains(animalPath, "implicit val decoderDog"); + + // Cat and Dog should NOT have separate files + Assert.assertFalse( + Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Cat.scala").toFile().exists(), + "Cat.scala should not exist (inlined in Animal.scala)"); + Assert.assertFalse( + Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Dog.scala").toFile().exists(), + "Dog.scala should not exist (inlined in Animal.scala)"); + } + + @Test(description = "container-wrapped model ref (Seq[Dog]) prevents inlining of oneOf child") + public void verifyContainerWrappedRefPreventsInlining() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + generateFromSpec("src/test/resources/3_0/scala-sttp-circe/petstore.yaml", output); + + // Dog is referenced both as a oneOf child of Animal AND as Seq[Dog] in Kennel.dogs. + // Since not all children can be inlined, Animal becomes a regular trait (not sealed). + // Both Dog and Cat get their own files. + Path dogPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Dog.scala"); + Assert.assertTrue(dogPath.toFile().exists(), + "Dog.scala must exist as a separate file (used as array element in Kennel)"); + assertFileContains(dogPath, "case class Dog"); + assertFileContains(dogPath, "extends Animal"); + + // Cat also gets its own file (regular trait = no inlining) + Path catPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Cat.scala"); + Assert.assertTrue(catPath.toFile().exists(), + "Cat.scala must exist (Animal is a regular trait, no children inlined)"); + assertFileContains(catPath, "case class Cat"); + assertFileContains(catPath, "extends Animal"); + + // Animal is a regular trait (not sealed) because not all children can be inlined + Path animalPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Animal.scala"); + assertFileContains(animalPath, "trait Animal"); + assertFileNotContains(animalPath, "sealed trait Animal"); + assertFileNotContains(animalPath, "case class Cat"); + assertFileNotContains(animalPath, "case class Dog"); + + // Kennel should reference Dog via Seq + Path kennelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Kennel.scala"); + assertFileContains(kennelPath, "dogs: Option[Seq[Dog]]"); + } + + @Test(description = "oneOf + discriminator generates sealed trait (standard pattern)") + public void verifyOneOfDiscriminator() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + String outputPath = output.getAbsolutePath().replace('\\', '/'); + + generateFromSpec("src/test/resources/3_0/oneOfDiscriminator.yaml", output); + + // FruitReqDisc: sealed trait with inlined members (oneOf + discriminator, no mapping) + Path fruitPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/FruitReqDisc.scala"); + assertFileContains(fruitPath, "sealed trait FruitReqDisc"); + assertFileContains(fruitPath, "case class AppleReqDisc"); + assertFileContains(fruitPath, "case class BananaReqDisc"); + assertFileContains(fruitPath, "extends FruitReqDisc"); + assertFileContains(fruitPath, "\"fruitType\""); + + // FruitOneOfEnumMappingDisc: sealed trait with explicit discriminator mapping + Path fruitMappingPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/FruitOneOfEnumMappingDisc.scala"); + assertFileContains(fruitMappingPath, "sealed trait FruitOneOfEnumMappingDisc"); + assertFileContains(fruitMappingPath, "\"APPLE\""); + assertFileContains(fruitMappingPath, "\"BANANA\""); + + // Inlined members should not have separate files + Assert.assertFalse( + Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/AppleReqDisc.scala").toFile().exists(), + "AppleReqDisc.scala should not exist (inlined)"); + } +} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java index e5f24e8d722f..c6f2150321bd 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/scala/SttpCodegenTest.java @@ -110,69 +110,6 @@ public void verifyApiKeyLocations() throws IOException { assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)"); } - @Test - public void verifyCirceSerdeWithMixedCaseFields() 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/mixed-case-fields.yaml", null, new ParseOptions()).getOpenAPI(); - - ScalaSttpClientCodegen codegen = new ScalaSttpClientCodegen(); - 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, "true"); - generator.opts(input).generate(); - - Path mixedCaseModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/MixedCaseModel.scala"); - - assertFileContains(mixedCaseModelPath, "firstName"); - assertFileContains(mixedCaseModelPath, "phoneNumber"); - assertFileContains(mixedCaseModelPath, "lastName"); - assertFileContains(mixedCaseModelPath, "zipCode"); - assertFileContains(mixedCaseModelPath, "address"); - - assertFileContains(mixedCaseModelPath, "\"first-name\""); - assertFileContains(mixedCaseModelPath, "\"phone_number\""); - assertFileContains(mixedCaseModelPath, "\"lastName\""); - assertFileContains(mixedCaseModelPath, "\"ZipCode\""); - assertFileContains(mixedCaseModelPath, "\"address\""); - - assertFileContains(mixedCaseModelPath, "c.downField(\"first-name\")"); - assertFileContains(mixedCaseModelPath, "c.downField(\"phone_number\")"); - assertFileContains(mixedCaseModelPath, "c.downField(\"ZipCode\")"); - - assertFileContains(mixedCaseModelPath, "object MixedCaseModel"); - assertFileContains(mixedCaseModelPath, "implicit val encoderMixedCaseModel"); - assertFileContains(mixedCaseModelPath, "implicit val decoderMixedCaseModel"); - - Path binaryModelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/BinaryPayload.scala"); - assertFileContains(binaryModelPath, "data: Option[File]"); - assertFileContains(binaryModelPath, "metadata: Option[io.circe.Json]"); - assertFileContains(binaryModelPath, "c.downField(\"data\")"); - assertFileContains(binaryModelPath, "c.downField(\"metadata\")"); - assertFileContains(binaryModelPath, "implicit val encoderBinaryPayload"); - assertFileContains(binaryModelPath, "implicit val decoderBinaryPayload"); - - Path additionalSerializersPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/core/AdditionalTypeSerializers.scala"); - assertFileContains(additionalSerializersPath, "FileDecoder"); - assertFileContains(additionalSerializersPath, "FileEncoder"); - assertFileContains(additionalSerializersPath, "AnyDecoder"); - assertFileContains(additionalSerializersPath, "AnyEncoder"); - } - @Test public void headerSerialization() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/scala-sttp-circe/petstore.yaml b/modules/openapi-generator/src/test/resources/3_0/scala-sttp-circe/petstore.yaml new file mode 100644 index 000000000000..65c92603c2bf --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/scala-sttp-circe/petstore.yaml @@ -0,0 +1,897 @@ +openapi: 3.0.0 +servers: + - url: 'http://petstore.swagger.io/v2' +info: + description: >- + This is a sample server Petstore server. For this sample, you can use the api key + `special-key` to test the authorization filters. + version: 1.0.0 + title: OpenAPI Petstore + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + summary: Add a new pet to the store + description: '' + operationId: addPet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + summary: Update an existing pet + description: '' + operationId: updatePet + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '405': + description: Validation exception + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + deprecated: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - 'read:pets' + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: >- + Multiple tags can be provided with comma separated strings. Use tag1, + tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - 'read:pets' + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + '/pet/{petId}/uploadImage': + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - 'write:pets' + - 'read:pets' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + additionalMetadata: + description: Additional data to pass to server + type: string + file: + description: file to upload + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: '' + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + '/store/order/{orderId}': + get: + tags: + - store + summary: Find purchase order by ID + description: >- + For valid response try integer IDs with value <= 5 or > 10. Other values + will generate exceptions + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of pet that needs to be fetched + required: true + schema: + type: integer + format: int64 + minimum: 1 + maximum: 5 + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: >- + For valid response try integer IDs with value < 1000. Anything above + 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithArray: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithArrayInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: '' + operationId: createUsersWithListInput + responses: + default: + description: successful operation + security: + - api_key: [] + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + Set-Cookie: + description: >- + Cookie authentication key for use with the `api_key` + apiKey authentication. + schema: + type: string + example: AUTH_KEY=abcde12345; Path=/; HttpOnly + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + responses: + default: + description: successful operation + security: + - api_key: [] + '/user/{username}': + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/User' + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Updated user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + security: + - api_key: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found + security: + - api_key: [] + /fake/parameter-name-mapping: + get: + tags: + - fake + summary: parameter name mapping test + operationId: getParameterNameMapping + parameters: + - name: _type + in: header + description: _type + required: true + schema: + type: integer + format: int64 + - name: type + in: query + description: type + required: true + schema: + type: string + - name: type_ + in: header + description: type_ + required: true + schema: + type: string + - name: http_debug_option + in: query + description: http debug option (to test parameter naming option) + required: true + schema: + type: string + responses: + 200: + description: OK +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: 'http://petstore.swagger.io/api/oauth/dialog' + scopes: + 'write:pets': modify pets in your account + 'read:pets': read your pets + api_key: + type: apiKey + name: api_key + in: header + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + xml: + name: Order + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + xml: + name: Category + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + xml: + name: User + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: Tag + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + xml: + name: photoUrl + wrapped: true + items: + type: string + tags: + type: array + xml: + name: tag + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + xml: + name: Pet + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + EnumTest: + properties: + emails: + items: + type: string + type: array + search: + enum: + - first_name + - last_name + - email + - full_name + type: string + sort_by: + items: + enum: + - first_name + - last_name + - email + type: string + type: array + type: object + PropertyNameMapping: + properties: + http_debug_operation: + type: string + _type: + type: string + type: + type: string + type_: + type: string + Animal: + type: object + required: + - className + properties: + className: + type: string + color: + type: string + default: red + discriminator: + propertyName: className + mapping: + DOG: '#/components/schemas/Dog' + CAT: '#/components/schemas/Cat' + Cat: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + declawed: + type: boolean + Dog: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + breed: + type: string + Treat: + oneOf: + - $ref: '#/components/schemas/DryFood' + - $ref: '#/components/schemas/WetFood' + discriminator: + propertyName: treatType + DryFood: + type: object + required: + - treatType + - kibbleSize + properties: + treatType: + type: string + kibbleSize: + type: number + format: double + WetFood: + type: object + required: + - treatType + - canSize + properties: + treatType: + type: string + canSize: + type: string + Collar: + type: object + required: + - collarType + properties: + collarType: + type: string + color: + type: string + oneOf: + - $ref: '#/components/schemas/LeatherCollar' + - $ref: '#/components/schemas/NylonCollar' + discriminator: + propertyName: collarType + mapping: + LEATHER: '#/components/schemas/LeatherCollar' + NYLON: '#/components/schemas/NylonCollar' + LeatherCollar: + type: object + properties: + grainType: + type: string + NylonCollar: + type: object + properties: + reflective: + type: boolean + Kennel: + type: object + properties: + name: + type: string + dogs: + type: array + items: + $ref: '#/components/schemas/Dog' diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml index 6b78aa8fc08a..e5cdbae40845 100644 --- a/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/scala/mixed-case-fields.yaml @@ -36,3 +36,32 @@ components: format: binary metadata: type: object + Animal: + type: object + required: + - className + properties: + className: + type: string + color: + type: string + default: red + discriminator: + propertyName: className + mapping: + DOG: '#/components/schemas/Dog' + CAT: '#/components/schemas/Cat' + Cat: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + declawed: + type: boolean + Dog: + allOf: + - $ref: '#/components/schemas/Animal' + - type: object + properties: + breed: + type: string diff --git a/samples/client/petstore/scala-sttp-circe/.openapi-generator/FILES b/samples/client/petstore/scala-sttp-circe/.openapi-generator/FILES index 7e661202c5c2..36d094a9e07c 100644 --- a/samples/client/petstore/scala-sttp-circe/.openapi-generator/FILES +++ b/samples/client/petstore/scala-sttp-circe/.openapi-generator/FILES @@ -10,11 +10,17 @@ 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/Animal.scala src/main/scala/org/openapitools/client/model/ApiResponse.scala +src/main/scala/org/openapitools/client/model/Cat.scala src/main/scala/org/openapitools/client/model/Category.scala +src/main/scala/org/openapitools/client/model/Collar.scala +src/main/scala/org/openapitools/client/model/Dog.scala src/main/scala/org/openapitools/client/model/EnumTest.scala +src/main/scala/org/openapitools/client/model/Kennel.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/PropertyNameMapping.scala src/main/scala/org/openapitools/client/model/Tag.scala +src/main/scala/org/openapitools/client/model/Treat.scala src/main/scala/org/openapitools/client/model/User.scala diff --git a/samples/client/petstore/scala-sttp-circe/README.md b/samples/client/petstore/scala-sttp-circe/README.md index d23798dac4ba..1fa29d4e86b3 100644 --- a/samples/client/petstore/scala-sttp-circe/README.md +++ b/samples/client/petstore/scala-sttp-circe/README.md @@ -91,13 +91,19 @@ Class | Method | HTTP request | Description ## Documentation for Models + - [Animal](Animal.md) - [ApiResponse](ApiResponse.md) + - [Cat](Cat.md) - [Category](Category.md) + - [Collar](Collar.md) + - [Dog](Dog.md) - [EnumTest](EnumTest.md) + - [Kennel](Kennel.md) - [Order](Order.md) - [Pet](Pet.md) - [PropertyNameMapping](PropertyNameMapping.md) - [Tag](Tag.md) + - [Treat](Treat.md) - [User](User.md) diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Animal.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Animal.scala new file mode 100644 index 000000000000..6a77fa7542e7 --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Animal.scala @@ -0,0 +1,32 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +trait Animal +object Animal { + implicit val encoderAnimal: Encoder[Animal] = Encoder.instance { + case obj: Cat => obj.asJson.mapObject(("className" -> "CAT".asJson) +: _) + case obj: Dog => obj.asJson.mapObject(("className" -> "DOG".asJson) +: _) + } + implicit val decoderAnimal: Decoder[Animal] = Decoder.instance { c => + c.downField("className").as[String].flatMap { + case "CAT" => c.as[Cat] + case "DOG" => c.as[Dog] + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +} + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala index 514f416b07a1..66394f9951d0 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/ApiResponse.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Cat.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Cat.scala new file mode 100644 index 000000000000..38fde3f6419c --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Cat.scala @@ -0,0 +1,45 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +case class Cat( + className: String, + color: Option[String] = None, + declawed: Option[Boolean] = None +) extends Animal +object Cat { + implicit val encoderCat: Encoder[Cat] = Encoder.instance { t => + Json.fromFields{ + Seq( + Some("className" -> t.className.asJson), + t.color.map(v => "color" -> v.asJson), + t.declawed.map(v => "declawed" -> v.asJson) + ).flatten + } + } + implicit val decoderCat: Decoder[Cat] = Decoder.instance { c => + for { + className <- c.downField("className").as[String] + color <- c.downField("color").as[Option[String]] + declawed <- c.downField("declawed").as[Option[Boolean]] + } yield Cat( + className = className, + color = color, + declawed = declawed + ) + } +} + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala index c03cc7fc6a1f..a7de397eb9b9 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Category.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Collar.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Collar.scala new file mode 100644 index 000000000000..9c52ef2abae0 --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Collar.scala @@ -0,0 +1,73 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +sealed trait Collar +object Collar { + implicit val encoderCollar: Encoder[Collar] = Encoder.instance { + case obj: LeatherCollar => obj.asJson.mapObject(("collarType" -> "LEATHER".asJson) +: _) + case obj: NylonCollar => obj.asJson.mapObject(("collarType" -> "NYLON".asJson) +: _) + } + implicit val decoderCollar: Decoder[Collar] = Decoder.instance { c => + c.downField("collarType").as[String].flatMap { + case "LEATHER" => c.as[LeatherCollar] + case "NYLON" => c.as[NylonCollar] + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +} + +case class LeatherCollar( + grainType: Option[String] = None +) extends Collar +object LeatherCollar { + implicit val encoderLeatherCollar: Encoder[LeatherCollar] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.grainType.map(v => "grainType" -> v.asJson) + ).flatten + } + } + implicit val decoderLeatherCollar: Decoder[LeatherCollar] = Decoder.instance { c => + for { + grainType <- c.downField("grainType").as[Option[String]] + } yield LeatherCollar( + grainType = grainType + ) + } +} + +case class NylonCollar( + reflective: Option[Boolean] = None +) extends Collar +object NylonCollar { + implicit val encoderNylonCollar: Encoder[NylonCollar] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.reflective.map(v => "reflective" -> v.asJson) + ).flatten + } + } + implicit val decoderNylonCollar: Decoder[NylonCollar] = Decoder.instance { c => + for { + reflective <- c.downField("reflective").as[Option[Boolean]] + } yield NylonCollar( + reflective = reflective + ) + } +} + + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Dog.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Dog.scala new file mode 100644 index 000000000000..56d25e4e722f --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Dog.scala @@ -0,0 +1,45 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +case class Dog( + className: String, + color: Option[String] = None, + breed: Option[String] = None +) extends Animal +object Dog { + implicit val encoderDog: Encoder[Dog] = Encoder.instance { t => + Json.fromFields{ + Seq( + Some("className" -> t.className.asJson), + t.color.map(v => "color" -> v.asJson), + t.breed.map(v => "breed" -> v.asJson) + ).flatten + } + } + implicit val decoderDog: Decoder[Dog] = Decoder.instance { c => + for { + className <- c.downField("className").as[String] + color <- c.downField("color").as[Option[String]] + breed <- c.downField("breed").as[Option[String]] + } yield Dog( + className = className, + color = color, + breed = breed + ) + } +} + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala index 27de86d3402d..1b7428b8edb5 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/EnumTest.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Kennel.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Kennel.scala new file mode 100644 index 000000000000..16a0b8cd5297 --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Kennel.scala @@ -0,0 +1,41 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +case class Kennel( + name: Option[String] = None, + dogs: Option[Seq[Dog]] = None +) +object Kennel { + implicit val encoderKennel: Encoder[Kennel] = Encoder.instance { t => + Json.fromFields{ + Seq( + t.name.map(v => "name" -> v.asJson), + t.dogs.map(v => "dogs" -> v.asJson) + ).flatten + } + } + implicit val decoderKennel: Decoder[Kennel] = Decoder.instance { c => + for { + name <- c.downField("name").as[Option[String]] + dogs <- c.downField("dogs").as[Option[Seq[Dog]]] + } yield Kennel( + name = name, + dogs = dogs + ) + } +} + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala index a3bf7ea39dcf..d5aa9a596d09 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Order.scala @@ -12,7 +12,7 @@ package org.openapitools.client.model import java.time.OffsetDateTime -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala index e9f8b4b2eb61..1767a660ac7c 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Pet.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala index cd870e77f4e9..7148a0485f67 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/PropertyNameMapping.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala index d1b9289b90cf..c569965d956b 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Tag.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._ diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Treat.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Treat.scala new file mode 100644 index 000000000000..10ee5bfcf544 --- /dev/null +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/Treat.scala @@ -0,0 +1,81 @@ +/** + * 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 io.circe.{Decoder, DecodingFailure, Encoder, Json} +import io.circe.syntax._ +import org.openapitools.client.core.JsonSupport._ + +sealed trait Treat +object Treat { + implicit val encoderTreat: Encoder[Treat] = Encoder.instance { + case obj: DryFood => obj.asJson.mapObject(("treatType" -> "DryFood".asJson) +: _) + case obj: WetFood => obj.asJson.mapObject(("treatType" -> "WetFood".asJson) +: _) + } + implicit val decoderTreat: Decoder[Treat] = Decoder.instance { c => + c.downField("treatType").as[String].flatMap { + case "DryFood" => c.as[DryFood] + case "WetFood" => c.as[WetFood] + case other => Left(DecodingFailure(s"Unknown discriminator value: $other", c.history)) + } + } +} + +case class DryFood( + treatType: String, + kibbleSize: Double +) extends Treat +object DryFood { + implicit val encoderDryFood: Encoder[DryFood] = Encoder.instance { t => + Json.fromFields{ + Seq( + Some("treatType" -> t.treatType.asJson), + Some("kibbleSize" -> t.kibbleSize.asJson) + ).flatten + } + } + implicit val decoderDryFood: Decoder[DryFood] = Decoder.instance { c => + for { + treatType <- c.downField("treatType").as[String] + kibbleSize <- c.downField("kibbleSize").as[Double] + } yield DryFood( + treatType = treatType, + kibbleSize = kibbleSize + ) + } +} + +case class WetFood( + treatType: String, + canSize: String +) extends Treat +object WetFood { + implicit val encoderWetFood: Encoder[WetFood] = Encoder.instance { t => + Json.fromFields{ + Seq( + Some("treatType" -> t.treatType.asJson), + Some("canSize" -> t.canSize.asJson) + ).flatten + } + } + implicit val decoderWetFood: Decoder[WetFood] = Decoder.instance { c => + for { + treatType <- c.downField("treatType").as[String] + canSize <- c.downField("canSize").as[String] + } yield WetFood( + treatType = treatType, + canSize = canSize + ) + } +} + + diff --git a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala index 339936997b69..273cf9191239 100644 --- a/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala +++ b/samples/client/petstore/scala-sttp-circe/src/main/scala/org/openapitools/client/model/User.scala @@ -11,7 +11,7 @@ */ package org.openapitools.client.model -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, DecodingFailure, Encoder, Json} import io.circe.syntax._ import org.openapitools.client.core.JsonSupport._