diff --git a/core/src/main/scala/cats/data/Ior.scala b/core/src/main/scala/cats/data/Ior.scala index 8b79aa013a..d62a7e51b6 100644 --- a/core/src/main/scala/cats/data/Ior.scala +++ b/core/src/main/scala/cats/data/Ior.scala @@ -1,7 +1,9 @@ package cats package data +import cats.data.Validated.{Invalid, Valid} import cats.functor.Bifunctor + import scala.annotation.tailrec /** Represents a right-biased disjunction that is either an `A`, or a `B`, or both an `A` and a `B`. @@ -47,6 +49,7 @@ sealed abstract class Ior[+A, +B] extends Product with Serializable { final def unwrap: Either[Either[A, B], (A, B)] = fold(a => Left(Left(a)), b => Left(Right(b)), (a, b) => Right((a, b))) final def toEither: Either[A, B] = fold(Left(_), Right(_), (_, b) => Right(b)) + final def toValidated: Validated[A, B] = fold(Invalid(_), Valid(_), (_, b) => Valid(b)) final def toOption: Option[B] = right final def toList: List[B] = right.toList @@ -102,7 +105,7 @@ sealed abstract class Ior[+A, +B] extends Product with Serializable { fold(identity, ev, (_, b) => ev(b)) // scalastyle:off cyclomatic.complexity - final def append[AA >: A, BB >: B](that: AA Ior BB)(implicit AA: Semigroup[AA], BB: Semigroup[BB]): AA Ior BB = this match { + final def combine[AA >: A, BB >: B](that: AA Ior BB)(implicit AA: Semigroup[AA], BB: Semigroup[BB]): AA Ior BB = this match { case Ior.Left(a1) => that match { case Ior.Left(a2) => Ior.Left(AA.combine(a1, a2)) case Ior.Right(b2) => Ior.Both(a1, b2) @@ -150,7 +153,7 @@ private[data] sealed abstract class IorInstances extends IorInstances0 { } implicit def catsDataSemigroupForIor[A: Semigroup, B: Semigroup]: Semigroup[Ior[A, B]] = new Semigroup[Ior[A, B]] { - def combine(x: Ior[A, B], y: Ior[A, B]) = x.append(y) + def combine(x: Ior[A, B], y: Ior[A, B]) = x.combine(y) } implicit def catsDataMonadForIor[A: Semigroup]: Monad[A Ior ?] = new Monad[A Ior ?] { @@ -198,6 +201,8 @@ private[data] sealed trait IorFunctions { def left[A, B](a: A): A Ior B = Ior.Left(a) def right[A, B](b: B): A Ior B = Ior.Right(b) def both[A, B](a: A, b: B): A Ior B = Ior.Both(a, b) + def leftNel[A, B](a: A): IorNel[A, B] = left(NonEmptyList.of(a)) + def bothNel[A, B](a: A, b: B): IorNel[A, B] = both(NonEmptyList.of(a), b) /** * Create an `Ior` from two Options if at least one of them is defined. diff --git a/core/src/main/scala/cats/data/Validated.scala b/core/src/main/scala/cats/data/Validated.scala index 7d65e570ec..f8a4a97bcb 100644 --- a/core/src/main/scala/cats/data/Validated.scala +++ b/core/src/main/scala/cats/data/Validated.scala @@ -75,6 +75,11 @@ sealed abstract class Validated[+E, +A] extends Product with Serializable { */ def toOption: Option[A] = fold(_ => None, Some.apply) + /** + * Returns Valid values wrapped in Ior.Right, and None for Ior.Left values + */ + def toIor: Ior[E, A] = fold(Ior.left, Ior.right) + /** * Convert this value to a single element List if it is Valid, * otherwise return an empty List @@ -430,13 +435,18 @@ private[data] trait ValidatedFunctions { } /** - * Converts an `Either[A, B]` to an `Validated[A, B]`. + * Converts an `Either[A, B]` to a `Validated[A, B]`. */ def fromEither[A, B](e: Either[A, B]): Validated[A, B] = e.fold(invalid, valid) /** - * Converts an `Option[B]` to an `Validated[A, B]`, where the provided `ifNone` values is returned on + * Converts an `Option[B]` to a `Validated[A, B]`, where the provided `ifNone` values is returned on * the invalid of the `Validated` when the specified `Option` is `None`. */ def fromOption[A, B](o: Option[B], ifNone: => A): Validated[A, B] = o.fold(invalid[A, B](ifNone))(valid) + + /** + * Converts an `Ior[A, B]` to a `Validated[A, B]`. + */ + def fromIor[A, B](ior: Ior[A, B]): Validated[A, B] = ior.fold(invalid, valid, (_, b) => valid(b)) } diff --git a/core/src/main/scala/cats/data/package.scala b/core/src/main/scala/cats/data/package.scala index 396f22908d..e81751b06c 100644 --- a/core/src/main/scala/cats/data/package.scala +++ b/core/src/main/scala/cats/data/package.scala @@ -3,6 +3,7 @@ package cats package object data { type NonEmptyStream[A] = OneAnd[Stream, A] type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A] + type IorNel[+B, +A] = Ior[NonEmptyList[B], A] def NonEmptyStream[A](head: A, tail: Stream[A] = Stream.empty): NonEmptyStream[A] = OneAnd(head, tail) diff --git a/core/src/main/scala/cats/syntax/all.scala b/core/src/main/scala/cats/syntax/all.scala index 2377a19497..ec98e2a6e3 100644 --- a/core/src/main/scala/cats/syntax/all.scala +++ b/core/src/main/scala/cats/syntax/all.scala @@ -22,6 +22,7 @@ trait AllSyntax with FunctorFilterSyntax with GroupSyntax with InvariantSyntax + with IorSyntax with ListSyntax with MonadSyntax with MonadCombineSyntax diff --git a/core/src/main/scala/cats/syntax/ior.scala b/core/src/main/scala/cats/syntax/ior.scala new file mode 100644 index 0000000000..bc8bdfacba --- /dev/null +++ b/core/src/main/scala/cats/syntax/ior.scala @@ -0,0 +1,37 @@ +package cats.syntax + +import cats.data.Ior + +trait IorSyntax { + implicit def catsSyntaxIorId[A](a: A): IorIdOps[A] = new IorIdOps(a) +} + +final class IorIdOps[A](val a: A) extends AnyVal { + /** + * Wrap a value in `Ior.Right`. + * + * Example: + * {{{ + * scala> import cats.data.Ior + * scala> import cats.implicits._ + * + * scala> "hello".rightIor[String] + * res0: Ior[String, String] = Right(hello) + * }}} + */ + def rightIor[B]: Ior[B, A] = Ior.right(a) + + /** + * Wrap a value in `Ior.Left`. + * + * Example: + * {{{ + * scala> import cats.data.Ior + * scala> import cats.implicits._ + * + * scala> "error".leftIor[String] + * res0: Ior[String, String] = Left(error) + * }}} + */ + def leftIor[B]: Ior[A, B] = Ior.left(a) +} diff --git a/core/src/main/scala/cats/syntax/list.scala b/core/src/main/scala/cats/syntax/list.scala index 7c2a2c0aca..7079dbfad4 100644 --- a/core/src/main/scala/cats/syntax/list.scala +++ b/core/src/main/scala/cats/syntax/list.scala @@ -8,6 +8,24 @@ trait ListSyntax { } final class ListOps[A](val la: List[A]) extends AnyVal { + + /** + * Returns an Option of NonEmptyList from a List + * + * Example: + * {{{ + * scala> import cats.data.NonEmptyList + * scala> import cats.implicits._ + * + * scala> val result1: List[Int] = List(1, 2) + * scala> result1.toNel + * res0: Option[NonEmptyList[Int]] = Some(NonEmptyList(1, 2)) + * + * scala> val result2: List[Int] = List.empty[Int] + * scala> result2.toNel + * res1: Option[NonEmptyList[Int]] = None + * }}} + */ def toNel: Option[NonEmptyList[A]] = NonEmptyList.fromList(la) def groupByNel[B](f: A => B): Map[B, NonEmptyList[A]] = toNel.fold(Map.empty[B, NonEmptyList[A]])(_.groupBy(f)) diff --git a/core/src/main/scala/cats/syntax/option.scala b/core/src/main/scala/cats/syntax/option.scala index a824768611..25dbbffb07 100644 --- a/core/src/main/scala/cats/syntax/option.scala +++ b/core/src/main/scala/cats/syntax/option.scala @@ -1,7 +1,7 @@ package cats package syntax -import cats.data.{Validated, ValidatedNel} +import cats.data.{Ior, Validated, ValidatedNel} trait OptionSyntax { final def none[A]: Option[A] = Option.empty[A] @@ -112,6 +112,46 @@ final class OptionOps[A](val oa: Option[A]) extends AnyVal { */ def toValidNel[B](b: => B): ValidatedNel[B, A] = oa.fold[ValidatedNel[B, A]](Validated.invalidNel(b))(Validated.Valid(_)) + /** + * If the `Option` is a `Some`, return its value in a [[cats.data.Ior.Right]]. + * If the `Option` is `None`, wrap the provided `B` value in a [[cats.data.Ior.Left]] + * + * Example: + * {{{ + * scala> import cats.data.Ior + * scala> import cats.implicits._ + * + * scala> val result1: Option[Int] = Some(3) + * scala> result1.toRightIor("error!") + * res0: Ior[String, Int] = Right(3) + * + * scala> val result2: Option[Int] = None + * scala> result2.toRightIor("error!") + * res1: Ior[String, Int] = Left(error!) + * }}} + */ + def toRightIor[B](b: => B): Ior[B, A] = oa.fold[Ior[B, A]](Ior.Left(b))(Ior.Right(_)) + + /** + * If the `Option` is a `Some`, return its value in a [[cats.data.Ior.Left]]. + * If the `Option` is `None`, wrap the provided `B` value in a [[cats.data.Ior.Right]] + * + * Example: + * {{{ + * scala> import cats.data.Ior + * scala> import cats.implicits._ + * + * scala> val result1: Option[String] = Some("error!") + * scala> result1.toLeftIor(3) + * res0: Ior[String, Int] = Left(error!) + * + * scala> val result2: Option[String] = None + * scala> result2.toLeftIor(3) + * res1: Ior[String, Int] = Right(3) + * }}} + */ + def toLeftIor[B](b: => B): Ior[A, B] = oa.fold[Ior[A, B]](Ior.Right(b))(Ior.Left(_)) + /** * If the `Option` is a `Some`, return its value. If the `Option` is `None`, * return the `empty` value for `Monoid[A]`. diff --git a/core/src/main/scala/cats/syntax/package.scala b/core/src/main/scala/cats/syntax/package.scala index ce28a9c2a4..725c8d7bbd 100644 --- a/core/src/main/scala/cats/syntax/package.scala +++ b/core/src/main/scala/cats/syntax/package.scala @@ -22,6 +22,7 @@ package object syntax { object functorFilter extends FunctorFilterSyntax object group extends GroupSyntax object invariant extends InvariantSyntax + object ior extends IorSyntax object list extends ListSyntax object monad extends MonadSyntax object monadCombine extends MonadCombineSyntax diff --git a/tests/src/test/scala/cats/tests/IorTests.scala b/tests/src/test/scala/cats/tests/IorTests.scala index ae8e4946af..0c85652904 100644 --- a/tests/src/test/scala/cats/tests/IorTests.scala +++ b/tests/src/test/scala/cats/tests/IorTests.scala @@ -1,10 +1,10 @@ package cats package tests -import cats.data.Ior +import cats.data.{Ior, NonEmptyList} import cats.kernel.laws.GroupLaws -import cats.laws.discipline.{BifunctorTests, CartesianTests, MonadTests, SerializableTests, TraverseTests} import cats.laws.discipline.arbitrary._ +import cats.laws.discipline.{BifunctorTests, CartesianTests, MonadTests, SerializableTests, TraverseTests} import org.scalacheck.Arbitrary._ class IorTests extends CatsSuite { @@ -153,15 +153,15 @@ class IorTests extends CatsSuite { } } - test("append left") { + test("combine left") { forAll { (i: Int Ior String, j: Int Ior String) => - i.append(j).left should === (i.left.map(_ + j.left.getOrElse(0)).orElse(j.left)) + i.combine(j).left should === (i.left.map(_ + j.left.getOrElse(0)).orElse(j.left)) } } - test("append right") { + test("combine right") { forAll { (i: Int Ior String, j: Int Ior String) => - i.append(j).right should === (i.right.map(_ + j.right.getOrElse("")).orElse(j.right)) + i.combine(j).right should === (i.right.map(_ + j.right.getOrElse("")).orElse(j.right)) } } @@ -198,6 +198,24 @@ class IorTests extends CatsSuite { } } + test("toValidated consistent with right") { + forAll { (x: Int Ior String) => + x.toValidated.toOption should === (x.right) + } + } + + test("leftNel") { + forAll { (x: String) => + Ior.leftNel(x).left should === (Some(NonEmptyList.of(x))) + } + } + + test("bothNel") { + forAll { (x: Int, y: String) => + Ior.bothNel(y, x).onlyBoth should === (Some((NonEmptyList.of(y), x))) + } + } + test("getOrElse consistent with Option getOrElse") { forAll { (x: Int Ior String, default: String) => x.getOrElse(default) should === (x.toOption.getOrElse(default)) diff --git a/tests/src/test/scala/cats/tests/ValidatedTests.scala b/tests/src/test/scala/cats/tests/ValidatedTests.scala index 7904f7a860..ed248ec9fa 100644 --- a/tests/src/test/scala/cats/tests/ValidatedTests.scala +++ b/tests/src/test/scala/cats/tests/ValidatedTests.scala @@ -1,13 +1,13 @@ package cats package tests -import cats.data.{EitherT, NonEmptyList, Validated, ValidatedNel} -import cats.data.Validated.{Valid, Invalid} -import cats.laws.discipline.{BitraverseTests, TraverseTests, ApplicativeErrorTests, SerializableTests, CartesianTests} +import cats.data._ +import cats.data.Validated.{Invalid, Valid} +import cats.laws.discipline.{ApplicativeErrorTests, BitraverseTests, CartesianTests, SerializableTests, TraverseTests} import org.scalacheck.Arbitrary._ -import cats.laws.discipline.{SemigroupKTests} +import cats.laws.discipline.SemigroupKTests import cats.laws.discipline.arbitrary._ -import cats.kernel.laws.{OrderLaws, GroupLaws} +import cats.kernel.laws.{GroupLaws, OrderLaws} import scala.util.Try @@ -187,6 +187,18 @@ class ValidatedTests extends CatsSuite { } } + test("fromIor consistent with Ior.toValidated"){ + forAll { (i: Ior[String, Int]) => + Validated.fromIor(i) should === (i.toValidated) + } + } + + test("toIor then fromEither is identity") { + forAll { (v: Validated[String, Int]) => + Validated.fromIor(v.toIor) should === (v) + } + } + test("isValid after combine, iff both are valid") { forAll { (lhs: Validated[Int, String], rhs: Validated[Int, String]) => lhs.combine(rhs).isValid should === (lhs.isValid && rhs.isValid)