5

Since documentation is not ready I'll ask akka maintainers here.

Why akka-http Unmarshaler returns Future[T] instead of T ? Here is my goal. I want to unmarshal class from XML http response similarly how it's done for json. For example I'd like to write

Unmarshal(HttpResponse.entity).to[Person]

where case class and its unmarshaller looks like this

case class Person(name: String, age: Int)

implicit val personUnmarshaller = Unmarshaller[NodeSeq, Person] { _ => xml =>
      Future(Person((xml \\ "name").text, (xml \\ "age").text.toInt))
}

It's not gonna compile with ScalaXmlSupport supplied with 1.0-RC4 because Unmarshaller[ResponseEntity,Person] is not available in the scope. So to trick it I wrote two implicit conversions

implicit def xmlUnmarshallerConverter[T](marsh: Unmarshaller[NodeSeq, T])(implicit mat: Materializer): FromEntityUnmarshaller[T] =
  xmlUnmarshaller(marsh, mat)

implicit def xmlUnmarshaller[T](implicit marsh: Unmarshaller[NodeSeq, T], mat: Materializer): FromEntityUnmarshaller[T] =
  defaultNodeSeqUnmarshaller.map(Unmarshal(_).to[T].value.get.get)

It works but I don't like ugly .value.get.get. Is there more elegant way to implement this ?

expert
  • 29,290
  • 30
  • 110
  • 214
  • I think it returns a `Future` because the `HttpEntity` can be non-strict and thus the body may be incomplete by the type of receiving the response. That is, in case you have chunked responses, you cannot unmarshal until the response is complete. – ale64bit Jul 04 '15 at 16:52
  • 1
    I had a similar issue with the "comfort" of receiving a `Future`. As I remember, the default `HttpEntity` has a known length, but still exposes the content through a `Source[ByteString]`, and that's why you get a `Future` I guess. Nonetheless, I agree with you that it's not very nice when you want to keep it simple. – ale64bit Jul 04 '15 at 17:07
  • Is there a reason to define a `Unmarshaller[NodeSeq, Person]` in the first place? Otherwise, you could directly define an `FromEntityUnmarshaller[Person]` with `defaultNodeSeqUnmarshaller.map(xml => /* the code from your personUnmarshaller*/)`. – jrudolph Jul 04 '15 at 18:00
  • @jrudolph I just wanted to use `NodeSeq` unmarshaler you guys provided which is being used based on `Content-Type`. I guess I could create my own trait instead of using `Unmarshaller[NodeSeq, T]` but I prefer to use as many standard components as possible.. – expert Jul 04 '15 at 18:18
  • @jrudolph How should I rewrite `xmlUnmarshaller` to avoid `value.get.get` ? I get an exception because `None,get` is called. Apparently unmarshaller tries to get the value before it's calculated. – expert Jul 05 '15 at 00:12
  • I just meant that you don't need to define an intermediary `Unmarshaller[NodeSeq, Person]` to make use of the standard `NodeSeq` unmashaller. That's why I would suggest to try `implicit val personUnmarshaller: FromEntityUnmarshaller[Person] = defaultNodeSeqUnmarshaller.map(xml => /* the code from your personUnmarshaller*/)` – jrudolph Jul 05 '15 at 09:39
  • @jrudolph I agree. But what if I want to reuse unmarshaller's code not only in processing of http responses but also somewhere else (in tests for example). Having generic solution in form of Unmarshaller[NodeSeq, T] is nice. Why don't you guys make FromEntityUnmarshaller work with Futures like everything else? Current design is inconsistent with your goal to make everything asynchronous – expert Jul 05 '15 at 10:08

1 Answers1

4

Well, I've implemented my own solution for now but I hope Akka team will make sync/async stuff consistent in the library.

So I created simple clone of Unmarshaller which is defined as follows

trait SyncUnmarshaller[-A, B] {
  def apply(value: A): B
}

object SyncUnmarshaller {
  def apply[A, B](f: A => B): SyncUnmarshaller[A, B] =
    new SyncUnmarshaller[A, B] {
      def apply(a: A) = f(a)
    }
}

object SyncUnmarshal {
  def apply[T](value: T): SyncUnmarshal[T] = new SyncUnmarshal(value)
}

class SyncUnmarshal[A](val value: A) {
  def to[B](implicit um: SyncUnmarshaller[A, B]): B = um(value)
}

Therefore unmarshallers for domain classes will be defined like this

implicit val articleBodyUnmarshaller = SyncUnmarshaller[NodeSeq, ArticleBody] { xml =>
  ArticleBody(xml.toString())
}

Then there are two implicits for ScalaXmlSupport that I already mentioned

implicit def xmlUnmarshallerConverter[T](marshaller: SyncUnmarshaller[NodeSeq, T])(implicit mat: Materializer): FromEntityUnmarshaller[T] =
  xmlUnmarshaller(marshaller, mat)

implicit def xmlUnmarshaller[T](implicit marshaller: SyncUnmarshaller[NodeSeq, T], mat: Materializer): FromEntityUnmarshaller[T] = {
  defaultNodeSeqUnmarshaller.map(marshaller(_))

That's it. And finally in case you want to use Akka's calls like

Unmarshal(response.entity).to[Article].map(Right(_))

You'd need converter from my SyncUnmarshaller to akka's Unmarshaller

implicit def syncToAsyncConverter[A, B](marshaller: SyncUnmarshaller[A, B]): Unmarshaller[A, B] =
  new Unmarshaller[A, B] {
    def apply(a: A)(implicit ec: ExecutionContext) =
      try FastFuture.successful(marshaller(a))
      catch { case NonFatal(e) ⇒ FastFuture.failed(e) }
  }
expert
  • 29,290
  • 30
  • 110
  • 214