Skip to content

Commit

Permalink
Use errors: HCursor => List[String] as the way of signaling both th…
Browse files Browse the repository at this point in the history
…e predicate and the errors to be generated by the validation check.
  • Loading branch information
pfcoperez committed Dec 5, 2018
1 parent ff728ac commit ca44c79
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 11 deletions.
21 changes: 14 additions & 7 deletions modules/core/shared/src/main/scala/io/circe/Decoder.scala
Expand Up @@ -154,21 +154,26 @@ trait Decoder[A] extends Serializable { self =>
}

/**
* Build a new instance that fails if the condition does not hold for the
* input, producing a message from the cursor.
* Build a new instance that fails with one or more errors if the condition
* does not hold for the input.
*
* Note that this condition is checked before decoding with the current
* decoder, and if it does not hold, decoding does not continue. This means
* that if you chain calls to this method, errors will not be accumulated
* (instead only the error of the last failing `validate` in the chain will be
* returned).
*/
final def validate(pred: HCursor => Boolean)(message: HCursor => String): Decoder[A] = new Decoder[A] {
final def validate(errors: HCursor => List[String]): Decoder[A] = new Decoder[A] {
final def apply(c: HCursor): Decoder.Result[A] =
if (pred(c)) self(c) else Left(DecodingFailure(message(c), c.history))
errors(c).headOption.map { message =>
Left(DecodingFailure(message, c.history))
} getOrElse self(c)

override def decodeAccumulating(c: HCursor): AccumulatingDecoder.Result[A] =
if (pred(c)) self.decodeAccumulating(c) else Validated.invalidNel(DecodingFailure(message(c), c.history))
errors(c).map(DecodingFailure(_, c.history)) match {
case Nil => self.decodeAccumulating(c)
case h :: t => Validated.invalid(NonEmptyList(h, t))
}
}

/**
Expand All @@ -181,8 +186,10 @@ trait Decoder[A] extends Serializable { self =>
* (instead only the error of the last failing `validate` in the chain will be
* returned).
*/
final def validate(pred: HCursor => Boolean, message: => String): Decoder[A] =
validate(pred)(_ => message)
final def validate(pred: HCursor => Boolean, message: => String): Decoder[A] = validate { c =>
if (pred(c)) Nil
else message :: Nil
}

/**
* Convert to a Kleisli arrow.
Expand Down
17 changes: 13 additions & 4 deletions modules/tests/shared/src/test/scala/io/circe/DecoderSuite.scala
Expand Up @@ -404,20 +404,29 @@ class DecoderSuite extends CirceSuite with LargeNumberDecoderTests with TableDri
assert(validatingDecoder.decodeAccumulating(Json.True.hcursor).isInvalid)
}

it should "generate error messages from HCursor when a function is passed" in {
it should "provide the generated error messages from HCursor when a function is passed" in {
case class Foo(x: Int, y: String)

val decoder: Decoder[Foo] = Decoder.const(Foo(42, "meaning")).validate(_ => false) { c =>
val decoder: Decoder[Foo] = Decoder.const(Foo(42, "meaning")).validate { c =>
val maybeFieldsStr = for {
json <- c.focus
jsonKeys <- json.hcursor.keys
} yield jsonKeys.mkString(",")
maybeFieldsStr.getOrElse("")
maybeFieldsStr.getOrElse("") :: Nil
}

val Right(fooJson) = parse("""{"x":42, "y": "meaning"}""")

assert(decoder.decodeJson(fooJson).left.get.message == "x,y")
assert(decoder.decodeJson(fooJson).left.get.message === "x,y")
}

it should "not fail when the passed errors function returns an empty list" in {
val testValue = 42
val decoder = Decoder[Int].validate(_ => Nil)

val Right(intJson) = parse(testValue.toString)

assert(decoder.decodeJson(intJson) === Right(testValue))
}

"either" should "return the correct disjunct" in forAll { (value: Either[String, Boolean]) =>
Expand Down

0 comments on commit ca44c79

Please sign in to comment.