-
Notifications
You must be signed in to change notification settings - Fork 158
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #449 from fthomas/topic/447-cats
Port of #447 for Cats
- Loading branch information
Showing
5 changed files
with
183 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
modules/cats/shared/src/test/scala-2.12/eu/timepit/refined/cats/RefTypeMonadErrorSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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).")) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
modules/cats/shared/src/test/scala/eu/timepit/refined/cats/RefTypeContravariantSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
modules/cats/shared/src/test/scala/eu/timepit/refined/cats/SyntaxSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} |