From f26a763cf384ec02418c489a4ba982a29a5f98b4 Mon Sep 17 00:00:00 2001 From: Michal Sitko Date: Fri, 5 Mar 2021 14:56:09 +0100 Subject: [PATCH] Accommodating to Decoder based on HCursor --- project/Dependencies.scala | 2 +- .../dhallj/generic/GenericDecoder.scala | 63 ++++++++++--------- .../decoder/AutoDeriveDecoderSpec.scala | 31 +++++---- .../decoder/SemiautoDeriveDecoderSpec.scala | 33 ++++++---- 4 files changed, 75 insertions(+), 54 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index c9b9727..d3b5f34 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -13,7 +13,7 @@ object Dependencies { ) def compileDeps(scalaVersion: String) = Seq( - "org.dhallj" %% "dhall-scala-codec" % Versions.Dhall + "org.dhallj" %% "dhall-scala-codec" % "0.8.1-local" ) ++ magnolia(scalaVersion) lazy val testDeps = Seq( diff --git a/src/main/scala/pl/msitko/dhallj/generic/GenericDecoder.scala b/src/main/scala/pl/msitko/dhallj/generic/GenericDecoder.scala index 723a397..783a8d1 100644 --- a/src/main/scala/pl/msitko/dhallj/generic/GenericDecoder.scala +++ b/src/main/scala/pl/msitko/dhallj/generic/GenericDecoder.scala @@ -7,43 +7,40 @@ import cats.instances.list._ import magnolia.{CaseClass, SealedTrait} import org.dhallj.ast._ import org.dhallj.codec.Decoder.Result -import org.dhallj.codec.{Decoder, DecodingFailure} +import org.dhallj.codec.{ACursor, Decoder, DecodingFailure, DownField, HCursor} import org.dhallj.core.Expr -final case class MissingRecordField(override val target: String, missingFieldName: String, override val value: Expr) - extends DecodingFailure(target, value) { - override def toString: String = s"Missing record field '$missingFieldName' when decoding $target" -} - private[generic] object GenericDecoder { private[generic] def combine[T](caseClass: CaseClass[Decoder, T]): Decoder[T] = new Decoder[T] { - private def decodeAs(expr: Expr, recordMap: Map[String, Expr]) = + private def decodeAs(c: HCursor): Either[DecodingFailure, T] = Traverse[List] .traverse(caseClass.parameters.toList) { param => - recordMap.get(param.label) match { - case Some(expr) => - param.typeclass.decode(expr) - case None => - param.default match { - case Some(default) => Right(default) - case None => - Left(MissingRecordField(caseClass.typeName.full, param.label, expr)) - } + if (c.downField(param.label).succeeded) { + param.typeclass.tryDecode(c.downField(param.label)) + } else { + param.default match { + case Some(default) => default.asRight + case None => + new DecodingFailure( + "Attempt to decode value on failed cursor", + Some(c.expr), + DownField(param.label) :: c.history).asLeft + } } } .map(ps => caseClass.rawConstruct(ps)) - override def decode(expr: Expr): Result[T] = expr match { - case RecordLiteral(recordMap) => - decodeAs(expr, recordMap) + override def decode(c: HCursor): Result[T] = c.expr match { + case RecordLiteral(_) => + decodeAs(c) case FieldAccess(UnionType(_), _) => - decodeAs(expr, Map.empty) + decodeAs(c) case other => - Left(new DecodingFailure(caseClass.typeName.full, other)) + DecodingFailure.failedTarget(caseClass.typeName.full, other, c.history).asLeft } override def isValidType(typeExpr: Expr): Boolean = typeExpr match { @@ -60,21 +57,29 @@ private[generic] object GenericDecoder { private[generic] def dispatch[T](sealedTrait: SealedTrait[Decoder, T]): Decoder[T] = new Decoder[T] { - private def decodeAs(expr: Expr, subtypeName: String) = + private def decodeAs(c: ACursor, subtypeName: String) = sealedTrait.subtypes.find(_.typeName.short == subtypeName) match { case Some(subtype) => - subtype.typeclass.decode(expr) + subtype.typeclass.tryDecode(c) case None => - new DecodingFailure(s"$subtypeName is not a known subtype of ${sealedTrait.typeName.full}", expr).asLeft + c.focus match { + case Some(expr) => + DecodingFailure + .failedTarget(s"$subtypeName is not a known subtype of ${sealedTrait.typeName.full}", expr, c.history) + .asLeft + case None => + new DecodingFailure("Attempt to decode value on failed cursor", None, c.history).asLeft + } + } - override def decode(expr: Expr): Result[T] = expr match { - case Application(FieldAccess(UnionType(_), t), arg) => - decodeAs(arg, t) + override def decode(c: HCursor): Result[T] = c.expr match { + case Application(FieldAccess(UnionType(_), t), _) => + decodeAs(c.unionAlternative(t), t) case FieldAccess(UnionType(_), t) => - decodeAs(expr, t) + decodeAs(c.unionAlternative(t), t) case _ => - new DecodingFailure("Is not a union", expr).asLeft + DecodingFailure.failedTarget("Is not a union", c.expr, c.history).asLeft } override def isValidType(typeExpr: Expr): Boolean = true diff --git a/src/test/scala/pl/msitko/dhallj/generic/decoder/AutoDeriveDecoderSpec.scala b/src/test/scala/pl/msitko/dhallj/generic/decoder/AutoDeriveDecoderSpec.scala index 3b0fbc7..eaa2606 100644 --- a/src/test/scala/pl/msitko/dhallj/generic/decoder/AutoDeriveDecoderSpec.scala +++ b/src/test/scala/pl/msitko/dhallj/generic/decoder/AutoDeriveDecoderSpec.scala @@ -1,6 +1,6 @@ package pl.msitko.dhallj.generic.decoder -import org.dhallj.codec.Decoder +import org.dhallj.codec.{Decoder, DownField} import org.dhallj.codec.syntax._ import org.dhallj.syntax._ import pl.msitko.dhallj.generic.Fixtures @@ -109,23 +109,26 @@ class AutoDeriveDecoderSpec extends munit.FunSuite with Fixtures { assertEquals(decoded, OnOrOff2.Off()) } - test("Decoding error should be comprehensible for deeply nested case classes".ignore) { - val input = - """ - |let OnOrOff = < On: {} | Off: {} > - |in { http = { server = { preview = { enableHttp = OnOrOff.Off } } } } - |""".stripMargin + test("Decoding error should be comprehensible for deeply nested case classes") { + val input = """{ a1.b1 = { c1 = "someString", c2 = "hey there" } }""" val parsed = input.parseExpr.getOr("Parsing failed").normalize() - val decoded = parsed.as[Akka] + val Left(decodingFailure) = parsed.as[A] - val expectedMsg = - "Missing field http.server.preview.[enableHttp2] when decoding org.dhallj.generic.example.akka.Preview" + assertEquals(decodingFailure.message, "Error decoding Int") + assertEquals(decodingFailure.history, List(DownField("c2"), DownField("b1"), DownField("a1"))) + } - val errorMsg = decoded.left.map(_.getMessage) + test("Failed cursor should have proper history") { + val input = """{ a1.b1 = { c1 = "someString" } }""" - assert(errorMsg.left.map(_.contains(expectedMsg)).left.getOrElse(false)) + val parsed = input.parseExpr.getOr("Parsing failed").normalize() + + val Left(decodingFailure) = parsed.as[A] + + assertEquals(decodingFailure.message, "Attempt to decode value on failed cursor") + assertEquals(decodingFailure.history, List(DownField("c2"), DownField("b1"), DownField("a1"))) } implicit class DecodeString(s: String) { @@ -138,3 +141,7 @@ class AutoDeriveDecoderSpec extends munit.FunSuite with Fixtures { def getOr(clue: String): R = v.fold(l => fail(s"Unexpected Left when $clue: $l"), r => r) } } + +final case class A(a1: B) +final case class B(b1: C) +final case class C(c1: String, c2: Int) diff --git a/src/test/scala/pl/msitko/dhallj/generic/decoder/SemiautoDeriveDecoderSpec.scala b/src/test/scala/pl/msitko/dhallj/generic/decoder/SemiautoDeriveDecoderSpec.scala index bf15215..468d190 100644 --- a/src/test/scala/pl/msitko/dhallj/generic/decoder/SemiautoDeriveDecoderSpec.scala +++ b/src/test/scala/pl/msitko/dhallj/generic/decoder/SemiautoDeriveDecoderSpec.scala @@ -1,6 +1,6 @@ package pl.msitko.dhallj.generic.decoder -import org.dhallj.codec.Decoder +import org.dhallj.codec.{Decoder, DownField} import org.dhallj.codec.syntax._ import org.dhallj.syntax._ import pl.msitko.dhallj.generic.Fixtures @@ -43,6 +43,12 @@ class SemiautoDeriveDecoderSpec extends munit.FunSuite with Fixtures { } implicit val offDec = deriveDecoder[OnOrOff2] + implicit val aDec = { + implicit val cDec = deriveDecoder[C] + implicit val bDec = deriveDecoder[B] + deriveDecoder[A] + } + test("Load nested case classes") { val decoded = """ @@ -178,23 +184,26 @@ class SemiautoDeriveDecoderSpec extends munit.FunSuite with Fixtures { assertEquals(decoded, OnOrOff2.Off()) } - test("Decoding error should be comprehensible for deeply nested case classes".ignore) { - val input = - """ - |let OnOrOff = < On: {} | Off: {} > - |in { http = { server = { preview = { enableHttp = OnOrOff.Off } } } } - |""".stripMargin + test("Decoding error should be comprehensible for deeply nested case classes") { + val input = """{ a1.b1 = { c1 = "someString", c2 = "hey there" } }""" val parsed = input.parseExpr.getOr("Parsing failed").normalize() - val decoded = parsed.as[Akka] + val Left(decodingFailure) = parsed.as[A] - val expectedMsg = - "Missing field http.server.preview.[enableHttp2] when decoding org.dhallj.generic.example.akka.Preview" + assertEquals(decodingFailure.message, "Error decoding Int") + assertEquals(decodingFailure.history, List(DownField("c2"), DownField("b1"), DownField("a1"))) + } + + test("Failed cursor should have proper history") { + val input = """{ a1.b1 = { c1 = "someString" } }""" + + val parsed = input.parseExpr.getOr("Parsing failed").normalize() - val errorMsg = decoded.left.map(_.getMessage) + val Left(decodingFailure) = parsed.as[A] - assert(errorMsg.left.map(_.contains(expectedMsg)).left.getOrElse(false)) + assertEquals(decodingFailure.message, "Attempt to decode value on failed cursor") + assertEquals(decodingFailure.history, List(DownField("c2"), DownField("b1"), DownField("a1"))) } implicit class DecodeString(s: String) {