Skip to content

Commit bd135d9

Browse files
committed
Added support for @description annotations to generate enriched DefType definitions
1 parent c2b946a commit bd135d9

6 files changed

Lines changed: 208 additions & 48 deletions

File tree

core/shared/src/main/scala-2/fabric/rw/RWMacros.scala

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,22 @@ object RWMacros {
4444
val name = field.asTerm.name
4545
val key = name.decodedName.toString
4646
val returnType = tpe.decl(name).typeSignature.asSeenFrom(tpe, tpe.typeSymbol.asClass)
47-
if (defaults.contains(index)) {
48-
q"$key -> implicitly[RW[$returnType]].definition.opt"
47+
val descAnn = field.annotations.find(_.tree.tpe =:= typeOf[description])
48+
val baseDef = if (defaults.contains(index)) {
49+
q"implicitly[RW[$returnType]].definition.opt"
4950
} else {
50-
q"$key -> implicitly[RW[$returnType]].definition"
51+
q"implicitly[RW[$returnType]].definition"
52+
}
53+
descAnn match {
54+
case Some(ann) =>
55+
ann.tree.children.tail.head match {
56+
case l: LiteralApi =>
57+
val text = l.value.value.toString
58+
q"$key -> $baseDef.describe($text)"
59+
case _ => q"$key -> $baseDef"
60+
}
61+
case None =>
62+
q"$key -> $baseDef"
5163
}
5264
}
5365
context.Expr[DefType](q"""

core/shared/src/main/scala-3/fabric/rw/CompileRW.scala

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ private def safeSimpleName(cls: Class[_]): String = {
4444
name.substring(start)
4545
}
4646

47+
private def safeTypeName(value: Any): String = value match {
48+
case p: Product => p.productPrefix
49+
case _ => safeTypeName(value)
50+
}
51+
4752
@nowarn()
4853
trait CompileRW {
4954
inline final def derived[T](using ct: ClassTag[T]): RW[T] = gen[T]
@@ -86,7 +91,7 @@ trait CompileRW {
8691
private lazy val childRWs = getSealedTraitChildren[T, m.MirroredElemTypes]
8792

8893
override def read(value: T): Json = {
89-
val typeName = safeSimpleName(value.getClass)
94+
val typeName = safeTypeName(value)
9095
val (_, rw) = childRWs.find(_._1 == typeName).getOrElse {
9196
throw RWException(s"Unknown subtype: $typeName")
9297
}
@@ -273,6 +278,16 @@ trait CompileRW {
273278
}
274279

275280
object CompileRW extends CompileRW {
281+
def applyFieldDescriptions(dt: DefType, descs: Map[String, String]): DefType = {
282+
if (descs.isEmpty) dt
283+
else dt match {
284+
case o: DefType.Obj => o.copy(map = o.map.map { case (k, v) =>
285+
descs.get(k).fold(k -> v)(d => k -> v.describe(d))
286+
})
287+
case other => other
288+
}
289+
}
290+
276291
/** Write a field value with error context wrapping. Non-inline to ensure try-catch works. */
277292
def writeField[T](writer: Writer[T], json: Json, className: String, fieldName: String): T = {
278293
try {
@@ -456,14 +471,20 @@ object CompileRW extends CompileRW {
456471
else child.typeRef
457472
}
458473

459-
val childExprs = childTypes.map { childType =>
474+
val childExprs = childSymbols.zip(childTypes).map { case (childSym, childType) =>
460475
childType.asType match {
461476
case '[t] =>
462477
val rw = Expr.summon[RW[t]].getOrElse {
463-
// No existing RW — generate one for case class children
464-
val childSym = childType.typeSymbol
478+
// No existing RW — generate one for case class or case object children
465479
if (childSym.isClassDef && childSym.flags.is(Flags.Case)) {
466480
genMacro[t]
481+
} else if (childSym.flags.is(Flags.Module)) {
482+
val ref = Ref(childSym.termRef.termSymbol).asExprOf[t]
483+
'{ RW.static[t]($ref) }
484+
} else if (childSym.flags.is(Flags.Enum) && childSym.flags.is(Flags.Case) && !childSym.isClassDef) {
485+
// Simple enum case (e.g., `case Point` in a mixed enum) — treated as singleton
486+
val ref = Ref(childSym.termRef.termSymbol).asExprOf[t]
487+
'{ RW.static[t]($ref) }
467488
} else {
468489
report.errorAndAbort(s"No RW found for child type ${childType.show}. Provide an RW instance or make it a case class.")
469490
}
@@ -504,7 +525,7 @@ object CompileRW extends CompileRW {
504525
private lazy val childRWs = $childRWsExpr
505526

506527
override def read(value: T): Json = {
507-
val typeName = safeSimpleName(value.getClass)
528+
val typeName = safeTypeName(value)
508529
val (_, rw) = childRWs.find(_._1 == typeName).getOrElse {
509530
throw RWException(s"Unknown subtype: $typeName")
510531
}
@@ -610,6 +631,10 @@ object CompileRW extends CompileRW {
610631
report.errorAndAbort(s"No ClassTag found for ${Type.show[T]}")
611632
}
612633

634+
// Extract @description annotations from constructor parameters
635+
val fieldDescs = extractFieldDescriptions(typeSymbol)
636+
val fieldDescsExpr = Expr(fieldDescs)
637+
613638
'{
614639
new ClassRW[T] {
615640
override protected def t2Map(t: T): Map[String, Json] = CompileRW.toMap(t)(using $mirror)
@@ -618,11 +643,27 @@ object CompileRW extends CompileRW {
618643
${ generateDirectConstructor[T]('{map}) }
619644
}
620645

621-
override def definition: DefType = CompileRW.toDefinition[T](using $mirror, $ct)
646+
override def definition: DefType = CompileRW.applyFieldDescriptions(
647+
CompileRW.toDefinition[T](using $mirror, $ct),
648+
$fieldDescsExpr
649+
)
622650
}
623651
}
624652
}
625653

654+
private def extractFieldDescriptions(typeSymbol: Any)(using Quotes): Map[String, String] = {
655+
import quotes.reflect._
656+
val sym = typeSymbol.asInstanceOf[Symbol]
657+
sym.primaryConstructor.paramSymss.flatten.flatMap { param =>
658+
param.annotations.collectFirst {
659+
case ann if ann.tpe.typeSymbol.fullName == "fabric.rw.description" =>
660+
ann match {
661+
case Apply(_, List(Literal(StringConstant(text)))) => param.name -> text
662+
}
663+
}
664+
}.toMap
665+
}
666+
626667
def genWMacro[T: Type](using Quotes): Expr[Writer[T]] = {
627668
'{
628669
new ClassW[T] {

core/shared/src/main/scala/fabric/define/DefType.scala

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import scala.util.Try
3030
sealed trait DefType {
3131
def className: Option[String]
3232

33+
def description: Option[String] = None
34+
35+
def describe(desc: String): DefType = DefType.Described(this, Some(desc))
36+
3337
def isOpt: Boolean = false
3438

3539
def isNull: Boolean = false
@@ -42,8 +46,13 @@ sealed trait DefType {
4246

4347
protected def template(path: JsonPath, config: TemplateConfig): Json
4448

45-
def merge(that: DefType): DefType =
46-
if (this == that) {
49+
def merge(that: DefType): DefType = {
50+
// Unwrap Described for comparison, descriptions are not preserved through merge
51+
val unwrappedThis = this match { case DefType.Described(dt, _) => dt; case _ => this }
52+
val unwrappedThat = that match { case DefType.Described(dt, _) => dt; case _ => that }
53+
if (unwrappedThis != this || unwrappedThat != that) {
54+
unwrappedThis.merge(unwrappedThat)
55+
} else if (this == that) {
4756
this
4857
} else if (this.isNull) {
4958
that.opt
@@ -60,35 +69,45 @@ sealed trait DefType {
6069
} else {
6170
throw new RuntimeException(s"Incompatible typed:\n$this\n\n$that")
6271
}
72+
}
6373
}
6474

6575
object DefType {
6676
implicit def rw: RW[DefType] = RW.from[DefType](r = dt2V, w = v2dt, d = DefType.Json)
6777

78+
private def withDesc(base: fabric.Obj, desc: Option[String]): fabric.Obj = desc match {
79+
case Some(d) => base.merge(fabric.Obj("description" -> str(d))).asObj
80+
case None => base
81+
}
82+
6883
private def dt2V(dt: DefType): Json = dt match {
69-
case Obj(map, cn) => obj(
84+
case Described(inner, desc) => withDesc(dt2V(inner).asObj, desc)
85+
case Obj(map, cn, desc) => withDesc(obj(
7086
"type" -> str("object"),
7187
"values" -> fabric.Obj(map.map { case (key, dt) => key -> dt2V(dt) }),
7288
"className" -> cn.json
73-
)
74-
case Arr(t) => obj("type" -> str("array"), "value" -> dt2V(t))
75-
case Opt(t) => obj("type" -> str("optional"), "value" -> dt2V(t))
89+
), desc)
90+
case Arr(t, desc) => withDesc(obj("type" -> str("array"), "value" -> dt2V(t)), desc)
91+
case Opt(t, desc) => withDesc(obj("type" -> str("optional"), "value" -> dt2V(t)), desc)
7692
case Str => obj("type" -> str("string"))
7793
case Int => obj("type" -> str("numeric"), "precision" -> str("integer"))
7894
case Dec => obj("type" -> str("numeric"), "precision" -> str("decimal"))
7995
case Bool => obj("type" -> str("boolean"))
80-
case Enum(values, cn) => obj("type" -> str("enum"), "values" -> values, "className" -> cn.json)
81-
case Poly(values, cn) => obj(
96+
case Enum(values, cn, desc) => withDesc(obj("type" -> str("enum"), "values" -> values, "className" -> cn.json), desc)
97+
case Poly(values, cn, desc) => withDesc(obj(
8298
"type" -> str("poly"),
8399
"values" -> values.map { case (key, dt) => key -> dt2V(dt) },
84100
"className" -> cn.json
85-
)
101+
), desc)
86102
case Json => obj("type" -> str("json"))
87103
case Null => obj("type" -> str("null"))
88104
}
89105

106+
private def readDesc(o: fabric.Obj): Option[String] = o.get("description").map(_.asString)
107+
90108
private def v2dt(v: Json): DefType = {
91109
val o = v.asObj
110+
val desc = readDesc(o)
92111
o.value("type").asString match {
93112
case "object" =>
94113
val map: Map[String, Json] = o.value
@@ -100,30 +119,31 @@ object DefType {
100119
case fabric.Null => None
101120
case s: Str => Some(s.value)
102121
case j => throw new RuntimeException(s"Unsupported className value: $j")
103-
}
104-
)
105-
case "array" => Arr(
106-
t = v2dt(o.value("value"))
122+
},
123+
description = desc
107124
)
108-
case "optional" => Opt(v2dt(o.value("value")))
109-
case "string" => Str
125+
case "array" => Arr(t = v2dt(o.value("value")), description = desc)
126+
case "optional" => Opt(v2dt(o.value("value")), description = desc)
127+
case "string" => desc.fold[DefType](Str)(Str.describe)
110128
case "numeric" => o.value("precision").asString match {
111-
case "integer" => Int
112-
case "decimal" => Dec
129+
case "integer" => desc.fold[DefType](Int)(Int.describe)
130+
case "decimal" => desc.fold[DefType](Dec)(Dec.describe)
113131
}
114-
case "boolean" => Bool
115-
case "enum" => Enum(o.value("values").asVector.toList, o.get("className").map(_.asString))
132+
case "boolean" => desc.fold[DefType](Bool)(Bool.describe)
133+
case "enum" => Enum(o.value("values").asVector.toList, o.get("className").map(_.asString), description = desc)
116134
case "poly" =>
117-
Poly(o.value("values").asMap.map { case (key, json) => key -> v2dt(json) }, o.get("className").map(_.asString))
118-
case "json" => Json
119-
case "null" => Null
135+
Poly(o.value("values").asMap.map { case (key, json) => key -> v2dt(json) }, o.get("className").map(_.asString), description = desc)
136+
case "json" => desc.fold[DefType](Json)(Json.describe)
137+
case "null" => desc.fold[DefType](Null)(Null.describe)
120138
}
121139
}
122140

123-
case class Obj(map: Map[String, DefType], className: Option[String]) extends DefType {
141+
case class Obj(map: Map[String, DefType], className: Option[String], override val description: Option[String] = None) extends DefType {
142+
override def describe(desc: String): Obj = copy(description = Some(desc))
143+
124144
override def merge(that: DefType): DefType = that match {
125-
case Obj(thatMap, cn) => Obj(mergeMap(map, thatMap), cn)
126-
case Opt(Obj(thatMap, cn)) => Opt(Obj(mergeMap(map, thatMap), cn))
145+
case Obj(thatMap, cn, _) => Obj(mergeMap(map, thatMap), cn)
146+
case Opt(Obj(thatMap, cn, _), _) => Opt(Obj(mergeMap(map, thatMap), cn))
127147
case _ => super.merge(that)
128148
}
129149

@@ -142,11 +162,12 @@ object DefType {
142162
object Obj {
143163
def apply(className: Option[String], entries: (String, DefType)*): Obj = Obj(VectorMap(entries*), className)
144164
}
145-
case class Arr(t: DefType) extends DefType {
165+
case class Arr(t: DefType, override val description: Option[String] = None) extends DefType {
146166
override def className: Option[String] = None
167+
override def describe(desc: String): Arr = copy(description = Some(desc))
147168

148169
override def merge(that: DefType): DefType = that match {
149-
case Arr(thatType) => Arr(t.merge(thatType))
170+
case Arr(thatType, _) => Arr(t.merge(thatType))
150171
case Null => this
151172
case _ => super.merge(that)
152173
}
@@ -157,13 +178,14 @@ object DefType {
157178
t.template(path \ 2, config)
158179
)
159180
}
160-
case class Opt(t: DefType) extends DefType {
181+
case class Opt(t: DefType, override val description: Option[String] = None) extends DefType {
161182
override def className: Option[String] = Some("scala.Option")
162183
override def isOpt: Boolean = true
163184
override def opt: DefType = this
185+
override def describe(desc: String): Opt = copy(description = Some(desc))
164186

165187
override def merge(that: DefType): DefType = that match {
166-
case Opt(thatOpt) => t.merge(thatOpt) match {
188+
case Opt(thatOpt, _) => t.merge(thatOpt) match {
167189
case o: Opt => o
168190
case result => Opt(result)
169191
}
@@ -211,12 +233,23 @@ object DefType {
211233

212234
override protected def template(path: JsonPath, config: TemplateConfig): Json = config.json(path)
213235
}
214-
case class Enum(values: List[Json], className: Option[String]) extends DefType {
236+
case class Enum(values: List[Json], className: Option[String], override val description: Option[String] = None) extends DefType {
237+
override def describe(desc: String): Enum = copy(description = Some(desc))
215238
override protected def template(path: JsonPath, config: TemplateConfig): Json = config.`enum`(path, values)
216239
}
217-
case class Poly(values: Map[String, DefType], className: Option[String]) extends DefType {
240+
case class Poly(values: Map[String, DefType], className: Option[String], override val description: Option[String] = None) extends DefType {
241+
override def describe(desc: String): Poly = copy(description = Some(desc))
218242
override protected def template(path: JsonPath, config: TemplateConfig): Json = values.head._2.template(path, config)
219243
}
244+
case class Described(dt: DefType, override val description: Option[String]) extends DefType {
245+
override def className: Option[String] = dt.className
246+
override def isOpt: Boolean = dt.isOpt
247+
override def isNull: Boolean = dt.isNull
248+
override def opt: DefType = Described(dt.opt, description)
249+
override def describe(desc: String): Described = copy(description = Some(desc))
250+
override def merge(that: DefType): DefType = dt.merge(that)
251+
override protected def template(path: JsonPath, config: TemplateConfig): Json = dt.template(path, config)
252+
}
220253
case object Null extends DefType {
221254
override def className: Option[String] = None
222255

core/shared/src/main/scala/fabric/define/FabricGenerator.scala

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,27 +48,28 @@ object FabricGenerator {
4848
val classExtras = extras(rootName)
4949
val map: Map[String, DefType] = original.filterNot {
5050
case (_, DefType.Null) => true
51-
case (_, DefType.Arr(DefType.Null)) => true
51+
case (_, DefType.Arr(DefType.Null, _)) => true
5252
case _ => false
5353
}
5454
def typeFor(name: String, dt: DefType): String = dt match {
55-
case DefType.Obj(map, _) =>
55+
case DefType.Described(inner, _) => typeFor(name, inner)
56+
case DefType.Obj(map, _, _) =>
5657
val className = resolver(name)
5758
additional = generate(className, map) :: additional
5859
if (className.contains('.')) {
5960
className.substring(className.lastIndexOf('.') + 1)
6061
} else {
6162
className
6263
}
63-
case DefType.Arr(DefType.Opt(t)) => s"Vector[${typeFor(name, t)}]"
64-
case DefType.Arr(t) => s"Vector[${typeFor(name, t)}]"
65-
case DefType.Opt(t) => s"Option[${typeFor(name, t)}]"
64+
case DefType.Arr(DefType.Opt(t, _), _) => s"Vector[${typeFor(name, t)}]"
65+
case DefType.Arr(t, _) => s"Vector[${typeFor(name, t)}]"
66+
case DefType.Opt(t, _) => s"Option[${typeFor(name, t)}]"
6667
case DefType.Str => "String"
6768
case DefType.Int => "Long"
6869
case DefType.Dec => "BigDecimal"
6970
case DefType.Bool => "Boolean"
70-
case DefType.Enum(_, _) => throw new RuntimeException("Unsupported")
71-
case DefType.Poly(_, _) => throw new RuntimeException("Unsupported")
71+
case DefType.Enum(_, _, _) => throw new RuntimeException("Unsupported")
72+
case DefType.Poly(_, _, _) => throw new RuntimeException("Unsupported")
7273
case DefType.Json => "Json"
7374
case DefType.Null => throw new RuntimeException(
7475
"Null type found in definition! Not supported for code generation!"
@@ -120,7 +121,7 @@ object FabricGenerator {
120121
}
121122

122123
dt match {
123-
case DefType.Obj(map, _) => generate(rootName, map)
124+
case DefType.Obj(map, _, _) => generate(rootName, map)
124125
case _ => throw new RuntimeException(
125126
s"Only DefType.Obj is supported for generation, but received: $dt"
126127
)

0 commit comments

Comments
 (0)