Skip to content

Commit

Permalink
Merge pull request #449 from fthomas/topic/447-cats
Browse files Browse the repository at this point in the history
Port of #447 for Cats
  • Loading branch information
fthomas authored Apr 7, 2018
2 parents e02e696 + 96b740d commit d11777b
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package eu.timepit.refined

import _root_.cats.Contravariant
import _root_.cats.MonadError
import _root_.cats.Show
import _root_.cats.implicits._
import _root_.cats.instances.eq._
import _root_.cats.instances.order._
import _root_.cats.kernel.{Eq, Order}
import eu.timepit.refined.api.RefType
import eu.timepit.refined.api.{RefType, Validate}

package object cats {

Expand All @@ -12,19 +15,45 @@ package object cats {
* instance of the base type.
*/
implicit def refTypeEq[F[_, _], T: Eq, P](implicit rt: RefType[F]): Eq[F[T, P]] =
Eq[T].contramap(rt.unwrap)
refTypeViaContravariant[F, Eq, T, P]

/**
* `Order` instance for refined types that delegates to the `Order`
* instance of the base type.
*/
implicit def refTypeOrder[F[_, _], T: Order, P](implicit rt: RefType[F]): Order[F[T, P]] =
Order[T].contramap(rt.unwrap)
refTypeViaContravariant[F, Order, T, P]

/**
* `Show` instance for refined types that delegates to the `Show`
* instance of the base type.
*/
implicit def refTypeShow[F[_, _], T: Show, P](implicit rt: RefType[F]): Show[F[T, P]] =
Show[T].contramap(rt.unwrap)
refTypeViaContravariant[F, Show, T, P]

/**
* `G` instance for refined types derived via `Contravariant[G]`
* that delegates to the `G` instance of the base type.
*
* Typical examples for `G` are encoders.
*/
implicit def refTypeViaContravariant[F[_, _], G[_], T, P](
implicit c: Contravariant[G],
rt: RefType[F],
gt: G[T]
): G[F[T, P]] = c.contramap(gt)(rt.unwrap)

/**
* `G` instance for refined types derived via `MonadError[G, String]`
* that is based on the `G` instance of the base type.
*
* Typical examples for `G` are decoders.
*/
implicit def refTypeViaMonadError[F[_, _], G[_], T, P](
implicit m: MonadError[G, String],
rt: RefType[F],
v: Validate[T, P],
gt: G[T]
): G[F[T, P]] =
m.flatMap(gt)(t => m.fromEither(rt.refine[P](t)))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package eu.timepit.refined.cats

import _root_.cats.MonadError
import eu.timepit.refined.types.numeric.PosInt
import org.scalacheck.Prop._
import org.scalacheck.Properties
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}

trait Decoder[A] {
def decode(s: String): Either[String, A]
}

object Decoder {
def apply[A](implicit d: Decoder[A]): Decoder[A] = d

def instance[A](f: String => Either[String, A]): Decoder[A] =
new Decoder[A] {
override def decode(s: String): Either[String, A] = f(s)
}

implicit val decoderMonadError: MonadError[Decoder, String] =
new MonadError[Decoder, String] {
override def flatMap[A, B](fa: Decoder[A])(f: A => Decoder[B]): Decoder[B] =
instance { s =>
fa.decode(s) match {
case Right(a) => f(a).decode(s)
case Left(err) => Left(err)
}
}

override def tailRecM[A, B](a: A)(f: A => Decoder[Either[A, B]]): Decoder[B] = {
@tailrec
def step(s: String, a1: A): Either[String, B] =
f(a1).decode(s) match {
case Right(Right(b)) => Right(b)
case Right(Left(a2)) => step(s, a2)
case Left(err) => Left(err)
}

instance(s => step(s, a))
}

override def raiseError[A](e: String): Decoder[A] =
instance(_ => Left(e))

override def handleErrorWith[A](fa: Decoder[A])(f: String => Decoder[A]): Decoder[A] =
instance { s =>
fa.decode(s) match {
case Right(a) => Right(a)
case Left(err) => f(err).decode(s)
}
}

override def pure[A](x: A): Decoder[A] =
instance(_ => Right(x))
}

implicit val intDecoder: Decoder[Int] =
instance(s =>
Try(s.toInt) match {
case Success(i) => Right(i)
case Failure(t) => Left(t.getMessage)
})
}

class RefTypeMonadErrorSpec extends Properties("MonadError") {

property("Deocder[Int]") = secure {
Decoder[Int].decode("1") ?= Right(1)
}

property("derive Decoder[PosInt] via MonadError[Decoder, String]") = {
// This import is needed because of https://github.com/scala/bug/issues/10753
import Decoder.decoderMonadError
val decoder = Decoder[PosInt]
(decoder.decode("1") ?= Right(PosInt.unsafeFrom(1))) &&
(decoder.decode("-1") ?= Left("Predicate failed: (-1 > 0)."))
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package eu.timepit.refined.cats

import cats.data.Validated
import cats.implicits._
import eu.timepit.refined.W
import eu.timepit.refined.api.{Refined, RefinedTypeOps}
import eu.timepit.refined.numeric.Interval
import _root_.cats.implicits._
import eu.timepit.refined.types.numeric.PosInt
import org.scalacheck.Prop._
import org.scalacheck.Properties
Expand All @@ -27,20 +23,4 @@ class CatsSpec extends Properties("cats") {
property("Show") = secure {
PosInt.unsafeFrom(5).show ?= "5"
}

property("Validate when Valid") = secure {
import syntax._
PosInt.validate(5) ?= Validated.Valid(PosInt.unsafeFrom(5))
}

property("Validate when Invalid") = secure {
import syntax._
PosInt.validate(-1) ?= Validated.invalidNel("Predicate failed: (-1 > 0).")
}

property("validate without import") = secure {
type OneToTen = Int Refined Interval.Closed[W.`1`.T, W.`10`.T]
object OneToTen extends RefinedTypeOps[OneToTen, Int] with CatsRefinedTypeOpsSyntax
OneToTen.validate(5) ?= Validated.valid(OneToTen.unsafeFrom(5))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package eu.timepit.refined.cats

import _root_.cats.Contravariant
import eu.timepit.refined.types.numeric.PosInt
import org.scalacheck.Prop._
import org.scalacheck.Properties

trait Encoder[A] {
def encode(a: A): String
}

object Encoder {
def apply[A](implicit e: Encoder[A]): Encoder[A] = e

def instance[A](f: A => String): Encoder[A] = new Encoder[A] {
override def encode(a: A): String = f(a)
}

implicit val encoderContravariant: Contravariant[Encoder] =
new Contravariant[Encoder] {
override def contramap[A, B](fa: Encoder[A])(f: B => A): Encoder[B] =
instance(b => fa.encode(f(b)))
}

implicit val intEncoder: Encoder[Int] =
instance(_.toString)
}

class RefTypeContravariantSpec extends Properties("Contravariant") {

property("Encoder[Int]") = secure {
Encoder[Int].encode(1) ?= "1"
}

property("derive Encoder[PosInt] via Contravariant[Encoder]") = secure {
// This import is needed because of https://github.com/scala/bug/issues/10753
import Encoder.encoderContravariant
Encoder[PosInt].encode(PosInt.unsafeFrom(1)) ?= "1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package eu.timepit.refined.cats

import _root_.cats.data.Validated
import eu.timepit.refined.W
import eu.timepit.refined.api.{Refined, RefinedTypeOps}
import eu.timepit.refined.numeric.Interval
import eu.timepit.refined.types.numeric.PosInt
import org.scalacheck.Prop._
import org.scalacheck.Properties

class SyntaxSpec extends Properties("syntax") {

property("Validate when Valid") = secure {
import syntax._
PosInt.validate(5) ?= Validated.Valid(PosInt.unsafeFrom(5))
}

property("Validate when Invalid") = secure {
import syntax._
PosInt.validate(-1) ?= Validated.invalidNel("Predicate failed: (-1 > 0).")
}

property("validate without import") = secure {
type OneToTen = Int Refined Interval.Closed[W.`1`.T, W.`10`.T]
object OneToTen extends RefinedTypeOps[OneToTen, Int] with CatsRefinedTypeOpsSyntax
OneToTen.validate(5) ?= Validated.valid(OneToTen.unsafeFrom(5))
}
}

0 comments on commit d11777b

Please sign in to comment.