1

I'm trying to create a generic HTTP client in Scala using spray. Here is the class definition:

object HttpClient extends HttpClient

class HttpClient {

  implicit val system = ActorSystem("api-spray-client")
  import system.dispatcher
  val log = Logging(system, getClass)

  def httpSaveGeneric[T1:Marshaller,T2:Unmarshaller](uri: String, model: T1, username: String, password: String): Future[T2] = {
    val pipeline: HttpRequest => Future[T2] = logRequest(log) ~> sendReceive ~> logResponse(log) ~> unmarshal[T2]
    pipeline(Post(uri, model))
  }

  val genericResult = httpSaveGeneric[Space,Either[Failure,Success]](
    "http://", Space("123", IdName("456", "parent"), "my name", "short_name", Updated("", 0)), "user", "password")

}

An object utils.AllJsonFormats has the following declaration. It contains all the model formats. The same class is used on the "other end" i.e. I also wrote the API and used the same formatters there with spray-can and spray-json.

object AllJsonFormats
  extends DefaultJsonProtocol with SprayJsonSupport with MetaMarshallers with MetaToResponseMarshallers with NullOptions {

Of course that object has definitions for the serialization of the models.api.Space, models.api.Failure and models.api.Success.

The Space type seems fine, i.e. when I tell the generic method that it will be receiving and returning a Space, no errors. But once I bring an Either into the method call, I get the following compiler error:

could not find implicit value for evidence parameter of type spray.httpx.unmarshalling.Unmarshaller[Either[models.api.Failure,models.api.Success]].

My expectation was that the either implicit in spray.json.DefaultJsonProtocol, i.e. in spray.json.StandardFormts, would have me covered.

The following is my HttpClient class trying it's best to be generic: Update: Clearer/Repeatable Code Sample

object TestHttpFormats
  extends DefaultJsonProtocol {

  // space formats
  implicit val idNameFormat = jsonFormat2(IdName)
  implicit val updatedByFormat = jsonFormat2(Updated)
  implicit val spaceFormat = jsonFormat17(Space)

  // either formats
  implicit val successFormat = jsonFormat1(Success)
  implicit val failureFormat = jsonFormat2(Failure)
}

object TestHttpClient
  extends SprayJsonSupport {

  import TestHttpFormats._
  import DefaultJsonProtocol.{eitherFormat => _, _ }

  val genericResult = HttpClient.httpSaveGeneric[Space,Either[Failure,Success]](
    "https://api.com/space", Space("123", IdName("456", "parent"), "my name", "short_name", Updated("", 0)), "user", "password")
}

With the above, the problem still occurs where the unmarshaller is unresolved. Help would be greatly appreciated..

Thanks.

atom.gregg
  • 987
  • 8
  • 14

1 Answers1

3

Spray defines a default marshaller for Either[A,B] if a Marshaller[A] and Marshaller[B] are in defined scope inside the MetaMarshallers trait. But, going the other direction requires an Unmarshaller. You will need to define an in-scope Unmarshaller for Either[Failure, Success]. This cannot be coded without specific knowledge of the expected response and what the strategy will be for choosing whether to unmarshall a response as a Left or as a Right. For example, let's say you want to return a Failure on a non-200 response and a Success from a 200 json response body:

type ResultT = Either[Failure,Success]
implicit val resultUnmarshaller: FromResponseUnmarshaller[ResultT] = 
  new FromResponseUnmarshaller[ResultT] {
    def apply(response: HttpResponse): Deserialized[ResultT] = response.status match {
      case StatusCodes.Success(200) => 
        Unmarshaller.unmarshal[Success](response.entity).right.map(Right(_))
      case _ => Right(Left(Failure(...)))
    }
  }

Update

Looking deeper into this, the problem appears to be that the default eitherFormat in spray.json.StandardFormats is not a RootJsonFormat, which is required by the default JSON unmarshaller defined in spray.httpx.SprayJsonSupport. Defining the following implicit method should solve the issue:

implicit def rootEitherFormat[A : RootJsonFormat, B : RootJsonFormat] = new RootJsonFormat[Either[A, B]] {
  val format = DefaultJsonProtocol.eitherFormat[A, B]

  def write(either: Either[A, B]) = format.write(either)

  def read(value: JsValue) = format.read(value)
}

I have an working example gist that hopefully explains how you would use this. https://gist.github.com/mikemckibben/fad4328de85a79a06bf3

MikeMcKibben
  • 468
  • 4
  • 8
  • Thanks for answering @MikeMcKibben, I also wrote a test where I explicity declared a marshaller for the types directly in scope, i.e. to ensure nothing funny was happening with the implicit resolution from my object and still nothing. I don't have an issue with un-marshalling. Marshalling is the issue. I will update the question once I'm in the office with some more details showing further examples of the same problem. Thanks.. – atom.gregg Aug 22 '14 at 06:01
  • The error message you pasted in your question suggests otherwise. The compiler error is due to not finding an implicit `Unmarshaller[Either[Failure,Success]]` not a `Marshaller[Space]`. You've only defined an `Unmarshaller` for `Success` and `Failure`, not `Either[Failure,Success]`. There is no default support in spray to unmarshal an `Either[A,B]`. – MikeMcKibben Aug 22 '14 at 16:51
  • oh, sorry. I missed that you are using the spray-json library. From your updated code sample, it looks like you are explicitly hiding the `eitherFormat` implicit. Shouldn't you remove the `import DefaultJsonProtocol.{eitherFormat => _, _ }` line? – MikeMcKibben Aug 22 '14 at 17:05
  • I'll give that a try, one of the things I'm not clear about but that was suggested on the spray-user forum, was how to explicitly bring in implicits. Matthias mentioned there that what he normally does is explicitly declare whats needed to assist the Scala compiler figure things out. But I honestly don't know exactly what the compiler should be looking for here and had found an example where he or someone else had written something similar to what I'd written in the sample. As you can probably guess, I'm writing code I don't completely understand.. – atom.gregg Aug 23 '14 at 14:53
  • 1
    ok, I've updated my answer with a working solution that you should be able to adapt to your sample. – MikeMcKibben Aug 23 '14 at 20:12
  • interesting.. regardless of whether your answer works or not (I'm yet to test it in my VM) you have taught me something more about how to read into the source code. a huge thank you MikeMcKibben, i'll check this out on Monday. – atom.gregg Aug 24 '14 at 08:59