diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/UnmarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/UnmarshallingSpec.scala index 883fb106a9..9512099b39 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/UnmarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/unmarshalling/UnmarshallingSpec.scala @@ -4,18 +4,22 @@ package akka.http.scaladsl.unmarshalling +import akka.http.scaladsl.unmarshalling.Unmarshaller.EitherUnmarshallingException import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers } import akka.http.scaladsl.testkit.ScalatestUtils import akka.actor.ActorSystem import akka.stream.ActorMaterializer import akka.http.scaladsl.model._ +import scala.concurrent.duration._ + +import scala.concurrent.Await class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils { implicit val system = ActorSystem(getClass.getSimpleName) implicit val materializer = ActorMaterializer() import system.dispatcher - "The PredefinedFromEntityUnmarshallers." - { + "The PredefinedFromEntityUnmarshallers" - { "stringUnmarshaller should unmarshal `text/plain` content in UTF-8 to Strings" in { Unmarshal(HttpEntity("Hällö")).to[String] should evaluateTo("Hällö") } @@ -23,5 +27,43 @@ class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll wi Unmarshal(HttpEntity("árvíztűrő ütvefúrógép")).to[Array[Char]] should evaluateTo("árvíztűrő ütvefúrógép".toCharArray) } } + + "The GenericUnmarshallers" - { + implicit val rawInt: FromEntityUnmarshaller[Int] = Unmarshaller(implicit ex ⇒ bs ⇒ bs.toStrict(1.second).map(_.data.utf8String.toInt)) + implicit val rawlong: FromEntityUnmarshaller[Long] = Unmarshaller(implicit ex ⇒ bs ⇒ bs.toStrict(1.second).map(_.data.utf8String.toLong)) + + "eitherUnmarshaller should unmarshal its Right value" in { + // we'll find: + // PredefinedFromEntityUnmarshallers.eitherUnmarshaller[String, Int] will be found + // + // which finds: + // rawInt: FromEntityUnmarshaller[Int] + // + + // stringUnmarshaller: FromEntityUnmarshaller[String] + + val testRight = Unmarshal(HttpEntity("42")).to[Either[String, Int]] + Await.result(testRight, 1.second) should ===(Right(42)) + } + + "eitherUnmarshaller should unmarshal its Left value" in { + val testLeft = Unmarshal(HttpEntity("I'm not a number, I'm a free man!")).to[Either[String, Int]] + Await.result(testLeft, 1.second) should ===(Left("I'm not a number, I'm a free man!")) + } + + "eitherUnmarshaller report both error messages if unmarshalling failed" in { + type ImmenseChoice = Either[Long, Int] + val testLeft = Unmarshal(HttpEntity("I'm not a number, I'm a free man!")).to[ImmenseChoice] + val ex = intercept[EitherUnmarshallingException] { + Await.result(testLeft, 1.second) + } + + ex.getMessage should include("Either[long, int]") + ex.getMessage should include("attempted int first") + ex.getMessage should include("Right failure: For input string") + ex.getMessage should include("Left failure: For input string") + } + + } + override def afterAll() = system.terminate() } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/GenericUnmarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/GenericUnmarshallers.scala index 3aa83fc25c..52e01f15ab 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/GenericUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/GenericUnmarshallers.scala @@ -4,7 +4,12 @@ package akka.http.scaladsl.unmarshalling +import akka.http.scaladsl.unmarshalling.Unmarshaller.EitherUnmarshallingException import akka.http.scaladsl.util.FastFuture +import akka.stream.impl.ConstantFun + +import scala.concurrent.Future +import scala.reflect.ClassTag trait GenericUnmarshallers extends LowerPriorityGenericUnmarshallers { @@ -26,4 +31,34 @@ sealed trait LowerPriorityGenericUnmarshallers { case Some(a) ⇒ um(a) case None ⇒ FastFuture.failed(Unmarshaller.NoContentException) }) + + /** + * Enables using [[Either]] to encode the following unmarshalling logic: + * Attempt unmarshalling the entity as as `R` first (yielding `R`), + * and if it fails attempt unmarshalling as `L` (yielding `Left`). + * + * Note that the Either's "R" type will be attempted first (as Left is often considered as the "failed case" in Either). + */ + // format: OFF + implicit def eitherUnmarshaller[L, R](implicit ua: FromEntityUnmarshaller[L], rightTag: ClassTag[R], + ub: FromEntityUnmarshaller[R], leftTag: ClassTag[L]): FromEntityUnmarshaller[Either[L, R]] = + Unmarshaller.withMaterializer { implicit ex ⇒ implicit mat ⇒ value ⇒ + import akka.http.scaladsl.util.FastFuture._ + @inline def right = ub(value).fast.map(Right(_)) + @inline def fallbackLeft: PartialFunction[Throwable, Future[Either[L, R]]] = { case rightFirstEx ⇒ + val left = ua(value).fast.map(Left(_)) + + // combine EitherUnmarshallingException by carring both exceptions + left.transform( + s = ConstantFun.scalaIdentityFunction, + f = leftSecondEx => new EitherUnmarshallingException( + rightClass = rightTag.runtimeClass, right = rightFirstEx, + leftClass = leftTag.runtimeClass, left = leftSecondEx) + ) + } + + right.recoverWith(fallbackLeft) + } + // format: ON + } \ No newline at end of file diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala index 01c2f11587..21f629c050 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/PredefinedFromEntityUnmarshallers.scala @@ -8,6 +8,8 @@ import akka.util.ByteString import akka.http.scaladsl.util.FastFuture import akka.http.scaladsl.model._ +import scala.concurrent.Future + trait PredefinedFromEntityUnmarshallers extends MultipartUnmarshallers { implicit def byteStringUnmarshaller: FromEntityUnmarshaller[ByteString] = diff --git a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala index a8a7098b7e..dd1a7984b5 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/unmarshalling/Unmarshaller.scala @@ -4,6 +4,7 @@ package akka.http.scaladsl.unmarshalling +import akka.event.Logging import akka.stream.Materializer import scala.util.control.{ NoStackTrace, NonFatal } @@ -141,6 +142,15 @@ object Unmarshaller final case class UnsupportedContentTypeException(supported: Set[ContentTypeRange]) extends RuntimeException(supported.mkString("Unsupported Content-Type, supported: ", ", ", "")) + /** Order of parameters (`right` first, `left` second) is intentional, since that's the order we evaluate them in. */ + final case class EitherUnmarshallingException( + rightClass: Class[_], right: Throwable, + leftClass: Class[_], left: Throwable) + extends RuntimeException( + s"Failed to unmarshal Either[${Logging.simpleName(leftClass)}, ${Logging.simpleName(rightClass)}] (attempted ${Logging.simpleName(rightClass)} first). " + + s"Right failure: ${right.getMessage}, " + + s"Left failure: ${left.getMessage}") + object UnsupportedContentTypeException { def apply(supported: ContentTypeRange*): UnsupportedContentTypeException = UnsupportedContentTypeException(Set(supported: _*)) }