Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

logstage derivation module #85

Open
wants to merge 5 commits into
base: base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
10 changes: 10 additions & 0 deletions modules/core/src/main/scala/evo/derivation/LazySummon.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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] =
Expand Down
129 changes: 129 additions & 0 deletions modules/logstage/src/main/scala/evo/derivation/logstage/EvoLog.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
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
*
* 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: 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: 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

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)

new EvoLog[A]:
override def write(writer: LogstageWriter, value: A): Unit =
writer.openMap()
writeInnerImpl(writer, value)
writer.closeMap()

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]) =>
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
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

new EvoLog[A]:
override def write(writer: LogstageWriter, value: A): Unit =
writer.openMap()
writeInnerImpl(writer, value)
writer.closeMap()

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).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()
writeInn(writer, value)
case (Some(codec, _), _) =>
writer.nextMapElementOpen()
LogstageCodec[String].write(writer, discrimValue)
writer.mapElementSplitter()
codec.write(writer, value)
writer.nextMapElementClose()
case (_, _) => () // throw exception ?
end match
end writeInnerImpl
end new
end sum

override def newtype[A](using nt: ValueClass[A])(using codec: EvoLog[nt.Representation]): EvoLog[A] =
new EvoLog[A]:
override def write(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
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package evo.derivation.logstage

import evo.derivation.Custom

case class Masked(template: String = "***masked***") extends Custom
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package evo.derivation.logstage

import evo.derivation.config.Config
import io.circe.{Json, Encoder}
import logstage.LogstageCodec
import logstage.circe.LogstageCirceCodec
import izumi.logstage.api.rendering.json.LogstageCirceWriter
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

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 =
"""
|{
| "type" : "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 =
"""
|{
| "type" : "Case2",
| "foo": "***masked***",
| "props" : {}
|}
|""".stripMargin

assertEquals(
parse(case2),
Right(WithProps(OneOf.Case2(10), Map()).toLog),
)
}

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" : "***masked***"
|}
|""".stripMargin

assertEquals(
parse(customJson),
Right(
Custom(OneOfCustom.Case2(100), Map("lol" -> "zoo")).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)

@Discriminator("type")
enum OneOf derives Config, EvoLog:
case Case1(foo: Int, param: String)
case Case2(@Masked foo: Int)
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, @Masked props: Map[String, String]) derives Config, EvoLog
end EvoLogTest
2 changes: 2 additions & 0 deletions project/Version.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ object Version {
val playJson = "2.9.3"

val cats = "2.9.0"

val logstage = "1.1.0-M23"
}