diff --git a/src/main/scala/parsley/Result.scala b/src/main/scala/parsley/Result.scala index 80f0685c3..263aa4afa 100644 --- a/src/main/scala/parsley/Result.scala +++ b/src/main/scala/parsley/Result.scala @@ -1,14 +1,137 @@ package parsley +import scala.util.{Try, Success => TSuccess, Failure => TFailure} + /** * Result of a parser. Either a `Success[A]` or a `Failure` * @tparam A The type of expected success result */ -//TODO: Make these a bit more full fledged sealed abstract class Result[+A] { - def toOption: Option[A] - def toEither: Either[String, A] + /** Applies `fa` if this is a `Failure` or `fb` if this is a `Success`. + * + * @param ferr the function to apply if this is a `Failure` + * @param fa the function to apply if this is a `Success` + * @return the results of applying the function + */ + def fold[B](ferr: String => B, fa: A => B): B = this match { + case Success(x) => fa(x) + case Failure(msg) => ferr(msg) + } + + /** Executes the given side-effecting function if this is a `Success`. + * + * @param f The side-effecting function to execute. + */ + def foreach[U](f: A => U): Unit = this match { + case Success(x) => f(x) + case _ => + } + + /** Returns the results's value. + * + * @note The result must not be a failure. + * @throws NoSuchElementException if the result is a failure. + */ + def get: A + + /** Returns the value from this `Success` or the given argument if this is a `Failure`. */ + def getOrElse[B >: A](or: =>B): B = orElse(Success(or)).get + + /** Returns this `Success` or the given argument if this is a `Failure`. */ + def orElse[B >: A](or: =>Result[B]): Result[B] = this match { + case Success(_) => this + case _ => or + } + + /** Returns `true` if this is a `Success` and its value is equal to `elem` (as determined by `==`), + * returns `false` otherwise. + * + * @param elem the element to test. + * @return `true` if this is a `Success` value equal to `elem`. + */ + final def contains[B >: A](elem: B): Boolean = exists(_ == elem) + + /** Returns `true` if `Failure` or returns the result of the application of + * the given predicate to the `Success` value. + */ + def forall(f: A => Boolean): Boolean = this match { + case Success(x) => f(x) + case _ => true + } + + /** Returns `false` if `Failure` or returns the result of the application of + * the given predicate to the `Success` value. + */ + def exists(p: A => Boolean): Boolean = this match { + case Success(x) => p(x) + case _ => false + } + + /** Binds the given function across `Success`. + * + * @param f The function to bind across `Success`. + */ + def flatMap[B](f: A => Result[B]): Result[B] = this match { + case Success(x) => f(x) + case _ => this.asInstanceOf[Result[B]] + } + + /** Returns the right value if this is right + * or this value if this is left + * + * Equivalent to `flatMap(id => id)` + */ + def flatten[B](implicit ev: A <:< Result[B]): Result[B] = flatMap(ev) + + /** The given function is applied if this is a `Success`. */ + def map[B](f: A => B): Result[B] = this match { + case Success(x) => Success(f(x)) + case _ => this.asInstanceOf[Result[B]] + } + + /** Returns `Success` with the existing value of `Success` if this is a `Success` + * and the given predicate `p` holds for the right value, + * or `Failure(msg)` if this is a `Success` and the given predicate `p` does not hold for the right value, + * or `Failure` with the existing value of `Failure` if this is a `Failure`. + */ + def filterOrElse(p: A => Boolean, msg: =>String): Result[A] = this match { + case Success(x) if !p(x) => Failure(msg) + case _ => this + } + + /** Returns a `Seq` containing the `Success` value if + * it exists or an empty `Seq` if this is a `Failure`. + */ + def toSeq: collection.immutable.Seq[A] = this match { + case Success(x) => collection.immutable.Seq(x) + case _ => collection.immutable.Seq.empty + } + + /** Returns a `Some` containing the `Success` value + * if it exists or a `None` if this is a `Failure`. + */ + def toOption: Option[A] = this match { + case Success(x) => Some(x) + case _ => None + } + + /** Converts the `Result` into a `Try` where `Failure` maps to a plain `Exception` */ + def toTry: Try[A] = this match { + case Success(x) => TSuccess(x) + case Failure(msg) => TFailure(new Exception(s"ParseError: $msg")) + } + + def toEither: Either[String, A] = this match { + case Success(x) => Right(x) + case Failure(msg) => Left(msg) + } + + /** Returns `true` if this is a `Success`, `false` otherwise. */ + def isSuccess: Boolean + + /** Returns `true` if this is a `Failure`, `false` otherwise. */ + def isFailure: Boolean } /** @@ -18,8 +141,9 @@ sealed abstract class Result[+A] */ case class Success[A] private [parsley] (x: A) extends Result[A] { - override def toOption: Option[A] = Some(x) - override def toEither: Either[String, A] = Right(x) + override def isSuccess: Boolean = true + override def isFailure: Boolean = false + override def get: A = x } /** @@ -28,6 +152,7 @@ case class Success[A] private [parsley] (x: A) extends Result[A] */ case class Failure private [parsley] (msg: String) extends Result[Nothing] { - override def toOption: Option[Nothing] = None - override def toEither: Either[String, Nothing] = Left(msg) + override def isSuccess: Boolean = false + override def isFailure: Boolean = true + override def get: Nothing = throw new NoSuchElementException("get called on Failure") } \ No newline at end of file diff --git a/src/test/scala/parsley/ResultTests.scala b/src/test/scala/parsley/ResultTests.scala new file mode 100644 index 000000000..ad576f1ef --- /dev/null +++ b/src/test/scala/parsley/ResultTests.scala @@ -0,0 +1,102 @@ +package parsley + +import scala.language.implicitConversions + +class ResultTests extends ParsleyTest { + "Success" should "return true for isSuccess" in { + Success(7).isSuccess shouldBe true + } + it should "return false for isFailure" in { + Success(7).isFailure shouldBe false + } + it should "not throw an exception on get" in { + noException should be thrownBy (Success(4).get) + } + + "Failure" should "return false for isSuccess" in { + Failure("oops").isSuccess shouldBe false + } + it should "return true for isFailure" in { + Failure("oops").isFailure shouldBe true + } + it should "throw an exception on get" in { + a [NoSuchElementException] should be thrownBy (Failure("oops").get) + } + + "Result[A]" should "behave like Either[String, A] on success" in { + val rx = for { + y <- Success(5) + x <- Success(y + 4) + } yield x + val ex = for { + y <- Right(5) + x <- Right(y + 4) + } yield x + rx.toEither should equal (ex) + } + it should "behave like Either[String, A] on failure" in { + val rx = for { + y <- Failure("oops") + x <- Success((y: Int) + 4) + } yield x + val ex = for { + y <- Left("oops") + x <- Right((y: Int) + 4) + } yield x + rx.toEither should equal (ex) + val rx2 = for { + y <- Success(5) + x <- Failure("oops") + } yield x + val ex2 = for { + y <- Right(5) + x <- Left("oops") + } yield x + rx2.toEither should equal (ex2) + } + it should "behave like Option[A] when extracting elements" in { + Success(7).getOrElse(5) should equal (Some(7).getOrElse(5)) + Failure("oops").getOrElse(5) should equal (None.getOrElse(5)) + Success(7).toOption should equal (Some(7)) + Failure("oops").toOption should equal (None) + } + it should "throw an exception when it fails and converted to Try" in { + noException should be thrownBy (Success(7).toTry.get) + an [Exception] should be thrownBy (Failure("oops").toTry.get) + } + it should "be convertible to a possibly one element sequence" in { + Success(4).toSeq should have size 1 + Failure("oops").toSeq shouldBe empty + } + it should "support contains" in { + Success(5).contains(5) shouldBe true + Success(4).contains(5) shouldBe false + Failure("oops").contains(5) shouldBe false + } + it should "support forall" in { + Success(5).forall((x: Int) => x % 2 == 0) shouldBe false + Success(4).forall((x: Int) => x % 2 == 0) shouldBe true + Failure("msg").forall((x: Int) => x % 2 == 0) shouldBe true + } + it should "support foreach" in { + var x = 0 + for (y <- Failure("msg")) x = y + x shouldBe 0 + for (y <- Success(3)) x = y + x shouldBe 3 + } + it should "flatten correctly" in { + Success(Success(4)).flatten.isSuccess shouldBe true + Success(Failure("ops")).flatten.isFailure shouldBe true + Failure("msg").flatten.isFailure shouldBe true + } + it should "be filterable" in { + Success(5).filterOrElse(_ == 5, "???") shouldBe Success(5) + Success(5).filterOrElse(_ == 4, "should be 4!") shouldBe Failure("should be 4!") + Failure("oops").filterOrElse(_ == 4, "should be 4!") shouldBe Failure("oops") + } + it should "be foldable" in { + Success(4).fold(_ => 0, _+1) shouldBe 5 + Failure("oops").fold(_ => 0, (x: Int) => x + 1) shouldBe 0 + } +} \ No newline at end of file