Skip to content

Commit 77ce8dd

Browse files
committed
Circe - fully qualify class names for components and properties to avoid naming conflicts
Previously, when a property was named the same as its component, references to the component class were misinterpreted as references to the property class, 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 77ce8dd

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)