Skip to content

Commit 59a8894

Browse files
committed
Circe - fully qualify references within component classes
Previously, when a property was named the same as its component, references to the component class and property class conflicted, causing errors. Fully qualifying the references to each should prevent this from happening. A regression test covering a minimal failing case is included. Fixes issue guardrail-dev#2050
1 parent cd6955b commit 59a8894

File tree

3 files changed

+67
-26
lines changed

3 files changed

+67
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
openapi: 3.0.1
2+
info:
3+
title: Minimal Error Case
4+
description: Testing that internal naming conflicts do not occur when a component and its own property have the same name
5+
version: "1.0"
6+
servers:
7+
- url: "http://localhost:1234"
8+
paths:
9+
/test:
10+
get:
11+
operationId: Test
12+
responses:
13+
'200':
14+
description: A test
15+
content:
16+
application/json:
17+
schema:
18+
$ref: '#/components/schemas/Test'
19+
components:
20+
schemas:
21+
Test:
22+
title: Test
23+
required:
24+
- test
25+
type: object
26+
properties:
27+
test:
28+
title: test
29+
enum:
30+
- optionA
31+
- optionB
32+
type: string

modules/scala-support/src/main/scala/dev/guardrail/generators/scala/circe/CirceProtocolGenerator.scala

+34-26
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
493493
}
494494

495495
private[this] def renderIntermediate(
496+
clsName: NonEmptyList[String],
496497
model: Tracker[Schema[_]],
497498
dtoName: String,
498499
concreteTypes: List[PropMeta[ScalaLanguage]],
@@ -510,12 +511,12 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
510511
): Target[(PropMeta[ScalaLanguage], (Option[Defn.Val], Option[Defn.Val], Defn.Class))] =
511512
for {
512513
prefixes <- Cl.vendorPrefixes()
513-
customTpe = CustomTypeName(model, prefixes).getOrElse(dtoName)
514-
tpe <- Sc.pureTypeName(customTpe)
514+
customTpe = NonEmptyList.of(CustomTypeName(model, prefixes).getOrElse(dtoName))
515+
tpe <- Sc.pureTypeName(customTpe.last)
515516
props <- extractProperties(model)
516517
requiredFields = getRequiredFieldsRec(model)
517518
(params, nestedDefinitions) <- prepareProperties(
518-
NonEmptyList.of(customTpe),
519+
customTpe,
519520
propertyToTypeLookup = Map.empty,
520521
props,
521522
requiredFields,
@@ -526,10 +527,10 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
526527
defaultPropertyRequirement,
527528
components
528529
)
529-
encoder <- encodeModel(customTpe, dtoPackage, params, parents = List.empty)
530-
decoder <- decodeModel(customTpe, dtoPackage, supportPackage, params, parents = List.empty)
531-
defn <- renderDTOClass(customTpe, supportPackage, params, parents = List.empty)
532-
} yield (PropMeta[ScalaLanguage](customTpe, tpe), (encoder, decoder, defn))
530+
encoder <- encodeModel(clsName ::: customTpe, dtoPackage, params, parents = List.empty)
531+
decoder <- decodeModel(clsName ::: customTpe, dtoPackage, supportPackage, params, parents = List.empty)
532+
defn <- renderDTOClass(customTpe.last, supportPackage, params, parents = List.empty)
533+
} yield (PropMeta[ScalaLanguage](customTpe.last, tpe), (encoder, decoder, defn))
533534

534535
private[this] def fromModel(
535536
clsName: NonEmptyList[String],
@@ -565,8 +566,8 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
565566
defaultPropertyRequirement,
566567
components
567568
)
568-
encoder <- encodeModel(clsName.last, dtoPackage, params, parents)
569-
decoder <- decodeModel(clsName.last, dtoPackage, supportPackage, params, parents)
569+
encoder <- encodeModel(clsName, dtoPackage, params, parents)
570+
decoder <- decodeModel(clsName, dtoPackage, supportPackage, params, parents)
570571
tpe <- parseTypeName(clsName.last)
571572
fullType <- selectType(dtoPackage.foldRight(clsName)((x, xs) => xs.prepend(x)))
572573
nestedClasses <- nestedDefinitions.flatTraverse {
@@ -652,6 +653,7 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
652653
case other => Target.raiseUserError(s"Unexpected type ${other}")
653654
}
654655
(pm, defns) <- renderIntermediate(
656+
clsName,
655657
model,
656658
dtoName,
657659
concreteTypes,
@@ -867,7 +869,7 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
867869
paramsAndNestedDefinitions <- props.traverse[Target, (Tracker[ProtocolParameter[ScalaLanguage]], Option[NestedProtocolElems[ScalaLanguage]])] {
868870
case (name, schema) =>
869871
for {
870-
typeName <- formatTypeName(name).map(formattedName => getClsName(name).append(formattedName))
872+
typeName <- formatTypeName(name).map(formattedName => getClsName(name).prependList(dtoPackage).append(formattedName))
871873
tpe <- selectType(typeName)
872874
maybeNestedDefinition <- processProperty(name, schema)
873875
resolvedType <- ModelResolver.propMetaWithName[ScalaLanguage, Target](() => Target.pure(tpe), schema, components)
@@ -1407,19 +1409,21 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
14071409
} yield names.flatMap(n => reduced.get(n))
14081410

14091411
private def encodeModel(
1410-
clsName: String,
1412+
clsName: NonEmptyList[String],
14111413
dtoPackage: List[String],
14121414
selfParams: List[ProtocolParameter[ScalaLanguage]],
14131415
parents: List[SuperClass[ScalaLanguage]] = Nil
1414-
) =
1416+
)(implicit Lt: LanguageTerms[ScalaLanguage, Target]) = {
1417+
import Lt._
14151418
for {
14161419
() <- Target.pure(())
14171420
discriminators = parents.flatMap(_.discriminators)
14181421
discriminatorNames = discriminators.map(_.propertyName).toSet
1419-
allParams <- finalizeParams(parents.reverse.flatMap(_.params) ++ selfParams)
1422+
allParams <- finalizeParams(parents.reverse.flatMap(_.params) ++ selfParams)
1423+
qualifiedClsType <- selectType(clsName.prependList(dtoPackage))
14201424
(discriminatorParams, params) = allParams.partition(param => discriminatorNames.contains(param.name.value))
14211425
readOnlyKeys: List[String] = params.flatMap(_.readOnlyKey).toList
1422-
typeName = Type.Name(clsName)
1426+
14231427
encVal = {
14241428
def encodeStatic(param: ProtocolParameter[ScalaLanguage], clsName: String) =
14251429
q"""(${Lit.String(param.name.value)}, _root_.io.circe.Json.fromString(${Lit.String(clsName)}))"""
@@ -1448,14 +1452,14 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
14481452
}
14491453
}
14501454

1451-
val pairsWithStatic = pairs ++ discriminatorParams.map(encodeStatic(_, clsName))
1455+
val pairsWithStatic = pairs ++ discriminatorParams.map(encodeStatic(_, clsName.last))
14521456
val simpleCase = q"_root_.scala.Vector(..${pairsWithStatic})"
14531457
val allFields = optional.foldLeft[Term](simpleCase) { (acc, field) =>
14541458
q"$acc ++ $field"
14551459
}
14561460

14571461
q"""
1458-
${circeVersion.encoderObjectCompanion}.instance[${Type.Name(clsName)}](a => _root_.io.circe.JsonObject.fromIterable($allFields))
1462+
${circeVersion.encoderObjectCompanion}.instance[${qualifiedClsType}](a => _root_.io.circe.JsonObject.fromIterable($allFields))
14591463
"""
14601464
}
14611465
(readOnlyDefn, readOnlyFilter) = NonEmptyList.fromList(readOnlyKeys).fold((List.empty[Stat], identity[Term] _)) { roKeys =>
@@ -1466,24 +1470,28 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
14661470
}
14671471

14681472
} yield Option(q"""
1469-
implicit val ${suffixClsName("encode", clsName)}: ${circeVersion.encoderObject}[${Type.Name(clsName)}] = {
1473+
implicit val ${suffixClsName("encode", clsName.last)}: ${circeVersion.encoderObject}[${qualifiedClsType}] = {
14701474
..${readOnlyDefn};
14711475
${readOnlyFilter(encVal)}
14721476
}
14731477
""")
1478+
}
14741479

14751480
private def decodeModel(
1476-
clsName: String,
1481+
clsName: NonEmptyList[String],
14771482
dtoPackage: List[String],
14781483
supportPackage: List[String],
14791484
selfParams: List[ProtocolParameter[ScalaLanguage]],
14801485
parents: List[SuperClass[ScalaLanguage]] = Nil
14811486
)(implicit Lt: LanguageTerms[ScalaLanguage, Target]): Target[Option[Defn.Val]] = {
1487+
import Lt._
14821488
for {
14831489
() <- Target.pure(())
14841490
discriminators = parents.flatMap(_.discriminators)
14851491
discriminatorNames = discriminators.map(_.propertyName).toSet
1486-
allParams <- finalizeParams(parents.reverse.flatMap(_.params) ++ selfParams)
1492+
allParams <- finalizeParams(parents.reverse.flatMap(_.params) ++ selfParams)
1493+
qualifiedClsType <- selectType(clsName.prependList(dtoPackage))
1494+
qualifiedClsTerm <- selectTerm(clsName.prependList(dtoPackage))
14871495
params = allParams.filterNot(param => discriminatorNames.contains(param.name.value))
14881496
needsEmptyToNull: Boolean = params.exists(_.emptyToNull == EmptyIsNull)
14891497
paramCount = params.length
@@ -1492,9 +1500,9 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
14921500
if (paramCount == 0) {
14931501
Target.pure(
14941502
q"""
1495-
new _root_.io.circe.Decoder[${Type.Name(clsName)}] {
1496-
final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[${Type.Name(clsName)}] =
1497-
_root_.scala.Right(${Term.Name(clsName)}())
1503+
new _root_.io.circe.Decoder[${qualifiedClsType}] {
1504+
final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[${qualifiedClsType}] =
1505+
_root_.scala.Right(${qualifiedClsTerm}())
14981506
}
14991507
"""
15001508
)
@@ -1569,17 +1577,17 @@ class CirceProtocolGenerator private (circeVersion: CirceModelGenerator, applyVa
15691577
.map { pairs =>
15701578
val (terms, enumerators) = pairs.unzip
15711579
q"""
1572-
new _root_.io.circe.Decoder[${Type.Name(clsName)}] {
1573-
final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[${Type.Name(clsName)}] =
1580+
new _root_.io.circe.Decoder[${qualifiedClsType}] {
1581+
final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[${qualifiedClsType}] =
15741582
for {
15751583
..${enumerators}
1576-
} yield ${Term.Name(clsName)}(..${terms})
1584+
} yield ${qualifiedClsTerm}(..${terms})
15771585
}
15781586
"""
15791587
}
15801588
}
15811589
} yield Option(q"""
1582-
implicit val ${suffixClsName("decode", clsName)}: _root_.io.circe.Decoder[${Type.Name(clsName)}] = $decVal
1590+
implicit val ${suffixClsName("decode", clsName.last)}: _root_.io.circe.Decoder[${qualifiedClsType}] = $decVal
15831591
""")
15841592
}
15851593

project/src/main/scala/RegressionTests.scala

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ object RegressionTests {
8686
ExampleCase(sampleResource("issues/issue1260.yaml"), "issues.issue1260"),
8787
ExampleCase(sampleResource("issues/issue1218.yaml"), "issues.issue1218").frameworks("scala" -> Set("http4s", "http4s-v0.22")),
8888
ExampleCase(sampleResource("issues/issue1594.yaml"), "issues.issue1594"),
89+
ExampleCase(sampleResource("issues/issue2050.yaml"), "issues.issue2050"),
8990
ExampleCase(sampleResource("multipart-form-data.yaml"), "multipartFormData"),
9091
ExampleCase(sampleResource("petstore.json"), "examples").args("--import", "examples.support.PositiveLong"),
9192
// ExampleCase(sampleResource("petstore-openapi-3.0.2.yaml"), "examples.petstore.openapi302").args("--import", "examples.support.PositiveLong"),

0 commit comments

Comments
 (0)