Skip to content

Commit

Permalink
Simplify laws
Browse files Browse the repository at this point in the history
  • Loading branch information
nrinaudo committed May 10, 2020
1 parent 99e7c54 commit 7ab4a3d
Showing 1 changed file with 28 additions and 10 deletions.
38 changes: 28 additions & 10 deletions laws/shared/src/main/scala/kantan/codecs/laws/DecoderLaws.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,65 @@ package kantan.codecs.laws

import kantan.codecs.Decoder
import kantan.codecs.laws.CodecValue.{IllegalValue, LegalValue}
import org.scalacheck.Prop

/** Laws that a [[Decoder]] must abide by to be considered lawful.
*
* These are executed through [[kantan.codecs.discipline.DecoderTests]].
*/
trait DecoderLaws[Encoded, Decoded, Failure, Tag] {
def decoder: Decoder[Encoded, Decoded, Failure, Tag]

private def cmp(result: Either[Failure, Decoded], cv: CodecValue[Encoded, Decoded, Tag]): Boolean =
(cv, result) match {
case (IllegalValue(_), Left(_)) => true
case (LegalValue(_, d), Right(d2)) => d == d2
case _ => false
}
/** Decoder whose lawfulness is being checked. */
def decoder: Decoder[Encoded, Decoded, Failure, Tag]

// - Simple laws -----------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------
def decode(v: CodecValue[Encoded, Decoded, Tag]): Boolean = cmp(decoder.decode(v.encoded), v)
// These two laws are the foundation on which the others are built: if we can trust that `decode` is well behaved,
// then relying on it in all other laws is sane.

/** Decoding a legal value must yield the expected result. */
def decode(v: LegalValue[Encoded, Decoded, Tag]): Boolean = decoder.decode(v.encoded) == Right(v.decoded)

/** Decoding an illegal value must fail. */
def decodeFailure(v: IllegalValue[Encoded, Decoded, Tag]): Boolean =
Prop.throws(classOf[Exception])(decoder.unsafeDecode(v.encoded))
decoder.decode(v.encoded).isLeft

// - Functor laws ----------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------
/** Mapping on `identity` should not change the decoder's behaviour. */
def mapIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean =
decoder.decode(v.encoded) == decoder.map(identity).decode(v.encoded)

/** Mapping on the composition of two functions must be the same as mapping on the first, then the second. */
def mapComposition[A, B](v: CodecValue[Encoded, Decoded, Tag], f: Decoded => A, g: A => B): Boolean =
decoder.map(f andThen g).decode(v.encoded) == decoder.map(f).map(g).decode(v.encoded)

// - Bifunctor laws --------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------
/** Same as [[mapIdentity]], but for `leftMap`. */
def leftMapIdentity[A](v: CodecValue[Encoded, Decoded, Tag]): Boolean =
decoder.decode(v.encoded) == decoder.leftMap(identity).decode(v.encoded)

/** Same as [[mapComposition]], but for `leftMap`. */
def leftMapComposition[A, B](v: CodecValue[Encoded, Decoded, Tag], f: Failure => A, g: A => B): Boolean =
decoder.leftMap(f andThen g).decode(v.encoded) == decoder.leftMap(f).leftMap(g).decode(v.encoded)

// - Contravariant laws ----------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------
/** Same as [[mapIdentity]], but for `contramapEncoded`. */
def contramapEncodedIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean =
decoder.decode(v.encoded) == decoder.contramapEncoded(identity[Encoded]).decode(v.encoded)

/** Same as [[mapComposition]], but for `contramapEncoded`. */
def contramapEncodedComposition[A, B](b: B, f: A => Encoded, g: B => A): Boolean =
decoder.contramapEncoded(g andThen f).decode(b) == decoder.contramapEncoded(f).contramapEncoded(g).decode(b)

// - "Kleisli" laws --------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------------------------
/** Same as [[mapIdentity]], but for `emap`. */
def emapIdentity(v: CodecValue[Encoded, Decoded, Tag]): Boolean =
decoder.decode(v.encoded) == decoder.emap(Right.apply).decode(v.encoded)

/** Same as [[mapComposition]], but for `emap`. */
def emapComposition[A, B](
v: CodecValue[Encoded, Decoded, Tag],
f: Decoded => Either[Failure, A],
Expand All @@ -76,6 +89,11 @@ trait DecoderLaws[Encoded, Decoded, Failure, Tag] {
}

object DecoderLaws {

/** Creates an instance of [[DecoderLaws]] for any type that has a valid decoder instance.
*
* There rarely is a need to call this directly. Consider using [[kantan.codecs.discipline.DecoderTests]] instead.
*/
def apply[E, D, F, T](implicit de: Decoder[E, D, F, T]): DecoderLaws[E, D, F, T] =
new DecoderLaws[E, D, F, T] {
override val decoder = de
Expand Down

0 comments on commit 7ab4a3d

Please sign in to comment.