Skip to content

Commit 99102d7

Browse files
authored
Merge pull request #332 from Equwece/generic_configuration
Add JsonConfiguration processing
2 parents 86da314 + 802ec35 commit 99102d7

File tree

7 files changed

+385
-37
lines changed

7 files changed

+385
-37
lines changed

README.md

+96
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ account.asJson == json
250250

251251
## Configuration
252252

253+
254+
### Configuration via **ReaderBuilder** and **WriterBuilder**
253255
1. You can configure only case class derivation
254256
2. To configure **JsonReader** use **ReaderBuilder**
255257
3. To configure **JsonWriter** use **WriterBuilder**
@@ -311,8 +313,102 @@ inline given ReaderBuilder[Foo] =
311313
case 2 => JsonReader[Int]
312314
case _ => JsonReader[Option[Boolean]]
313315
}
316+
317+
// ensure that json contains only fields that JsonReader knows about, otherwise throw ReaderError
318+
.strict
319+
```
320+
321+
### Configuration via **JsonConfiguration**
322+
1. To configure both **JsonWriter** and **JsonReader** you can use **JsonConfiguration**
323+
2. **JsonConfiguration** can be provided as an inline given to derives
324+
```scala 3
325+
inline given JsonConfiguration = JsonConfiguration.default
326+
```
327+
3. **JsonConfiguration** will be applied recursively to all nested readers/writers
328+
* Product types
329+
```scala 3
330+
import tethys.*
331+
import tethys.jackson.*
332+
333+
inline given JsonConfiguration =
334+
JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase)
335+
336+
case class Inner(innerField: String)
337+
case class Outer(outerField: Inner) derives JsonWriter, JsonReader
338+
339+
val outer = Outer(Inner("fooBar"))
340+
val json = """{"outer_field": {"inner_field": "fooBar"}}"""
341+
342+
json.jsonAs[Outer] == Right(outer)
343+
outer.asJson == json
344+
345+
```
346+
* Sum types
347+
```scala 3
348+
import tethys.*
349+
import tethys.jackson.*
350+
351+
inline given JsonConfiguration =
352+
JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase)
353+
354+
enum Choice(@selector val select: Int) derives JsonReader, JsonWriter:
355+
case First(firstField: Int) extends Choice(0)
356+
case Second(secondField: String) extends Choice(1)
357+
358+
val first = Choice.First(1)
359+
val second = Choice.Second("foo")
360+
val firstJson = """{"select": 0, "first_field": 1}"""
361+
val secondJson = """{"select": 1, "second_field": "foo"}"""
362+
363+
first.asJson == firstJson
364+
second.asJson == secondJson
365+
366+
firstJson.jsonAs[Choice] == first
367+
secondJson.jsonAs[Choice] == second
368+
```
369+
4. **WriterBuilder** and **ReaderBuilder** settings have higher priority than **JsonConfiguration** settings
370+
```scala 3
371+
import tethys.*
372+
import tethys.jackson.*
373+
374+
case class Customer(
375+
id: Long,
376+
phoneNumber: String
377+
) derives JsonWriter, JsonReader
378+
379+
inline given JsonConfiguration =
380+
JsonConfiguration.default.fieldStyle(FieldStyle.LowerSnakeCase)
381+
382+
inline given WriterBuilder[Customer] =
383+
// has higher priority than JsonConfiguration's fieldStyle
384+
WriterBuilder[Customer].fieldStyle(FieldStyle.UpperCase)
385+
386+
inline given ReaderBuilder[Customer] =
387+
// has higher priority than JsonConfiguration's fieldStyle
388+
ReaderBuilder[Customer].fieldStyle(FieldStyle.UpperCase)
389+
390+
val customer = Customer(id = 5L, phoneNumber = "+123")
391+
val json = """{"ID": 5, "PHONENUMBER": "+123"}"""
392+
393+
json.jsonAs[Customer] == Right(customer)
394+
customer.asJson == json
395+
314396
```
397+
5. **JsonConfiguration** features
398+
```scala 3
315399

400+
inline given JsonConfiguration =
401+
JsonConfiguration
402+
// default config, entrypoint for configuration
403+
.default
404+
405+
// choose field style
406+
.fieldStyle(FieldStyle.UpperSnakeCase)
407+
408+
// ensure that json contains only fields that JsonReader knows about, otherwise throw ReaderError
409+
// applicable only for JsonReader
410+
.strict
411+
```
316412

317413
## integrations
318414
In some cases, you may need to work with raw AST,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package tethys
2+
3+
trait JsonConfiguration:
4+
def fieldStyle(fieldStyle: FieldStyle): JsonConfiguration
5+
6+
def strict: JsonConfiguration
7+
8+
object JsonConfiguration:
9+
@scala.annotation.compileTimeOnly(
10+
"JsonConfiguration should be declared as inline given"
11+
)
12+
def default: JsonConfiguration = throw IllegalAccessException()

modules/core/src/main/scala-3/tethys/derivation/ConfigurationMacroUtils.scala

+133-19
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,41 @@ trait ConfigurationMacroUtils:
3636
case failure: ImplicitSearchFailure =>
3737
None
3838

39+
private val FieldStyleAlreadyConfigured = "FieldStyle is already configured"
40+
41+
private def showUnknownConfigTree(tree: String): String =
42+
s"Unknown tree. Config must be an inlined given. \nTree: $tree"
43+
44+
private def mergeWriterMacroConfigs(
45+
writerBuilderConfig: WriterBuilderMacroConfig,
46+
jsonConfig: WriterBuilderMacroConfig
47+
): WriterBuilderMacroConfig =
48+
writerBuilderConfig.copy(fieldStyle =
49+
writerBuilderConfig.fieldStyle.orElse(jsonConfig.fieldStyle)
50+
)
51+
52+
private def mergeReaderMacroConfigs(
53+
readerBuilderConfig: ReaderBuilderMacroConfig,
54+
jsonConfig: ReaderBuilderMacroConfig
55+
): ReaderBuilderMacroConfig =
56+
readerBuilderConfig.copy(
57+
fieldStyle = readerBuilderConfig.fieldStyle.orElse(jsonConfig.fieldStyle),
58+
isStrict = readerBuilderConfig.isStrict.orElse(jsonConfig.isStrict)
59+
)
60+
3961
def prepareWriterProductFields[T: Type](
40-
config: Expr[WriterBuilder[T]]
62+
config: Expr[WriterBuilder[T]],
63+
jsonConfig: Expr[JsonConfiguration]
4164
): List[WriterField] =
42-
val macroConfig = parseWriterBuilderMacroConfig[T](config)
43-
val updates = macroConfig.update.map(it => it.name -> it).toMap
65+
val writerConfig = parseWriterBuilderMacroConfig[T](config)
66+
val parsedJsonConfig = parseWriterMacroJsonConfig(jsonConfig)
67+
val mergedConfig = mergeWriterMacroConfigs(writerConfig, parsedJsonConfig)
68+
val updates = mergedConfig.update.map(it => it.name -> it).toMap
4469
val tpe = TypeRepr.of[T]
4570
tpe.typeSymbol.caseFields.zipWithIndex
46-
.filterNot((symbol, _) => macroConfig.delete(symbol.name))
71+
.filterNot((symbol, _) => mergedConfig.delete(symbol.name))
4772
.collect { (symbol, idx) =>
48-
val name = macroConfig.fieldStyle.fold(symbol.name)(
73+
val name = mergedConfig.fieldStyle.fold(symbol.name)(
4974
FieldStyle.applyStyle(symbol.name, _)
5075
)
5176
updates.get(symbol.name) match
@@ -65,7 +90,49 @@ trait ConfigurationMacroUtils:
6590
tpe = tpe.memberType(symbol),
6691
newName = None
6792
)
68-
} ::: macroConfig.add
93+
} ::: mergedConfig.add
94+
95+
private def parseWriterMacroJsonConfig(
96+
config: Expr[JsonConfiguration]
97+
): WriterBuilderMacroConfig = {
98+
@tailrec
99+
def loop(
100+
config: Expr[JsonConfiguration],
101+
acc: WriterBuilderMacroConfig = WriterBuilderMacroConfig()
102+
): WriterBuilderMacroConfig =
103+
config match
104+
case '{
105+
JsonConfiguration.default
106+
} =>
107+
acc
108+
109+
case '{
110+
($rest: JsonConfiguration).strict
111+
} =>
112+
loop(rest, acc)
113+
114+
case '{
115+
($rest: JsonConfiguration).fieldStyle(${ fieldStyle }: FieldStyle)
116+
} =>
117+
acc.fieldStyle match
118+
case None =>
119+
loop(rest, acc.copy(fieldStyle = Some(fieldStyle.valueOrAbort)))
120+
case Some(_) =>
121+
report.errorAndAbort(FieldStyleAlreadyConfigured)
122+
123+
case other =>
124+
other.asTerm match
125+
case Inlined(_, _, term) =>
126+
loop(term.asExprOf[JsonConfiguration])
127+
case _ =>
128+
report.errorAndAbort(
129+
showUnknownConfigTree(
130+
other.asTerm.show(using Printer.TreeStructure)
131+
)
132+
)
133+
134+
loop(traverseTree(config.asTerm).asExprOf[JsonConfiguration])
135+
}
69136

70137
private def parseWriterBuilderMacroConfig[T: Type](
71138
config: Expr[WriterBuilder[T]]
@@ -362,24 +429,28 @@ trait ConfigurationMacroUtils:
362429
loop(term.asExprOf[WriterBuilder[T]])
363430
case _ =>
364431
report.errorAndAbort(
365-
s"Unknown tree. Config must be an inlined given.\nTree: ${other.asTerm
366-
.show(using Printer.TreeStructure)}"
432+
showUnknownConfigTree(
433+
other.asTerm.show(using Printer.TreeStructure)
434+
)
367435
)
368436

369437
loop(traverseTree(config.asTerm).asExprOf[WriterBuilder[T]])._1
370438

371439
end parseWriterBuilderMacroConfig
372440

373441
def prepareReaderProductFields[T: Type](
374-
config: Expr[ReaderBuilder[T]]
442+
config: Expr[ReaderBuilder[T]],
443+
jsonConfig: Expr[JsonConfiguration]
375444
): (List[ReaderField], IsStrict) =
376-
val macroConfig = parseReaderBuilderMacroConfig[T](config)
445+
val readerConfig = parseReaderBuilderMacroConfig[T](config)
446+
val parsedJsonConfig = parseReaderMacroJsonConfig(jsonConfig)
447+
val mergedConfig = mergeReaderMacroConfigs(readerConfig, parsedJsonConfig)
377448
val tpe = TypeRepr.of[T]
378449
val defaults = collectDefaults[T]
379450
val fields = tpe.typeSymbol.caseFields.zipWithIndex
380451
.map { case (symbol, idx) =>
381452
val default = defaults.get(idx).map(_.asExprOf[Any])
382-
macroConfig.extracted.get(symbol.name) match
453+
mergedConfig.extracted.get(symbol.name) match
383454
case Some(field: ReaderField.Basic) =>
384455
val updatedDefault = field.extractor match
385456
case None => default
@@ -392,15 +463,15 @@ trait ConfigurationMacroUtils:
392463
)
393464
.map(_.asExprOf[Any])
394465

395-
field.update(idx, updatedDefault, macroConfig.fieldStyle)
466+
field.update(idx, updatedDefault, mergedConfig.fieldStyle)
396467

397468
case Some(field) =>
398-
field.update(idx, default, macroConfig.fieldStyle)
469+
field.update(idx, default, mergedConfig.fieldStyle)
399470

400471
case None =>
401472
ReaderField
402473
.Basic(symbol.name, tpe.memberType(symbol), None)
403-
.update(idx, default, macroConfig.fieldStyle)
474+
.update(idx, default, mergedConfig.fieldStyle)
404475
}
405476
val existingFieldNames = fields.map(_.name).toSet
406477
val additionalFields = fields
@@ -420,7 +491,47 @@ trait ConfigurationMacroUtils:
420491
.distinctBy(_.name)
421492
val allFields = fields ::: additionalFields
422493
checkLoops(allFields)
423-
(sortDependencies(allFields), macroConfig.isStrict)
494+
(sortDependencies(allFields), mergedConfig.isStrict.getOrElse(false))
495+
496+
private def parseReaderMacroJsonConfig(
497+
jsonConfig: Expr[JsonConfiguration]
498+
): ReaderBuilderMacroConfig =
499+
@tailrec
500+
def loop(
501+
config: Expr[JsonConfiguration],
502+
acc: ReaderBuilderMacroConfig = ReaderBuilderMacroConfig()
503+
): ReaderBuilderMacroConfig =
504+
config match
505+
case '{
506+
JsonConfiguration.default
507+
} =>
508+
acc
509+
510+
case '{
511+
($rest: JsonConfiguration).strict
512+
} =>
513+
acc.copy(isStrict = Some(true))
514+
515+
case '{
516+
($rest: JsonConfiguration).fieldStyle($fieldStyle: FieldStyle)
517+
} =>
518+
acc.fieldStyle match
519+
case None =>
520+
loop(rest, acc.copy(fieldStyle = Some(fieldStyle.valueOrAbort)))
521+
case Some(_) =>
522+
report.errorAndAbort(FieldStyleAlreadyConfigured)
523+
524+
case other =>
525+
other.asTerm match
526+
case Inlined(_, _, term) =>
527+
loop(term.asExprOf[JsonConfiguration])
528+
case _ =>
529+
report.errorAndAbort(
530+
showUnknownConfigTree(
531+
other.asTerm.show(using Printer.TreeStructure)
532+
)
533+
)
534+
loop(traverseTree(jsonConfig.asTerm).asExprOf[JsonConfiguration])
424535

425536
private def sortDependencies(fields: List[ReaderField]): List[ReaderField] =
426537
val known = fields.map(_.name).toSet
@@ -526,6 +637,7 @@ trait ConfigurationMacroUtils:
526637
s"Field '$name' exists in your model, use selector or .extract(_.$name).as[...] instead"
527638
)
528639

640+
@tailrec
529641
def loop(
530642
config: Expr[ReaderBuilder[T]],
531643
acc: ReaderBuilderMacroConfig = ReaderBuilderMacroConfig(Map.empty)
@@ -583,9 +695,10 @@ trait ConfigurationMacroUtils:
583695
case '{ ($rest: ReaderBuilder[T]).strict } =>
584696
loop(
585697
config = rest,
586-
acc = acc.copy(isStrict = true)
698+
acc = acc.copy(isStrict = Some(true))
587699
)
588700
case other =>
701+
@tailrec
589702
def loopInner(
590703
term: Term,
591704
extractors: List[(String, TypeRepr)] = Nil,
@@ -664,8 +777,9 @@ trait ConfigurationMacroUtils:
664777
)
665778
case other =>
666779
report.errorAndAbort(
667-
s"Unknown tree. Config must be an inlined given.\nTree: ${other
668-
.show(using Printer.TreeStructure)}"
780+
showUnknownConfigTree(
781+
other.show(using Printer.TreeStructure)
782+
)
669783
)
670784

671785
loopInner(other.asTerm)
@@ -1056,7 +1170,7 @@ trait ConfigurationMacroUtils:
10561170
case class ReaderBuilderMacroConfig(
10571171
extracted: Map[String, ReaderField] = Map.empty,
10581172
fieldStyle: Option[FieldStyle] = None,
1059-
isStrict: IsStrict = false
1173+
isStrict: Option[IsStrict] = None
10601174
):
10611175
def withExtracted(field: ReaderField): ReaderBuilderMacroConfig =
10621176
copy(extracted = extracted.updated(field.name, field))

0 commit comments

Comments
 (0)