org.scalactic.FutureSugar.scala Maven / Gradle / Ivy
/*
* Copyright 2001-2013 Artima, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.scalactic
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
import exceptions.ValidationFailedException
/**
* Trait providing an implicit class that adds a validating
method to
* Future
, which takes one or more validation functions and returns either the
* same Future
if either the Future
had already failed or its value
* passes all the functions, or [[org.scalactic.exceptions.ValidationFailedException `ValidationFailedException`]] containing an error message
* describing the first validation that failed.
*
*
* Here's an example validation method, which passes if the given Int
is evenly
* divisible by 10 (i.e., the result will be [[org.scalactic.Pass Pass
]]). If the value does not pass
* this test, the result is a [[org.scalactic.Fail Fail
]] containing a helpful error message string.
*
*
*
* scala> import org.scalactic._
* import org.scalactic._
*
* scala> import FutureSugar._
* import org.scalactic.FutureSugar._
*
* scala> import scala.concurrent.Future
* import scala.concurrent.Future
*
* scala> import scala.concurrent.ExecutionContext.Implicits.global
* import scala.concurrent.ExecutionContext.Implicits.global
*
* scala> def isRound(i: Int): Validation[ErrorMessage] =
* | if (i % 10 == 0) Pass else Fail(i + " was not a round number")
* isRound: (i: Int)org.scalactic.Validation[org.scalactic.ErrorMessage]
*
*
*
* Validation will be attempted on a successful Try
. If the validation succeeds, the
* resulting Future
will be the same successful Future
with the same value. (A
* "validation" only transforms the Future
if the validation fails, otherwise it is the
* same Future
. The only difference is its value has now been proven valid.)
* In the following example, a successful Future[Int]
with the value 100
* passes the validation (which checks whether 100 is evenly divisible by 10), therefore
* the result of the validating
call is the same successful Future
* with the same value.
*
*
*
* scala> val fut100 = Future(100)
* fut100: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@67f9c9c6
*
* scala> fut100.value
* res0: Option[scala.util.Try[Int]] = Some(Success(100))
*
* scala> val round100 = fut100.validating(isRound)
* round100: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@1ac2f0d1
*
* scala> round100.value
* res1: Option[scala.util.Try[Int]] = Some(Success(100))
*
*
*
* If validation fails, the successful Future
will be transformed into a failed one, with
* a ValidationFailedException
that contains the error message
* returned by the validation function. In the following example, 42 fails the validation because it
* is not evenly divisible by 10:
*
*
*
* scala> val fut42 = Future(42)
* fut42: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@19c6e4d1
*
* scala> fut42.value
* res2: Option[scala.util.Try[Int]] = Some(Success(42))
*
* scala> val round42 = fut42.validating(isRound)
* round42: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@b5175d
*
* scala> round42.value
* res3: Option[scala.util.Try[Int]] = Some(Failure(org.scalactic.exceptions.ValidationFailedException: 42 was not a round number))
*
*
*
* If validating
is called on a failed Future
, it just returns the same failed Future
:
*
*
*
* scala> val futEx = Future[Int] { throw new Exception("oops!") }
* futEx: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@3ba0299c
*
* scala> futEx.value
* res4: Option[scala.util.Try[Int]] = Some(Failure(java.lang.Exception: oops!))
*
* scala> val roundEx = futEx.validating(isRound)
* roundEx: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@22bf1acf
*
* scala> roundEx.value
* res5: Option[scala.util.Try[Int]] = Some(Failure(java.lang.Exception: oops!))
*
*
*
* The validating
method accepts one or more validation functions. If you
* pass more than one, they will be tried in order up until the first failure, whose
* error message will appear in the ValidationFailedException
. In other words,
* validating
will short circuit at the first error and return that. It
* will not accumulate errors. For example, the following validation will short circuit
* after the isDivBy3
function fails:
*
*
*
* scala> def isDivBy3(i: Int): Validation[ErrorMessage] =
* | if (i % 3 == 0) Pass else Fail(i + " was not divisible by 3")
* isDivBy3: (i: Int)org.scalactic.Validation[org.scalactic.ErrorMessage]
*
* scala> def isAnswerToLifeTheUniverseAndEverything(i: Int): Validation[ErrorMessage] =
* | if (i == 42) Pass else Fail(i + " did not equal 42")
* isAnswerToLifeTheUniverseAndEverything: (i: Int)org.scalactic.Validation[org.scalactic.ErrorMessage]
*
* scala> val futShort = fut100.validating(isRound, isDivBy3, isAnswerToLifeTheUniverseAndEverything)
* futShort: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@30bb943e
*
* scala> futShort.value
* res11: Option[scala.util.Try[Int]] = Some(Failure(org.scalactic.exceptions.ValidationFailedException: 100 was not divisible by 3))
*
*/
trait FutureSugar {
/**
* Implicit class that adds a validation
method to
* Future
, which takes one or more functions that validate
* the Future
's value.
*
*
* See the main documentation for trait [[org.scalactic.FutureSugar `FutureSugar`]] for more detail and examples.
*
*
* @param theFuture the Future
to which to add a validating
method.
*/
implicit class Futureizer[T](theFuture: Future[T]) {
/**
* Validates a Future
using the passed validation functions.
*
*
* See the main documentation for trait [[org.scalactic.FutureSugar `FutureSugar`]] for more detail and examples.
*
*
* @param first the first validation function to apply
* @param rest the subsequent validation functions to apply, if any
* @return a "validated" Future
, either a Future
with the same value, or
* if validation failed, a failed Future
containing a ValidationFailedException
.
*/
def validating(first: T => Validation[ErrorMessage], rest: (T => Validation[ErrorMessage])*)(implicit executor: ExecutionContext): Future[T] = {
theFuture.flatMap { (o: T) =>
TrySugar.passOrFirstFail(o, first :: rest.toList) match {
case Pass => theFuture
case Fail(errorMessage) => Future.failed(ValidationFailedException(errorMessage))
}
}
}
}
}
/**
* Companion object for FutureSugar
enabling its members to be
* imported as an alternative to mixing them in.
*/
object FutureSugar extends FutureSugar