diff --git a/build.sbt b/build.sbt index bdab851..0a62155 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,5 @@ -import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} +import com.typesafe.tools.mima.core.{IncompatibleSignatureProblem, ProblemFilters} +import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} ThisBuild / organization := "org.julienrf" @@ -10,6 +11,11 @@ ThisBuild / versionPolicyIntention := Compatibility.BinaryAndSourceCompatible // Temporary, because version 10.0.0 was invalid ThisBuild / versionPolicyPreviousVersions := Seq("10.0.1") +ThisBuild / mimaBinaryIssueFilters ++= Seq( + // package private method + ProblemFilters.exclude[IncompatibleSignatureProblem]("julienrf.json.derived.DerivedOWritesUtil.makeCoProductOWrites") +) + ThisBuild / developers := List( Developer( "julienrf", diff --git a/library/src/main/scala/julienrf/json/derived/DerivedOWrites.scala b/library/src/main/scala/julienrf/json/derived/DerivedOWrites.scala index b83a221..2e2e6c0 100644 --- a/library/src/main/scala/julienrf/json/derived/DerivedOWrites.scala +++ b/library/src/main/scala/julienrf/json/derived/DerivedOWrites.scala @@ -32,7 +32,7 @@ trait DerivedOWritesInstances extends DerivedOWritesInstances1 { typeTag: TT[FieldType[K, L]] ): DerivedOWrites[FieldType[K, L] :+: R, TT] = DerivedOWritesUtil.makeCoProductOWrites( - (_, _) => writesL.value, + (_, _) => writesL, owritesR, typeTag) } @@ -77,7 +77,7 @@ trait DerivedOWritesInstances1 extends DerivedOWritesInstances2 { typeTag: TT[FieldType[K, L]] ): DerivedOWrites[FieldType[K, L] :+: R, TT] = DerivedOWritesUtil.makeCoProductOWrites( - owritesL.value.owrites, + (tagOwrites, adapter) => owritesL.map(_.owrites(tagOwrites, adapter)), owritesR, typeTag) } @@ -116,7 +116,7 @@ trait DerivedOWritesInstances3 { private[derived] object DerivedOWritesUtil { def makeCoProductOWrites[K <: Symbol, L, R <: Coproduct, TT[A] <: TypeTag[A]]( - makeWritesL: (TypeTagOWrites, NameAdapter) => Writes[L], + makeWritesL: (TypeTagOWrites, NameAdapter) => Lazy[Writes[L]], owritesR: Lazy[DerivedOWrites[R, TT]], typeTag: TT[FieldType[K, L]] ): DerivedOWrites[FieldType[K, L] :+: R, TT] = @@ -127,7 +127,7 @@ private[derived] object DerivedOWritesUtil { val derivedOwriteR = owritesR.value.owrites(tagOwrites, adapter) OWrites[FieldType[K, L] :+: R] { - case Inl(l) => tagOwrites.owrites(typeTag.value, writesL).writes(l) + case Inl(l) => tagOwrites.owrites(typeTag.value, writesL.value).writes(l) case Inr(r) => derivedOwriteR.writes(r) } } diff --git a/library/src/main/scala/julienrf/json/derived/typetags.scala b/library/src/main/scala/julienrf/json/derived/typetags.scala index 3a6c17e..68b6b9e 100644 --- a/library/src/main/scala/julienrf/json/derived/typetags.scala +++ b/library/src/main/scala/julienrf/json/derived/typetags.scala @@ -139,7 +139,6 @@ object TypeTagReads { * * @param tagReads A way to decode the type tag value. */ - //TODO docs on edge case def flat(tagReads: Reads[String]): TypeTagReads = new TypeTagReads { def reads[A](typeName: String, reads: Reads[A]): Reads[A] = { diff --git a/library/src/test/scala/julienrf/json/derived/DerivedOFormatSuite.scala b/library/src/test/scala/julienrf/json/derived/DerivedOFormatSuite.scala index 6b90b8f..1abc12d 100644 --- a/library/src/test/scala/julienrf/json/derived/DerivedOFormatSuite.scala +++ b/library/src/test/scala/julienrf/json/derived/DerivedOFormatSuite.scala @@ -269,5 +269,61 @@ class DerivedOFormatSuite extends AnyFeatureSpec with Checkers { assert(json == Json.obj("type" -> "Bar", "__syntheticWrap__" -> JsNumber(42))) assert(fooFormat.reads(json).asEither == Right(foo)) } + + import TestHelpers._ + val adt = Z(X(1), Y("VVV")) + + Scenario("supports user-defined recursive formats - nested") { + val adtFormat: Format[ADTBase] = { + implicit val f1: Format[X] = Json.format + implicit val f2: Format[Y] = Json.format + implicit lazy val f3: Format[Z] = Json.format + + implicit lazy val f4: Format[ADTBase] = oformat[ADTBase]() + + f4 + } + + val json = adtFormat.writes(adt) + val obj = Json.obj( + "Z" -> Json.obj( + "l" -> Json.obj("X" -> Json.obj("a" -> 1)), + "r" -> Json.obj("Y" -> Json.obj("b" -> "VVV")))) + + assert(json == obj) + assert(adtFormat.reads(json).asEither == Right(adt)) + } + + Scenario("supports user-defined recursive formats - flat") { + val adtFormat: Format[ADTBase] = { + implicit val f1: Format[X] = Json.format + implicit val f2: Format[Y] = Json.format + implicit lazy val f3: Format[Z] = Json.format + + implicit lazy val f4: Format[ADTBase] = flat.oformat((__ \ "type").format[String]) + + f4 + } + + val json = adtFormat.writes(adt) + val obj = Json.obj( + "type" -> "Z", + "l" -> Json.obj("type" -> "X", "a" -> 1), + "r" -> Json.obj("type" -> "Y", "b" -> "VVV")) + + assert(json == obj) + assert(adtFormat.reads(json).asEither == Right(adt)) + } } } + + +object TestHelpers { + // Placing it here in a separate object since otherwise the Json.format macro fails to compile + // for these types + sealed trait ADTBase + + case class X(a: Int) extends ADTBase + case class Y(b: String) extends ADTBase + case class Z(l: ADTBase, r: ADTBase) extends ADTBase +}