Skip to content

Code generation fails when components are named identically to one of their enum properties #2050

@Jonnty

Description

@Jonnty

I've been getting errors when generating code for a YAML OpenAPI 3 spec (this one, incidentally). They appear to be happen when one of the enum properties of a component is the same as the component itself. The cause seems to be that attempts to reference the component class are taken to be references to the identically named property object within the class when such an object happens to exist. (This doesn't happen for standard, non-enum strings as no child object is generated.)

The conflict is not case-sensitive, as generated class/object names are automatically camel cased.

I think the suggested fix for #1147 - fully qualifying component class names within generated code - would probably fix this as well.

I've narrowed this down into a minimal failing case - I get the following error when attempting to generate code for the specification below it by running sbt compile on a project using sbt-guardrail 1.0.0-M1.

Error

[info] compiling 1 Scala source to C:\...\untitled\target\scala-2.13\classes ...
[error] C:\...\untitled\target\scala-2.13\src_managed\main\nhclosures\definitions\Test.scala:13:129: value test is not a member of nhclosures.definitions.Test.Test
[error]     _root_.io.circe.Encoder.AsObject.instance[Test](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("test", a.test.asJson))))
[error]                                                                                                                                 ^
[error] C:\...\untitled\target\scala-2.13\src_managed\main\nhclosures\definitions\Test.scala:15:219: type Test is not a member of object nhclosures.definitions.Test.Test
[error]   implicit val decodeTest: _root_.io.circe.Decoder[Test] = new _root_.io.circe.Decoder[Test] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Test] = for (v0 <- c.downField("test").as[Test.Test]) yield Test(v0) }
[error]                                                                                                                                                                                                                           ^
[error] C:\...\untitled\target\scala-2.13\src_managed\main\nhclosures\definitions\Test.scala:15:236: nhclosures.definitions.Test.Test.type does not take parameters
[error]   implicit val decodeTest: _root_.io.circe.Decoder[Test] = new _root_.io.circe.Decoder[Test] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Test] = for (v0 <- c.downField("test").as[Test.Test]) yield Test(v0) }
[error]                                                                                                                                                                                                                                            ^
[error] C:\...\untitled\target\scala-2.13\src_managed\main\nhclosures\definitions\Test.scala:10:28: type Test is not a member of object nhclosures.definitions.Test.Test
[error] case class Test(test: Test.Test)

Spec used

openapi: 3.0.1
info:
  title: Minimal Error Case
  version: "1.0"
servers:
  - url: https://example.com
paths:
  /test:
    get:
      operationId: Test
      responses:
        '200':
          description: A test
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Test'
components:
  schemas:
    Test:
      title: Test
      required:
        - test
      type: object
      properties:
        test:
          title: test
          enum:
            - optionA
            - optionB
          type: string

Code generated (Test.scala)

/*
 * This file was generated by guardrail (https://github.com/guardrail-dev/guardrail).
 * Modifications will be overwritten; instead edit the OpenAPI/Swagger spec file.
 */
package nhclosures.definitions
import cats.syntax.either._
import io.circe.syntax._
import cats.instances.all._
import _root_.nhclosures.Implicits._
case class Test(test: Test.Test)
object Test {
  implicit val encodeTest: _root_.io.circe.Encoder.AsObject[Test] = {
    _root_.io.circe.Encoder.AsObject.instance[Test](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("test", a.test.asJson))))
  }
  implicit val decodeTest: _root_.io.circe.Decoder[Test] = new _root_.io.circe.Decoder[Test] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Test] = for (v0 <- c.downField("test").as[Test.Test]) yield Test(v0) }
  sealed abstract class Test(val value: String) extends _root_.scala.Product with _root_.scala.Serializable { override def toString: String = value.toString }
  object Test {
    object members {
      case object OptionA extends Test("optionA")
      case object OptionB extends Test("optionB")
    }
    val OptionA: Test = members.OptionA
    val OptionB: Test = members.OptionB
    val values = _root_.scala.Vector(OptionA, OptionB)
    implicit val encodeTest: _root_.io.circe.Encoder[Test] = _root_.io.circe.Encoder[String].contramap(_.value)
    implicit val decodeTest: _root_.io.circe.Decoder[Test] = _root_.io.circe.Decoder[String].emap(value => from(value).toRight(s"$value not a member of Test"))
    implicit val showTest: Show[Test] = Show[String].contramap[Test](_.value)
    def from(value: String): _root_.scala.Option[Test] = values.find(_.value == value)
    implicit val order: cats.Order[Test] = cats.Order.by[Test, Int](values.indexOf)
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions