guardrail-dev / guardrail-dev/guardrail

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

Open
#2,050 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Scala
Stars
541
Forks
138
PR merge metrics
No merged PRs in 30d

Description

I've been getting errors when generating code for a YAML OpenAPI 3 spec ([this one](https://developer.data.nationalhighways.co.uk/api-details#api=road-and-lane-closures&operation=RoadClosures), 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 https://github.com/guardrail-dev/guardrail/issues/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)
}
}
```

Contributor guide

Open the contributing guide

Research direction

Reproduce the failure from the minimal OpenAPI specification by running sbt compile in an sbt-guardrail project, then inspect the generated definitions/Test.scala and its compiler errors. Done means code generation produces Scala that compiles when a component and one of its enum properties have the same name.

Written by the indexing model from the issue text.

Assessment

Tech stack
openapi, scala
Domain
devtools
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.