From dcd86c8c5e1d96469699ed5b324b03a1f1d4b6cd Mon Sep 17 00:00:00 2001 From: Vivek Mahajan Date: Sun, 19 Jul 2026 10:53:42 +0200 Subject: [PATCH] [scala-sttp4] Fix enum operation parameters: type import and wire-value toString Enums used as operation parameters in the scala-sttp4 generator are broken in two ways: 1. Generated code does not compile: the API imports enum types Enumeration-style ('import pkg.MyEnum._'), inherited from scala-sttp where 'type MyEnum = Value' lives inside the companion. scala-sttp4 enums are sealed traits + case objects, so the wildcard does not bring the type into scope. Import the type itself instead; same-package model imports need no import at all, so the enum special-case (and the now unused enumRefs/isEnumClass/getEnumRefs helpers) is removed. 2. Path/query interpolation serializes the Scala identifier instead of the wire value: uri"...${param}..." uses toString, so a request for 'sold-out' goes out as '/pets/SoldOut'. Override toString on each enum case object with its wire value, matching scala-sttp's Enumeration behavior where Value("sold-out").toString was the wire value. JSON codecs are unaffected (circe and json4s both map values explicitly). Covered by Sttp4CodegenTest#verifyEnumParameterImportAndWireValues; sttp4 petstore samples regenerated (jsoniter has its own codegen/templates and is unaffected). Made-With: pi --- .../languages/ScalaSttp4ClientCodegen.java | 57 +++---------------- .../main/resources/scala-sttp4/model.mustache | 4 +- .../codegen/scala/Sttp4CodegenTest.java | 44 ++++++++++++++ .../resources/3_0/scala/sttp4-enum-param.yaml | 38 +++++++++++++ .../org/openapitools/client/model/Order.scala | 6 +- .../org/openapitools/client/model/Pet.scala | 6 +- .../org/openapitools/client/model/Order.scala | 6 +- .../org/openapitools/client/model/Pet.scala | 6 +- 8 files changed, 105 insertions(+), 62 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/scala/sttp4-enum-param.yaml 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 c8e70af1270d..1e0285c66d0b 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 @@ -75,8 +75,6 @@ public class ScalaSttp4ClientCodegen extends AbstractScalaCodegen implements Cod protected boolean renderJavadoc = true; protected boolean removeOAuthSecurities = true; - Map enumRefs = new HashMap<>(); - public ScalaSttp4ClientCodegen() { super(); generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) @@ -507,7 +505,6 @@ private void propagateParentProperties(CodegenModel childModel, List - * append '._" if the import is a Enum class, otherwise * remove model imports to avoid warnings for importing class in the same package in Scala * * @param models processed models to be further processed @@ -516,8 +513,6 @@ private void propagateParentProperties(CodegenModel childModel, List models) { final String prefix = modelPackage() + "."; - enumRefs = getEnumRefs(models); - for (String openAPIName : models.keySet()) { CodegenModel model = ModelUtils.getModelByName(openAPIName, models); if (model == null) { @@ -534,13 +529,11 @@ private void postProcessUpdateImports(final Map models) { Iterator> iterator = imports.iterator(); while (iterator.hasNext()) { String importPath = iterator.next().get("import"); - Map item = new HashMap<>(); - if (importPath.startsWith(prefix)) { - if (isEnumClass(importPath, enumRefs)) { - item.put("import", importPath.concat("._")); - newImports.add(item); - } - } else { + // Same-package imports are dropped: sttp4 enums are sealed traits + + // case objects (not Enumeration), so unlike scala-sttp there is no + // `MyEnum._` wildcard needed to bring a type alias into scope. + if (!importPath.startsWith(prefix)) { + Map item = new HashMap<>(); item.put("import", importPath); newImports.add(item); } @@ -550,37 +543,6 @@ private void postProcessUpdateImports(final Map models) { } } - private Map getEnumRefs(final Map models) { - Map enums = new HashMap<>(); - for (String key : models.keySet()) { - CodegenModel model = ModelUtils.getModelByName(key, models); - if (model.isEnum) { - ModelsMap objs = models.get(key); - enums.put(key, objs); - } - } - return enums; - } - - private boolean isEnumClass(final String importPath, final Map enumModels) { - if (enumModels == null || enumModels.isEmpty()) { - return false; - } - for (ModelsMap objs : enumModels.values()) { - List models = objs.getModels(); - if (models == null || models.isEmpty()) { - continue; - } - for (final Map model : models) { - String enumImportPath = (String) model.get("importPath"); - if (enumImportPath != null && enumImportPath.equals(importPath)) { - return true; - } - } - } - return false; - } - @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { if (registerNonStandardStatusCodes) { @@ -618,11 +580,10 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List item = new HashMap<>(); - if (isEnumClass(importPath, enumRefs)) { - item.put("import", importPath.concat("._")); - } else { - item.put("import", importPath); - } + // sttp4 enums are sealed traits + case objects (not Enumeration): + // import the type itself, not `MyEnum._` wildcard members, which + // would not bring the type into scope. + item.put("import", importPath); newImports.add(item); } } 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 d26af861beaa..716618904a00 100644 --- a/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache +++ b/modules/openapi-generator/src/main/resources/scala-sttp4/model.mustache @@ -258,7 +258,7 @@ sealed trait {{classname}} object {{classname}} { {{#allowableValues}} {{#values}} - case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{classname}} + case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{classname}} { override def toString: String = "{{.}}" } {{/values}} {{/allowableValues}} @@ -338,7 +338,7 @@ object {{classname}}Enums { sealed trait {{datatypeWithEnum}} object {{datatypeWithEnum}} { {{#_enum}} - case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{datatypeWithEnum}} + case object {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} extends {{datatypeWithEnum}} { override def toString: String = "{{.}}" } {{/_enum}} {{#circe}} 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 9b44ed0a3019..85236a31f077 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 @@ -55,6 +55,50 @@ public void verifyApiKeyLocations() throws IOException { assertFileContains(path, ".cookie(\"apikey\", apiKeyCookie)"); } + @Test + public void verifyEnumParameterImportAndWireValues() 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-enum-param.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, "true"); + generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false"); + generator.opts(input).generate(); + + // Enums are sealed traits + case objects: the API must import the type itself, + // not Enumeration-style wildcard members (which would not bring the type into scope). + Path apiPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/api/DefaultApi.scala"); + assertFileContains(apiPath, "import org.openapitools.client.model.PetStatus\n"); + assertFileNotContains(apiPath, "import org.openapitools.client.model.PetStatus._"); + + // Models live in the same package as the enum: no import is needed at all + // (sealed trait, not an Enumeration with a type alias inside the companion). + Path modelPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/Pet.scala"); + assertFileNotContains(modelPath, "import org.openapitools.client.model.PetStatus"); + + // Case objects carry their wire value as toString so path/query interpolation + // (uri"...${param}...") serializes the wire value, not the Scala identifier. + Path enumPath = Paths.get(outputPath + "/src/main/scala/org/openapitools/client/model/PetStatus.scala"); + assertFileContains(enumPath, "case object Available extends PetStatus { override def toString: String = \"available\" }"); + assertFileContains(enumPath, "case object SoldOut extends PetStatus { override def toString: String = \"sold-out\" }"); + } + @Test public void verifyOneOfSupportWithCirce() throws IOException { File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); diff --git a/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-enum-param.yaml b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-enum-param.yaml new file mode 100644 index 000000000000..8f3c2ccb68c8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/scala/sttp4-enum-param.yaml @@ -0,0 +1,38 @@ +openapi: 3.0.3 +info: + title: Enum parameter test + version: 1.0.0 +paths: + /pets/{status}: + get: + operationId: getPetsByStatus + parameters: + - name: status + in: path + required: true + schema: + $ref: '#/components/schemas/PetStatus' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' +components: + schemas: + PetStatus: + type: string + enum: + - available + - pending + - sold-out + Pet: + type: object + required: + - name + properties: + name: + type: string + status: + $ref: '#/components/schemas/PetStatus' 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 index 6cb20a7a07cd..09bf24ef8c13 100644 --- 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 @@ -38,9 +38,9 @@ object OrderEnums { sealed trait Status object Status { - case object Placed extends Status - case object Approved extends Status - case object Delivered extends Status + case object Placed extends Status { override def toString: String = "placed" } + case object Approved extends Status { override def toString: String = "approved" } + case object Delivered extends Status { override def toString: String = "delivered" } import io.circe.{Encoder, Decoder} 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 index 88f0ce279b25..127ca9c8354d 100644 --- 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 @@ -37,9 +37,9 @@ object PetEnums { sealed trait Status object Status { - case object Available extends Status - case object Pending extends Status - case object Sold extends Status + case object Available extends Status { override def toString: String = "available" } + case object Pending extends Status { override def toString: String = "pending" } + case object Sold extends Status { override def toString: String = "sold" } import io.circe.{Encoder, Decoder} 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 893cad11d288..c3b07d869d04 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 @@ -30,9 +30,9 @@ object OrderEnums { sealed trait Status object Status { - case object Placed extends Status - case object Approved extends Status - case object Delivered extends Status + case object Placed extends Status { override def toString: String = "placed" } + case object Approved extends Status { override def toString: String = "approved" } + case object Delivered extends Status { override def toString: String = "delivered" } import org.json4s._ 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 d5805371fac8..baf3c20544e9 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 @@ -29,9 +29,9 @@ object PetEnums { sealed trait Status object Status { - case object Available extends Status - case object Pending extends Status - case object Sold extends Status + case object Available extends Status { override def toString: String = "available" } + case object Pending extends Status { override def toString: String = "pending" } + case object Sold extends Status { override def toString: String = "sold" } import org.json4s._