Skip to content
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
2 changes: 1 addition & 1 deletion project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
63 changes: 34 additions & 29 deletions src/main/scala/pl/msitko/dhallj/generic/GenericDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 =
"""
Expand Down Expand Up @@ -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) {
Expand Down