Skip to content

Commit 3c3e2ad

Browse files
committed
Rewrite structural tests to actually test .structural method
Per reviewer feedback on PR #614: - EnumToUnionSpec now tests schema.structural conversion for Scala 3 enums - SealedTraitToUnionSpec now tests schema.structural for sealed traits - Tests verify structural schemas are Variants with Tag markers - Tests verify DynamicValue round-trip through structural schema - Removed AsPrimitiveSpec (unrelated to structural types)
1 parent 31531b2 commit 3c3e2ad

File tree

2 files changed

+363
-0
lines changed

2 files changed

+363
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package zio.blocks.schema.structural
2+
3+
import zio.blocks.schema._
4+
import zio.test._
5+
6+
/**
7+
* Tests for Scala 3 enum to structural union type conversion.
8+
*
9+
* Per issue #517: Enums convert to union types with Tag discriminators.
10+
* Example: enum Color { Red, Green, Blue } → Schema[{type Tag = "Red"} | {type
11+
* Tag = "Green"} | {type Tag = "Blue"}]
12+
*/
13+
object EnumToUnionSpec extends ZIOSpecDefault {
14+
15+
enum Color {
16+
case Red, Green, Blue
17+
}
18+
19+
enum Status {
20+
case Active, Inactive, Suspended
21+
}
22+
23+
enum Shape {
24+
case Circle(radius: Double)
25+
case Rectangle(width: Double, height: Double)
26+
case Triangle(base: Double, height: Double)
27+
}
28+
29+
def spec = suite("EnumToUnionSpec")(
30+
suite("Structural Conversion")(
31+
test("simple enum converts to structural union") {
32+
val schema = Schema.derived[Color]
33+
val structural = schema.structural
34+
assertTrue(structural != null)
35+
},
36+
test("structural enum schema is a Variant") {
37+
val schema = Schema.derived[Color]
38+
val structural = schema.structural
39+
val isVariant = (structural.reflect: @unchecked) match {
40+
case _: Reflect.Variant[_, _] => true
41+
case _ => false
42+
}
43+
assertTrue(isVariant)
44+
},
45+
test("structural enum preserves case count") {
46+
val schema = Schema.derived[Status]
47+
val structural = schema.structural
48+
val caseCount = (structural.reflect: @unchecked) match {
49+
case v: Reflect.Variant[_, _] => v.cases.size
50+
case _ => -1
51+
}
52+
assertTrue(caseCount == 3)
53+
},
54+
test("structural enum type name contains Tag markers") {
55+
val schema = Schema.derived[Color]
56+
val structural = schema.structural
57+
val typeName = structural.reflect.typeName.name
58+
assertTrue(
59+
typeName.contains("Tag:\"Red\""),
60+
typeName.contains("Tag:\"Green\""),
61+
typeName.contains("Tag:\"Blue\""),
62+
typeName.contains("|")
63+
)
64+
},
65+
test("parameterized enum structural preserves case fields") {
66+
val schema = Schema.derived[Shape]
67+
val structural = schema.structural
68+
69+
val circleFields = (structural.reflect: @unchecked) match {
70+
case v: Reflect.Variant[_, _] =>
71+
v.cases.find(_.name == "Circle").flatMap { c =>
72+
(c.value: @unchecked) match {
73+
case r: Reflect.Record[_, _] => Some(r.fields.map(_.name).toSet)
74+
case _ => None
75+
}
76+
}
77+
case _ => None
78+
}
79+
assertTrue(
80+
circleFields.isDefined,
81+
circleFields.get.contains("radius")
82+
)
83+
}
84+
),
85+
suite("Structural Schema Behavior")(
86+
test("structural enum encodes via DynamicValue") {
87+
val schema = Schema.derived[Color]
88+
val structural = schema.structural
89+
val value: Color = Color.Red
90+
91+
val structuralAny = structural.asInstanceOf[Schema[Any]]
92+
val dynamic = structuralAny.toDynamicValue(value)
93+
94+
assertTrue(dynamic match {
95+
case DynamicValue.Variant("Red", _) => true
96+
case _ => false
97+
})
98+
},
99+
test("structural enum decodes from DynamicValue") {
100+
val schema = Schema.derived[Color]
101+
val structural = schema.structural
102+
103+
val dynamic = DynamicValue.Variant("Green", DynamicValue.Record(Vector.empty))
104+
105+
val structuralAny = structural.asInstanceOf[Schema[Any]]
106+
val result = structuralAny.fromDynamicValue(dynamic)
107+
108+
assertTrue(result.isRight)
109+
},
110+
test("structural enum round-trips through DynamicValue") {
111+
val schema = Schema.derived[Shape]
112+
val structural = schema.structural
113+
val value: Shape = Shape.Circle(5.0)
114+
115+
val structuralAny = structural.asInstanceOf[Schema[Any]]
116+
val dynamic = structuralAny.toDynamicValue(value)
117+
val result = structuralAny.fromDynamicValue(dynamic)
118+
119+
assertTrue(result match {
120+
case Right(recovered) =>
121+
val shape = recovered.asInstanceOf[Shape]
122+
shape == value
123+
case _ => false
124+
})
125+
},
126+
test("all structural enum cases round-trip correctly") {
127+
val schema = Schema.derived[Color]
128+
val structural = schema.structural
129+
130+
val values = List(Color.Red, Color.Green, Color.Blue)
131+
132+
val structuralAny = structural.asInstanceOf[Schema[Any]]
133+
val results = values.map { value =>
134+
val dynamic = structuralAny.toDynamicValue(value)
135+
structuralAny.fromDynamicValue(dynamic).map(_.asInstanceOf[Color])
136+
}
137+
138+
assertTrue(results == values.map(Right(_)))
139+
},
140+
test("parameterized enum structural preserves field data") {
141+
val schema = Schema.derived[Shape]
142+
val structural = schema.structural
143+
val value: Shape = Shape.Rectangle(10.0, 20.0)
144+
145+
val structuralAny = structural.asInstanceOf[Schema[Any]]
146+
val dynamic = structuralAny.toDynamicValue(value)
147+
148+
val hasCorrectData = dynamic match {
149+
case DynamicValue.Variant("Rectangle", DynamicValue.Record(fields)) =>
150+
val fieldMap = fields.toMap
151+
fieldMap.get("width").contains(DynamicValue.Primitive(PrimitiveValue.Double(10.0))) &&
152+
fieldMap.get("height").contains(DynamicValue.Primitive(PrimitiveValue.Double(20.0)))
153+
case _ => false
154+
}
155+
assertTrue(hasCorrectData)
156+
}
157+
)
158+
)
159+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package zio.blocks.schema.structural
2+
3+
import zio.blocks.schema._
4+
import zio.test._
5+
6+
/**
7+
* Tests for sealed trait to structural union type conversion (Scala 3 only).
8+
*
9+
* Per issue #517: Sealed traits convert to union types with Tag discriminators.
10+
* Example: sealed trait Result { Success, Failure } → Schema[{ type Tag =
11+
* "Success"; def value: Int } | { type Tag = "Failure"; def error: String }]
12+
*/
13+
object SealedTraitToUnionSpec extends ZIOSpecDefault {
14+
15+
sealed trait Result
16+
object Result {
17+
case class Success(value: Int) extends Result
18+
case class Failure(error: String) extends Result
19+
}
20+
21+
sealed trait Status
22+
object Status {
23+
case object Active extends Status
24+
case object Inactive extends Status
25+
}
26+
27+
sealed trait Animal
28+
object Animal {
29+
case class Dog(name: String, breed: String) extends Animal
30+
case class Cat(name: String, indoor: Boolean) extends Animal
31+
case class Bird(name: String, canFly: Boolean) extends Animal
32+
}
33+
34+
def spec = suite("SealedTraitToUnionSpec")(
35+
suite("Structural Conversion")(
36+
test("sealed trait converts to structural union") {
37+
val schema = Schema.derived[Result]
38+
val structural = schema.structural
39+
assertTrue(structural != null)
40+
},
41+
test("structural sealed trait schema is a Variant") {
42+
val schema = Schema.derived[Result]
43+
val structural = schema.structural
44+
val isVariant = (structural.reflect: @unchecked) match {
45+
case _: Reflect.Variant[_, _] => true
46+
case _ => false
47+
}
48+
assertTrue(isVariant)
49+
},
50+
test("structural sealed trait preserves case count") {
51+
val schema = Schema.derived[Result]
52+
val structural = schema.structural
53+
val caseCount = (structural.reflect: @unchecked) match {
54+
case v: Reflect.Variant[_, _] => v.cases.size
55+
case _ => -1
56+
}
57+
assertTrue(caseCount == 2)
58+
},
59+
test("structural sealed trait type name contains Tag markers") {
60+
val schema = Schema.derived[Result]
61+
val structural = schema.structural
62+
val typeName = structural.reflect.typeName.name
63+
assertTrue(
64+
typeName.contains("|"),
65+
typeName.contains("Tag:\"Success\""),
66+
typeName.contains("Tag:\"Failure\""),
67+
typeName.contains("value:Int"),
68+
typeName.contains("error:String")
69+
)
70+
},
71+
test("sealed trait with case objects converts to structural") {
72+
val schema = Schema.derived[Status]
73+
val structural = schema.structural
74+
val caseCount = (structural.reflect: @unchecked) match {
75+
case v: Reflect.Variant[_, _] => v.cases.size
76+
case _ => -1
77+
}
78+
assertTrue(caseCount == 2)
79+
},
80+
test("three variant sealed trait structural preserves all cases") {
81+
val schema = Schema.derived[Animal]
82+
val structural = schema.structural
83+
val caseNames = (structural.reflect: @unchecked) match {
84+
case v: Reflect.Variant[_, _] => v.cases.map(_.name).toSet
85+
case _ => Set.empty[String]
86+
}
87+
assertTrue(
88+
caseNames.size == 3,
89+
caseNames.contains("Dog"),
90+
caseNames.contains("Cat"),
91+
caseNames.contains("Bird")
92+
)
93+
}
94+
),
95+
suite("Structural Schema Behavior")(
96+
test("structural sealed trait encodes via DynamicValue") {
97+
val schema = Schema.derived[Result]
98+
val structural = schema.structural
99+
val value: Result = Result.Success(42)
100+
101+
val structuralAny = structural.asInstanceOf[Schema[Any]]
102+
val dynamic = structuralAny.toDynamicValue(value)
103+
104+
assertTrue(dynamic match {
105+
case DynamicValue.Variant("Success", DynamicValue.Record(fields)) =>
106+
val fieldMap = fields.toMap
107+
fieldMap.get("value").contains(DynamicValue.Primitive(PrimitiveValue.Int(42)))
108+
case _ => false
109+
})
110+
},
111+
test("structural sealed trait decodes from DynamicValue") {
112+
val schema = Schema.derived[Result]
113+
val structural = schema.structural
114+
115+
val dynamic = DynamicValue.Variant(
116+
"Success",
117+
DynamicValue.Record(Vector("value" -> DynamicValue.Primitive(PrimitiveValue.Int(100))))
118+
)
119+
120+
val structuralAny = structural.asInstanceOf[Schema[Any]]
121+
val result = structuralAny.fromDynamicValue(dynamic)
122+
123+
assertTrue(result match {
124+
case Right(recovered) =>
125+
val r = recovered.asInstanceOf[Result]
126+
r == Result.Success(100)
127+
case _ => false
128+
})
129+
},
130+
test("structural sealed trait round-trips through DynamicValue") {
131+
val schema = Schema.derived[Result]
132+
val structural = schema.structural
133+
val value: Result = Result.Failure("error message")
134+
135+
val structuralAny = structural.asInstanceOf[Schema[Any]]
136+
val dynamic = structuralAny.toDynamicValue(value)
137+
val result = structuralAny.fromDynamicValue(dynamic)
138+
139+
assertTrue(result match {
140+
case Right(recovered) =>
141+
val r = recovered.asInstanceOf[Result]
142+
r == value
143+
case _ => false
144+
})
145+
},
146+
test("structural case object round-trips correctly") {
147+
val schema = Schema.derived[Status]
148+
val structural = schema.structural
149+
val value: Status = Status.Active
150+
151+
val structuralAny = structural.asInstanceOf[Schema[Any]]
152+
val dynamic = structuralAny.toDynamicValue(value)
153+
val result = structuralAny.fromDynamicValue(dynamic)
154+
155+
assertTrue(result match {
156+
case Right(recovered) =>
157+
val s = recovered.asInstanceOf[Status]
158+
s == value
159+
case _ => false
160+
})
161+
},
162+
test("all structural animal variants round-trip correctly") {
163+
val schema = Schema.derived[Animal]
164+
val structural = schema.structural
165+
166+
val dog: Animal = Animal.Dog("Rex", "German Shepherd")
167+
val cat: Animal = Animal.Cat("Whiskers", true)
168+
val bird: Animal = Animal.Bird("Tweety", true)
169+
170+
val structuralAny = structural.asInstanceOf[Schema[Any]]
171+
172+
val dogResult = structuralAny.fromDynamicValue(structuralAny.toDynamicValue(dog)).map(_.asInstanceOf[Animal])
173+
val catResult = structuralAny.fromDynamicValue(structuralAny.toDynamicValue(cat)).map(_.asInstanceOf[Animal])
174+
val birdResult = structuralAny.fromDynamicValue(structuralAny.toDynamicValue(bird)).map(_.asInstanceOf[Animal])
175+
176+
assertTrue(
177+
dogResult == Right(dog),
178+
catResult == Right(cat),
179+
birdResult == Right(bird)
180+
)
181+
},
182+
test("structural animal variants preserve field information") {
183+
val schema = Schema.derived[Animal]
184+
val structural = schema.structural
185+
186+
val dogFields = (structural.reflect: @unchecked) match {
187+
case v: Reflect.Variant[_, _] =>
188+
v.cases.find(_.name == "Dog").flatMap { c =>
189+
(c.value: @unchecked) match {
190+
case r: Reflect.Record[_, _] => Some(r.fields.map(_.name).toSet)
191+
case _ => None
192+
}
193+
}
194+
case _ => None
195+
}
196+
assertTrue(
197+
dogFields.isDefined,
198+
dogFields.get.contains("name"),
199+
dogFields.get.contains("breed")
200+
)
201+
}
202+
)
203+
)
204+
}

0 commit comments

Comments
 (0)