Akka Http - How to Unmarshall ResponseEntity to CustomClass? - scala

I am using Akka Http to make requests to a 3rd party API. The responses are "application/json", and I would like to use Akka Http to convert them to a custom case class. I would like to do something like this:
val request = RequestBuilding.Get("https://service.com/v1/api/items")
val response : Future[ItemsResponse] = http.singleRequest(request).flatMap({ response =>
Unmarshal(response.entity).to[ItemsResponse]
})
This fails to compile, because I am missing an implicit unmarshaller of type akka.http.scaladsl.unmarshalling.Unmarshaller[akka.http.scaladsl.model.ResponseEntity, com.mycompany.models.ItemsResponse].
It's unclear to me what the idiomatic way to do this with akka http is. I am aware that I could use spray-json, but I'd like to understand how to do this without importing another library. It seems possible with Akka Http, but the documentation isn't clear (to me at least).

The simplest way is to use spray-json as it comes as part of Akka HTTP:
import spray.json._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
// change 2 to the number of attributes of ItemsResponse
implicit val ItemsResponseFormat = jsonFormat2(ItemsResponse)
This should make your existing code compile.

I think your question is valid, and there are cases where avoiding extra dependencies makes sense. Mine is from making an authentication library, where I don't want to impose my JSON library preferences to the users of such library. The library needs JSON unmarshalling for understanding a token info response.
To the code! :)
case class TokenInfo private (uid: String, realm: String, scope: Seq[String])
object TokenInfo {
private
def parseOpt(s: String): Option[TokenInfo] = {
util.parsing.json.JSON.parseFull(s) match {
case Some(map: Map[String,Any] #unchecked) =>
val tmp: Map[String,Any] = map.collect {
case (k# "uid",x: String) => k -> x
case (k# "realm",x: String) => k -> x
case (k# "scope",x: Seq[String] #unchecked) => k -> x
// other keys are ignored
}.toMap
if (tmp.size == 3) {
Some( TokenInfo( tmp("uid").asInstanceOf[String], tmp("realm").asInstanceOf[String], tmp("scope").asInstanceOf[Seq[String]]) )
} else {
None
}
case _ => None
}
}
implicit
val unm: FromEntityUnmarshaller[TokenInfo] = {
PredefinedFromEntityUnmarshallers.stringUnmarshaller.map{ s => parseOpt(s).getOrElse{
throw new RuntimeException(s"Unknown TokenInfo: $s")
}}
}
}
I chose to use util.parsing.json which comes within Scala. The other option was simply regex's, but in that case I'm either fixing the expected order of fields, or the code might get complex.

Related

Custom spray json format of Either[TimeSlot,DateSlot] stuck in some kind of type-inference loop

My spray json serialization does not seem to work, as my tests just keeps running, when i am trying to serialize an Either[TimeSlot, DateSlot] object to JsValue, and vice-versa when i am trying to parse a json string and convert it to Either[TimeSlot,DateSlot] object.
I have been reading some others with same issues, just with a Seq[Either[int,String], but the solution was hardly understood. Furthermore i have tried using the standardized Either json format, but problem is, that i need to define it with names and types, to make it more intuitive.
TimeSlot and DateSlot, is working fine.
implicit object eitherDateOrTimeSlotFormat
extends RootJsonFormat[Either[TimeSlot, DateSlot]] {
private val timeSlotTypeKey = "timeSlotType"
private val timeSlotValueKey = "timeSlotValue"
override def write(obj: Either[TimeSlot, DateSlot]): JsValue = obj match {
case Left(timeSlot) ⇒
JsObject(
timeSlotTypeKey → JsString("timeSlot"),
timeSlotValueKey → timeSlot.toJson
)
case Right(dateSlot) =>
JsObject(
timeSlotTypeKey → JsString("dateSlot"),
timeSlotValueKey → dateSlot.toJson
)
}
override def read(json: JsValue): Either[TimeSlot, DateSlot] = json match {
case JsObject(fields)
if fields.isDefinedAt("timeSlotType") && fields
.isDefinedAt("timeSlotValue") ⇒
fields("timeSlotType") match {
case JsString(slotType) ⇒
slotType match {
case "timeSlot" ⇒
Left(fields("timeSlotValue").convertTo[TimeSlot])
case "dateSlot" ⇒
Right(fields("timeSlotValue").convertTo[DateSlot])
case _ ⇒
throw DeserializationException(
s"${json.compactPrint} did not match protocol"
)
}
case _ ⇒
throw DeserializationException(
s"${json.compactPrint} did not match protocol"
)
}
}
}
It seems the tests is running forever, like if they were stuck in some kind of infinity-loop, and would of course be expected to just serialize, so my tests would assert the results.
This was not a programmatically error, it seems there is a problem with either Idea Intellij community, Sbt test or scalac. But I manually tested the code instead of running inside the test-environment, and everything was fine.
What i thought was Infinte-loop test

Mocking of BlazeClientBuilder[IO] to return mock client[IO]

I am using the BlazeClientBuilder[IO].resource method to get Client[IO]. Now, I want to mock the client for unit testing but cannot figure out how to do so. Is there a good way of mocking this and how would I do that?
class ExternalCall(val resource: Resource[IO, Client[IO]], externalServiceUrl: Uri) {
def retrieveData: IO[Either[Throwable, String]] = {
for {
req <- IO(Request[IO](Method.GET, uri = externalServiceUrl))
response <- resource.use(client => {
client.fetch[String](req)(httpResponse => {
if (!httpResponse.status.isSuccess)
throw new Exception(httpResponse.status.reason)
else
httpResponse.as[String]
})
})
} yield Right(response)
}
}
Caller code
new ExternalCall(BlazeClientBuilder[IO](global).resource).retrieveData
It seems you only need to do something like
val resourceMock = mock[Resource[IO, Client[IO]]]
//stub whatever is necessary
val call = new ExternalCall(resourceMock).retrieveData
//do asserts and verifications as needed
EDIT:
You can see a fully working example below, but I'd like to stress that this is a good example of why it is a good practice to avoid mocking APIs that you don't own.
A better way to test this would be to place the http4s related code witin a class you own (YourHttpClient or whatever) and write an integration test for that class that checks that the http4s client does the right thing (you can use wiremock to simulate a real http server).
Then you can pass mocks of YourHttpClient to the components that depend on it, with the advantage that you control its API so it will be simpler and if http4s ever updates its API you only have one breaking class rather than having to fix tens or hundreds of mock interactions.
BTW, the example is written using mockito-scala as using the Java version of mockito would have yielded code much harder to read.
val resourceMock = mock[Resource[IO, Client[IO]]]
val clientMock = mock[Client[IO]]
val response: Response[IO] = Response(Status.Ok,
body = Stream("Mocked!!!").through(text.utf8Encode),
headers = Headers(`Content-Type`(MediaType.text.plain, Charset.`UTF-8`)))
clientMock.fetch[String](any[Request[IO]])(*) shouldAnswer { (_: Request[IO], f: Response[IO] => IO[String]) =>
f(response)
}
resourceMock.use[String](*)(*) shouldAnswer { (f: Client[IO] => IO[String]) =>
f(clientMock)
}
val data = new ExternalCall(resourceMock, Uri.unsafeFromString("http://www.example.com")).retrieveData
data.unsafeRunSync().right.value shouldBe "Mocked!!!"
You can easly mock Client using following snippet
import fs2.Stream
import org.http4s.Response
import org.http4s.client.Client
def httpClient(body: String): Client[IO] = Client.apply[IO] { _ =>
Resource.liftF(IO(Response[IO](body = Stream.emits(body.getBytes("UTF-8")))))
}
In order to have the client as resource you need to wrap it with IO and lift to Resource
Resource.liftF(IO(httpClient("body")))

Error when use akka http unmarshall entity as case class with default value

Error found when I send a http post request:
The request content was malformed: No usable value for gender Did
not find value which can be converted into java.lang.String
My request body:
{
"name":"test"
}
Route in my scala code:
path("test"){
(post(entity(as[People]) { req =>
val resp = queryData(req)
complete(resp.meta.getOrElse("statusCode", 200).asInstanceOf[Int] -> resp)
}))
} ~
Code for People:
case class People(name: String, gender: String = "male")
Why still get the malformed error ???
Even though you put a default value, the extraction of the Json will look for that field, and it is not present there, so it will fail.
(I am assuming you are using spray-json as it's the default one in akka-http)
In order to avoid the issue, while keeping it simple, I would recommend you to create a case class for the request to create people, which contains an Option[String] for that field, and you can then convert the PeopleCreateRequest to a People easily.
case class PeopleCreateRequest(name: String, gender: Option[String])
That will work nicely with the framework...
Alternatively, if you want to keep the design that way, you'll need to look into implementing your own JsonFormat[People] which will treat this value as optional but add a default value when missing.
Look into spray-json https://github.com/spray/spray-json#providing-jsonformats-for-other-types
But I imagine it would be something like:
implicit val peopleFormat = new RootJsonFormat[People] {
def read(json: JsValue): People = json match {
case JsArray(Seq(JsString(name), JsString(gender))) =>
People(name, gender)
case JsArray(Seq(JsString(name))) =>
People(name)
case _ => deserializationError("Missing fields")
}
def write(obj: People): JsValue = ???
}
I am normally using different JsonSupport, using circe, but hopefully this gives you direction to solve your issue

Add exception handling in http4s with rho

I'm using http4s & rho (mainly for Swagger integration)
My services are using this DAO object, that methods that can throw Exceptions (fail the Task)
case class BasicMatchDao() {
def readAll(): Task[List[BasicMatch]] = Task.fail(ActionNotImplemented("readAll"))
def read(id: String): Task[Option[BasicMatch]] = readQuery(id).option.transact(xa)
}
In my RhoService I can handle these like
private def exceptionToJson(t: Throwable):Json = Json.obj("error" -> t.getMessage.asJson)
val rhoService = new RhoService {
GET / path |>> { (request: Request) =>
Ok(dao.readAll.map(_.asJson)).handleWith {
case t:ActionNotImplemented => NotImplemented(exceptionToJson(t))
case t:Throwable => InternalServerError(exceptionToJson(t))
}
}
This way I make sure that whatever I return, it's always a Json
Since I don't want to pollute every RhoRoute with a similar errorhandling I want to do something which is possible with the default http4s.dsl, but I can't seem to get working with rho:
1. Create default error handler
e.g. add
...
Ok(dao.readAll.map(_.asJson)).handleWith(errorHandler)
...
private def errorHandler(): PartialFunction[Throwable, Task[Response]] = {
case t:ActionNotImplemented => NotImplemented(exceptionToJson(t))
case t:Throwable => InternalServerError(exceptionToJson(t))
}
This will fail because NotImplemented is not a Response (I can call .pure on these to make type checking work)
But then the code will compile, but I get this exception:
Cannot convert from fs2.Task[Product with Serializable]
to an Entity, because no EntityEncoder[fs2.Task[Product with
Serializable]] instance could be found.
Ok(dao.readAll.map(_.asJson)).handleWith(errorHandler)
2. Add errorhandler to each RhoRoute
After defining the rhoRoute I'd like to map over it and add the errorhandler to each route, so do something at the r that let's me add the 'handleWith' somewhere (below will not work)
new RhoService(rhoService.getRoutes.map(_.handleWith(errorHandler))
If I can't get this to work, I'll probably move back to the default dsl, but I really liked rho
So Part 1 is fixed for now. Defining the Task as Task[BaseResult] instead of Task[Response] will work
import org.http4s.rho.Result.BaseResult
val errorHandler: PartialFunction[Throwable, Task[BaseResult]] = {
case t:ActionNotImplemented => NotImplemented(exceptionToJson(t))
case t:Throwable => InternalServerError(exceptionToJson(t))
}
I'm looking into part 2 as well. All help is welcome :-)

Akka: How to wrap a message content into an HTTP response?

In my Akka-http route I get a specific message back and I want to wrap its content as error message like:
val response:Future[T] = (actor ? command).mapTo[T]
response match {
case err : Future[InvalidRequest] =>
HttpResponse(408, entity = err.map(_.toJson).????)
case r : Future[T] => r.map(_.toJson)
}
case class InvalidRequest(error:String)
implicit val invalidRequestFormat = jsonFormat1(InvalidRequest)
but that doesn't work. How can I map it as text in json format?
I think I can provide a generic solution for what it is you are trying to do. You can start by creating a method that returns a Route as follows:
def service[T:ClassTag](actor:ActorRef, command:Any)
(implicit timeout:Timeout, _marshaller: ToResponseMarshaller[T]):Route = {
val fut = (actor ? command).mapTo[ServiceResponse]
onComplete(fut){
case util.Success(ir:InvalidRequest) =>
complete(StatusCodes.BadRequest, ir)
case util.Success(t:T) =>
complete(t)
case util.Failure(ex) =>
complete(StatusCodes.InternalServerError )
}
}
This method fires a request to a supplied actor, via ask, and gets the Future representing the result. It then uses the onComplete directive to apply special handling to the InvalidResponse case. It's important here that you have an implicit ToResponseMarshaller[T] in scope as you will need that for the success case.
Then, let's say you had the following classes and formatters defined:
trait ServiceResponse
case class Foo(id:Int) extends ServiceResponse
implicit val fooFormat = jsonFormat1(Foo)
case class InvalidRequest(error:String) extends ServiceResponse
implicit val invalidRequestFormat = jsonFormat1(InvalidRequest)
You could use your new service method within your routing tree as follows:
val routes:Route = {
path("api" / "foo"){
get{
service[Foo](fooActor, FooActor.DoFoo)
}
}
}
The problem with your example is that you were not waiting for the completion of the Future before building out the response. You were trying to match on the underlying type of the Future, which is eliminated by erasure at runtime, so is not a good idea to try and match against in that way. You instead need to wait until it's completed and then see the type that is behind the Future.