From c9b965cfed0035814bfc9ea89422d141739c21fb Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 31 May 2023 22:47:11 +0300 Subject: [PATCH 1/5] format --- build.sbt | 13 +++ .../scala/evo/derivation/LazySummon.scala | 10 ++ .../evo/derivation/logstage/EvoLog.scala | 73 ++++++++++++ .../evo/derivation/logstage/instances.scala | 9 ++ .../derivation/logstage/LogstageTest.scala | 105 ++++++++++++++++++ project/Version.scala | 2 + 6 files changed, 212 insertions(+) create mode 100644 modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala create mode 100644 modules/logstage/src/main/scala/evo/derivation/logstage/instances.scala create mode 100644 modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala diff --git a/build.sbt b/build.sbt index 52f5bd2..3f71865 100644 --- a/build.sbt +++ b/build.sbt @@ -120,3 +120,16 @@ lazy val playJson = project defaultSettings, ) .dependsOn(derivation) + +lazy val logstage = project + .in(modules / "logstage") + .settings( + name := "derivation-logstage", + libraryDependencies ++= Seq( + "io.7mind.izumi" %% "logstage-core" % Version.logstage, + "io.7mind.izumi" %% "logstage-rendering-circe" % Version.logstage % Test, + "io.circe" %% "circe-parser" % Version.circe % Test, + ), + defaultSettings, + ) + .dependsOn(derivation) diff --git a/modules/core/src/main/scala/evo/derivation/LazySummon.scala b/modules/core/src/main/scala/evo/derivation/LazySummon.scala index 85a9faa..02f5dfd 100644 --- a/modules/core/src/main/scala/evo/derivation/LazySummon.scala +++ b/modules/core/src/main/scala/evo/derivation/LazySummon.scala @@ -92,6 +92,16 @@ object LazySummon: } .toVector + def useForeach[U, Info](fields: Fields, infos: Vector[Info])( + f: [A] => (Info, A, TC[A]) => U, + ): Unit = + fields.toArray + .lazyZip(all) + .lazyZip(infos) + .foreach[U] { (field, inst, info) => + f(info, field.asInstanceOf[inst.FieldType], inst.tc) + } + def useEitherFast[Info, E](infos: IArray[Info])( f: (summon: Of[TC], info: Info) => Either[E, summon.FieldType], ): Either[E, Fields] = diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala new file mode 100644 index 0000000..1955bbe --- /dev/null +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala @@ -0,0 +1,73 @@ +package evo.derivation.logstage + +import evo.derivation.LazySummon.All +import evo.derivation.{LazySummon, ValueClass} +import evo.derivation.config.{Config, ForField} +import evo.derivation.internal.{Matching, tupleFromProduct} +import evo.derivation.template.{ConsistentTemplate, HomogenicTemplate, SummonHierarchy, Template} +import izumi.logstage.api.rendering.{LogstageCodec, LogstageWriter} +import logstage.LogstageCodec + +import scala.deriving.Mirror +import scala.deriving.Mirror.SumOf + +// Unable to extend LogstageCodec here due to problems with derivations for contravariant type classes +trait EvoLog[A]: + def write(writer: LogstageWriter, value: A): Unit + +object EvoLog extends EvoLogTemplate: + def fromLogstage[A](using codec: => LogstageCodec[A]): EvoLog[A] = + (writer: LogstageWriter, value: A) => codec.write(writer, value) + + given [A](using LogstageCodec[A]): EvoLog[A] = fromLogstage +end EvoLog + +trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: + override def product[A](using mirror: Mirror.ProductOf[A])( + all: LazySummon.All[OfField, mirror.MirroredElemTypes], + )(using config: => Config[A], ev: A <:< Product): EvoLog[A] = + lazy val infos = config.top.fields.map(_._2) + + (writer, a) => + val fields = tupleFromProduct(a) + + writer.openMap() + all.useForeach[Unit, ForField[_]](fields, infos) { + [X] => + (info: ForField[_], a: X, codec: EvoLog[X]) => + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, info.name) + writer.mapElementSplitter() + codec.write(writer, a) + writer.nextMapElementClose() + } + writer.closeMap() + end product + + override def sum[A](using mirror: SumOf[A])( + subs: All[EvoLog, mirror.MirroredElemTypes], + mkSubMap: => Map[String, EvoLog[A]], + )(using config: => Config[A], matching: Matching[A]): EvoLog[A] = + lazy val cfg = config + lazy val codecs: Map[String, EvoLog[A]] = mkSubMap + + (writer, a) => + val constructor = matching.matched(a) + val discrimValue = cfg.name(constructor) + + (codecs.get(constructor), cfg.discriminator) match + case (Some(codec), _) => // TODO: support discriminator + writer.openMap() + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, discrimValue) + writer.mapElementSplitter() + codec.write(writer, a) + writer.nextMapElementClose() + writer.closeMap() + case (None, _) => () // throw exception ? + end match + end sum + + override def newtype[A](using nt: ValueClass[A])(using codec: EvoLog[nt.Representation]): EvoLog[A] = + (writer, a) => codec.write(writer, nt.to(a)) +end EvoLogTemplate diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/instances.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/instances.scala new file mode 100644 index 0000000..05530e9 --- /dev/null +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/instances.scala @@ -0,0 +1,9 @@ +package evo.derivation.logstage + +import izumi.logstage.api.rendering.{LogstageCodec, LogstageWriter} + +trait EvoLogInstances: + given [A](using e: EvoLog[A]): LogstageCodec[A] = + (writer: LogstageWriter, value: A) => e.write(writer, value) + +object instances extends EvoLogInstances diff --git a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala new file mode 100644 index 0000000..3c93125 --- /dev/null +++ b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala @@ -0,0 +1,105 @@ +package evo.derivation.logstage + +import evo.derivation.config.Config +import io.circe.Json +import logstage.LogstageCodec +import izumi.logstage.api.rendering.json.LogstageCirceWriter +import EvoLogTest.{WithProps, OneOf, SimpleRec} +import evo.derivation.{Discriminator, Embed, Rename, SnakeCase} +import izumi.logstage.api.rendering.{LogstageCodec, LogstageWriter} +import io.circe.parser.parse + +class LogstageTest extends munit.FunSuite: + import evo.derivation.logstage.instances.given // arggh + + extension [A](a: A) + def toLog(using codec: LogstageCodec[A]): Json = + LogstageCirceWriter.write(codec, a) + + test("simple recursive data") { + val chebKekLol = + """ + |{ + | "bazar" : { + | "foo" : { + | "bazar" : { + | "foo" : { + | "bazar" : { + | "foo" : { + | "no" : {} + | }, + | "param" : "cheb" + | } + | }, + | "param" : "kek" + | } + | }, + | "param" : "lol" + | } + |} + |""".stripMargin + + assertEquals( + parse(chebKekLol), + Right( + (SimpleRec.Bar(SimpleRec.Bar(SimpleRec.Bar(SimpleRec.No, "cheb"), "kek"), "lol"): SimpleRec).toLog, + ), + ) + } + + test("WithProps") { + val case1 = + """ + |{ + | "variant" : { + | "Case1" : { + | "foo" : 1, + | "param" : "cheb" + | } + | }, + | "props" : { + | "lol" : "kek", + | "sad" : "pet" + | } + |} + |""".stripMargin + + assertEquals( + parse(case1), + Right(WithProps(OneOf.Case1(1, "cheb"), Map("lol" -> "kek", "sad" -> "pet")).toLog), + ) + + val case2 = + """ + |{ + | "variant" : { + | "Case2" : { + | "foo" : 10 + | } + | }, + | "props" : {} + |} + |""".stripMargin + + assertEquals( + parse(case2), + Right(WithProps(OneOf.Case2(10), Map()).toLog), + ) + } + +end LogstageTest + +object EvoLogTest: + @SnakeCase + enum SimpleRec derives Config, EvoLog: + @Rename("bazar") case Bar(foo: SimpleRec, param: String) + case No + case Bazz(i: Int) + + enum OneOf derives Config, EvoLog: + case Case1(foo: Int, param: String) + case Case2(foo: Int) + case Case3(param: String) + + case class WithProps(variant: OneOf, props: Map[String, String]) derives Config, EvoLog +end EvoLogTest diff --git a/project/Version.scala b/project/Version.scala index ab6a01c..3e3dbf4 100644 --- a/project/Version.scala +++ b/project/Version.scala @@ -10,4 +10,6 @@ object Version { val playJson = "2.9.3" val cats = "2.9.0" + + val logstage = "1.1.0-M23" } From d47e2752383f6e28ebeb6ad43af05d08f3227bbb Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Wed, 31 May 2023 23:15:07 +0300 Subject: [PATCH 2/5] minor fix --- .../evo/derivation/logstage/EvoLog.scala | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala index 1955bbe..8afc0ff 100644 --- a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala @@ -31,6 +31,7 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: (writer, a) => val fields = tupleFromProduct(a) + // TODO: support embed writer.openMap() all.useForeach[Unit, ForField[_]](fields, infos) { [X] => @@ -55,17 +56,19 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: val constructor = matching.matched(a) val discrimValue = cfg.name(constructor) - (codecs.get(constructor), cfg.discriminator) match - case (Some(codec), _) => // TODO: support discriminator - writer.openMap() - writer.nextMapElementOpen() - LogstageCodec[String].write(writer, discrimValue) - writer.mapElementSplitter() - codec.write(writer, a) - writer.nextMapElementClose() - writer.closeMap() - case (None, _) => () // throw exception ? - end match + // TODO: support discriminator + codecs + .get(constructor) + .foreach( // throw exception if empty? + codec => + writer.openMap() + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, discrimValue) + writer.mapElementSplitter() + codec.write(writer, a) + writer.nextMapElementClose() + writer.closeMap(), + ) end sum override def newtype[A](using nt: ValueClass[A])(using codec: EvoLog[nt.Representation]): EvoLog[A] = From b9633ca7baede9b3f504467af81c74b9c1bcc4bc Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Thu, 1 Jun 2023 15:00:15 +0300 Subject: [PATCH 3/5] support Embed, Discriminator annotations --- .../evo/derivation/logstage/EvoLog.scala | 90 ++++++++++++------- .../derivation/logstage/LogstageTest.scala | 19 ++-- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala index 8afc0ff..b77ef88 100644 --- a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala @@ -14,10 +14,13 @@ import scala.deriving.Mirror.SumOf // Unable to extend LogstageCodec here due to problems with derivations for contravariant type classes trait EvoLog[A]: def write(writer: LogstageWriter, value: A): Unit + def writeInner(writer: LogstageWriter, value: A): Unit object EvoLog extends EvoLogTemplate: def fromLogstage[A](using codec: => LogstageCodec[A]): EvoLog[A] = - (writer: LogstageWriter, value: A) => codec.write(writer, value) + new EvoLog[A]: + override def writeInner(writer: LogstageWriter, value: A): Unit = codec.write(writer, value) + override def write(writer: LogstageWriter, value: A): Unit = codec.write(writer, value) given [A](using LogstageCodec[A]): EvoLog[A] = fromLogstage end EvoLog @@ -28,21 +31,28 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: )(using config: => Config[A], ev: A <:< Product): EvoLog[A] = lazy val infos = config.top.fields.map(_._2) - (writer, a) => - val fields = tupleFromProduct(a) + new EvoLog[A]: + override def write(writer: LogstageWriter, value: A): Unit = + writer.openMap() + writeInner(writer, value) + writer.closeMap() - // TODO: support embed - writer.openMap() - all.useForeach[Unit, ForField[_]](fields, infos) { - [X] => - (info: ForField[_], a: X, codec: EvoLog[X]) => - writer.nextMapElementOpen() - LogstageCodec[String].write(writer, info.name) - writer.mapElementSplitter() - codec.write(writer, a) - writer.nextMapElementClose() - } - writer.closeMap() + override def writeInner(writer: LogstageWriter, value: A): Unit = + val fields = tupleFromProduct(value) + + all.useForeach[Unit, ForField[_]](fields, infos) { + [X] => + (info: ForField[_], a: X, codec: EvoLog[X]) => + if (info.embed) codec.writeInner(writer, a) + else + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, info.name) + writer.mapElementSplitter() + codec.write(writer, a) + writer.nextMapElementClose() + } + end writeInner + end new end product override def sum[A](using mirror: SumOf[A])( @@ -52,25 +62,41 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: lazy val cfg = config lazy val codecs: Map[String, EvoLog[A]] = mkSubMap - (writer, a) => - val constructor = matching.matched(a) - val discrimValue = cfg.name(constructor) + new EvoLog[A]: + override def write(writer: LogstageWriter, value: A): Unit = + writer.openMap() + writeInner(writer, value) + writer.closeMap() + + override def writeInner(writer: LogstageWriter, value: A): Unit = + val constructor = matching.matched(value) + val discrimValue = cfg.name(constructor) - // TODO: support discriminator - codecs - .get(constructor) - .foreach( // throw exception if empty? - codec => - writer.openMap() - writer.nextMapElementOpen() - LogstageCodec[String].write(writer, discrimValue) - writer.mapElementSplitter() - codec.write(writer, a) - writer.nextMapElementClose() - writer.closeMap(), - ) + (codecs.get(constructor), config.discriminator) match + case (Some(codec), Some(discr)) => + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, discr) + writer.mapElementSplitter() + LogstageCodec[String].write(writer, discrimValue) + writer.nextMapElementClose() + codec.writeInner(writer, value) + case (Some(codec), None) => + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, discrimValue) + writer.mapElementSplitter() + codec.write(writer, value) + writer.nextMapElementClose() + case (_, _) => () // throw exception + end match + end writeInner + end new end sum override def newtype[A](using nt: ValueClass[A])(using codec: EvoLog[nt.Representation]): EvoLog[A] = - (writer, a) => codec.write(writer, nt.to(a)) + new EvoLog[A]: + override def write(writer: LogstageWriter, value: A): Unit = + codec.write(writer, nt.to(value)) + + override def writeInner(writer: LogstageWriter, value: A): Unit = + codec.write(writer, nt.to(value)) end EvoLogTemplate diff --git a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala index 3c93125..3994567 100644 --- a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala +++ b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala @@ -51,12 +51,9 @@ class LogstageTest extends munit.FunSuite: val case1 = """ |{ - | "variant" : { - | "Case1" : { - | "foo" : 1, - | "param" : "cheb" - | } - | }, + | "type" : "Case1", + | "foo" : 1, + | "param" : "cheb", | "props" : { | "lol" : "kek", | "sad" : "pet" @@ -72,11 +69,8 @@ class LogstageTest extends munit.FunSuite: val case2 = """ |{ - | "variant" : { - | "Case2" : { - | "foo" : 10 - | } - | }, + | "type" : "Case2", + | "foo": 10, | "props" : {} |} |""".stripMargin @@ -96,10 +90,11 @@ object EvoLogTest: case No case Bazz(i: Int) + @Discriminator("type") enum OneOf derives Config, EvoLog: case Case1(foo: Int, param: String) case Case2(foo: Int) case Case3(param: String) - case class WithProps(variant: OneOf, props: Map[String, String]) derives Config, EvoLog + case class WithProps(@Embed variant: OneOf, props: Map[String, String]) derives Config, EvoLog end EvoLogTest From b0f91e646053cc06b00b31edfa8ab9663f24d9c7 Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Thu, 1 Jun 2023 16:44:51 +0300 Subject: [PATCH 4/5] fix behavior with custom codecs and @Embed or @Discriminator annotations --- .../evo/derivation/logstage/EvoLog.scala | 61 +++++++++------ .../derivation/logstage/LogstageTest.scala | 74 ++++++++++++++++++- 2 files changed, 109 insertions(+), 26 deletions(-) diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala index b77ef88..f903743 100644 --- a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala @@ -11,16 +11,21 @@ import logstage.LogstageCodec import scala.deriving.Mirror import scala.deriving.Mirror.SumOf -// Unable to extend LogstageCodec here due to problems with derivations for contravariant type classes +/** Unable to extend LogstageCodec here due to problems with derivations for contravariant type classes + * + * LogstageWriter api is too strict to support @Embed, @Discriminator annotations here without additional methods such + * as `writeInner` + */ trait EvoLog[A]: def write(writer: LogstageWriter, value: A): Unit - def writeInner(writer: LogstageWriter, value: A): Unit + def writeInner: Option[(LogstageWriter, A) => Unit] +end EvoLog object EvoLog extends EvoLogTemplate: def fromLogstage[A](using codec: => LogstageCodec[A]): EvoLog[A] = new EvoLog[A]: - override def writeInner(writer: LogstageWriter, value: A): Unit = codec.write(writer, value) - override def write(writer: LogstageWriter, value: A): Unit = codec.write(writer, value) + override def writeInner: Option[(LogstageWriter, A) => Unit] = None + override def write(writer: LogstageWriter, value: A): Unit = codec.write(writer, value) given [A](using LogstageCodec[A]): EvoLog[A] = fromLogstage end EvoLog @@ -34,24 +39,29 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: new EvoLog[A]: override def write(writer: LogstageWriter, value: A): Unit = writer.openMap() - writeInner(writer, value) + writeInnerImpl(writer, value) writer.closeMap() - override def writeInner(writer: LogstageWriter, value: A): Unit = + override val writeInner: Option[(LogstageWriter, A) => Unit] = + Some(writeInnerImpl) + + def writeInnerImpl(writer: LogstageWriter, value: A): Unit = val fields = tupleFromProduct(value) all.useForeach[Unit, ForField[_]](fields, infos) { [X] => (info: ForField[_], a: X, codec: EvoLog[X]) => - if (info.embed) codec.writeInner(writer, a) - else - writer.nextMapElementOpen() - LogstageCodec[String].write(writer, info.name) - writer.mapElementSplitter() - codec.write(writer, a) - writer.nextMapElementClose() + codec.writeInner + .filter(_ => info.embed) + .fold { + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, info.name) + writer.mapElementSplitter() + codec.write(writer, a) + writer.nextMapElementClose() + }(_(writer, a)) } - end writeInner + end writeInnerImpl end new end product @@ -65,30 +75,33 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: new EvoLog[A]: override def write(writer: LogstageWriter, value: A): Unit = writer.openMap() - writeInner(writer, value) + writeInnerImpl(writer, value) writer.closeMap() - override def writeInner(writer: LogstageWriter, value: A): Unit = + override val writeInner: Option[(LogstageWriter, A) => Unit] = + Some(writeInnerImpl) + + def writeInnerImpl(writer: LogstageWriter, value: A): Unit = val constructor = matching.matched(value) val discrimValue = cfg.name(constructor) - (codecs.get(constructor), config.discriminator) match - case (Some(codec), Some(discr)) => + (codecs.get(constructor).map(c => (c, c.writeInner)), config.discriminator) match + case (Some((_, Some(writeInn))), Some(discr)) => writer.nextMapElementOpen() LogstageCodec[String].write(writer, discr) writer.mapElementSplitter() LogstageCodec[String].write(writer, discrimValue) writer.nextMapElementClose() - codec.writeInner(writer, value) - case (Some(codec), None) => + writeInn(writer, value) + case (Some(codec, _), _) => writer.nextMapElementOpen() LogstageCodec[String].write(writer, discrimValue) writer.mapElementSplitter() codec.write(writer, value) writer.nextMapElementClose() - case (_, _) => () // throw exception + case (_, _) => () // throw exception ? end match - end writeInner + end writeInnerImpl end new end sum @@ -97,6 +110,6 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: override def write(writer: LogstageWriter, value: A): Unit = codec.write(writer, nt.to(value)) - override def writeInner(writer: LogstageWriter, value: A): Unit = - codec.write(writer, nt.to(value)) + override val writeInner: Option[(LogstageWriter, A) => Unit] = + codec.writeInner.map(impl => (writer: LogstageWriter, value: A) => impl(writer, nt.to(value))) end EvoLogTemplate diff --git a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala index 3994567..876b27a 100644 --- a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala +++ b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala @@ -1,10 +1,11 @@ package evo.derivation.logstage import evo.derivation.config.Config -import io.circe.Json +import io.circe.{Json, Encoder} import logstage.LogstageCodec +import logstage.circe.LogstageCirceCodec import izumi.logstage.api.rendering.json.LogstageCirceWriter -import EvoLogTest.{WithProps, OneOf, SimpleRec} +import EvoLogTest.{WithProps, OneOf, SimpleRec, Custom, OneOfCustom} import evo.derivation.{Discriminator, Embed, Rename, SnakeCase} import izumi.logstage.api.rendering.{LogstageCodec, LogstageWriter} import io.circe.parser.parse @@ -81,6 +82,57 @@ class LogstageTest extends munit.FunSuite: ) } + test("simple recursive data") { + val chebKekLol = + """ + |{ + | "bazar" : { + | "foo" : { + | "bazar" : { + | "foo" : { + | "bazar" : { + | "foo" : { + | "no" : {} + | }, + | "param" : "cheb" + | } + | }, + | "param" : "kek" + | } + | }, + | "param" : "lol" + | } + |} + |""".stripMargin + + assertEquals( + parse(chebKekLol), + Right( + (SimpleRec.Bar(SimpleRec.Bar(SimpleRec.Bar(SimpleRec.No, "cheb"), "kek"), "lol"): SimpleRec).toLog, + ), + ) + } + + test("with custom instance") { + val customJson = + """ + |{ + | "variant" : { + | "foo" : 100 + | }, + | "props" : { + | "lol" : "zoo" + | } + |} + |""".stripMargin + + assertEquals( + parse(customJson), + Right( + Custom(OneOfCustom.Case2(100), Map("lol" -> "zoo")).toLog, + ), + ) + } end LogstageTest object EvoLogTest: @@ -97,4 +149,22 @@ object EvoLogTest: case Case3(param: String) case class WithProps(@Embed variant: OneOf, props: Map[String, String]) derives Config, EvoLog + + enum OneOfCustom: + case Case1(foo: Int, param: String) + case Case2(foo: Int) + case Case3(param: String) + + object OneOfCustom: + given Encoder[OneOfCustom] = Encoder.instance { + case OneOfCustom.Case1(foo, _) => Json.obj("foo" -> Json.fromInt(foo)) + case OneOfCustom.Case2(foo) => Json.obj("foo" -> Json.fromInt(foo)) + case OneOfCustom.Case3(_) => Json.obj() + } + + given LogstageCodec[OneOfCustom] = LogstageCirceCodec.derived + end OneOfCustom + + // Embed is ignored in this case + case class Custom(@Embed variant: OneOfCustom, props: Map[String, String]) derives Config, EvoLog end EvoLogTest From d8116ba47da4554d942a4ee030361d5524162ccf Mon Sep 17 00:00:00 2001 From: Aleksey Troitskiy Date: Thu, 8 Jun 2023 18:13:08 +0300 Subject: [PATCH 5/5] Masked annotation --- .../evo/derivation/logstage/EvoLog.scala | 32 +++++++++++++------ .../evo/derivation/logstage/Masked.scala | 5 +++ .../derivation/logstage/LogstageTest.scala | 10 +++--- 3 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 modules/logstage/src/main/scala/evo/derivation/logstage/Masked.scala diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala index f903743..512ced9 100644 --- a/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala @@ -51,15 +51,29 @@ trait EvoLogTemplate extends HomogenicTemplate[EvoLog], SummonHierarchy: all.useForeach[Unit, ForField[_]](fields, infos) { [X] => (info: ForField[_], a: X, codec: EvoLog[X]) => - codec.writeInner - .filter(_ => info.embed) - .fold { - writer.nextMapElementOpen() - LogstageCodec[String].write(writer, info.name) - writer.mapElementSplitter() - codec.write(writer, a) - writer.nextMapElementClose() - }(_(writer, a)) + val maskOpt = info.annotations.collectFirst { case Masked(mask) => mask } + val writeField = (writer: LogstageWriter, a: X) => + maskOpt.fold(codec.write(writer, a))(mask => + LogstageCodec[String].write(writer, mask) + ) + + val writeFieldInner = + codec.writeInner.map(f => + (writer: LogstageWriter, a: X) => + maskOpt.fold(f(writer, a))(mask => + LogstageCodec[String].write(writer, info.name) + writer.mapElementSplitter() + LogstageCodec[String].write(writer, mask) + ) + ) + + writeFieldInner.filter(_ => info.embed).fold { + writer.nextMapElementOpen() + LogstageCodec[String].write(writer, info.name) + writer.mapElementSplitter() + writeField(writer, a) + writer.nextMapElementClose() + }(_(writer, a)) } end writeInnerImpl end new diff --git a/modules/logstage/src/main/scala/evo/derivation/logstage/Masked.scala b/modules/logstage/src/main/scala/evo/derivation/logstage/Masked.scala new file mode 100644 index 0000000..1f3725c --- /dev/null +++ b/modules/logstage/src/main/scala/evo/derivation/logstage/Masked.scala @@ -0,0 +1,5 @@ +package evo.derivation.logstage + +import evo.derivation.Custom + +case class Masked(template: String = "***masked***") extends Custom \ No newline at end of file diff --git a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala index 876b27a..9d6d4cf 100644 --- a/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala +++ b/modules/logstage/src/test/scala/evo/derivation/logstage/LogstageTest.scala @@ -71,7 +71,7 @@ class LogstageTest extends munit.FunSuite: """ |{ | "type" : "Case2", - | "foo": 10, + | "foo": "***masked***", | "props" : {} |} |""".stripMargin @@ -120,9 +120,7 @@ class LogstageTest extends munit.FunSuite: | "variant" : { | "foo" : 100 | }, - | "props" : { - | "lol" : "zoo" - | } + | "props" : "***masked***" |} |""".stripMargin @@ -145,7 +143,7 @@ object EvoLogTest: @Discriminator("type") enum OneOf derives Config, EvoLog: case Case1(foo: Int, param: String) - case Case2(foo: Int) + case Case2(@Masked foo: Int) case Case3(param: String) case class WithProps(@Embed variant: OneOf, props: Map[String, String]) derives Config, EvoLog @@ -166,5 +164,5 @@ object EvoLogTest: end OneOfCustom // Embed is ignored in this case - case class Custom(@Embed variant: OneOfCustom, props: Map[String, String]) derives Config, EvoLog + case class Custom(@Embed variant: OneOfCustom, @Masked props: Map[String, String]) derives Config, EvoLog end EvoLogTest