I'm trying to read a query parameter in an REST Api I'm building with Akka-http and having trouble unmarshalling it to a domain object. This is my route definition:
override lazy val getManyRoute: Route = (pathEndOrSingleSlash & get & parameters("userId".as[UserId].?)) { maybeUserId =>
complete {
eventService.getEventsBy(maybeUserId)
}
}
Everything compiles fine, so I am assuming the necessary unmarshalling implicits are in scope. Using curl to test the endpoint, I get the following error:
➜ ~ curl "localhost:8080/events?userId=6be0f45e-084e-41db-9e5b-e6bc8d208b54"
The query parameter 'userId' was malformed:
expected whitespace or eof got 'be0f45...' (line 1, column 2)%
It seems like it's reading just the first character ('6') from the UUID, and for some reason is not including the rest of the string in the parameter. However, if I change the Route definition to read a String and manually "unmarshall" it, it works fine. Something like the following:
override lazy val getManyRoute: Route = (pathEndOrSingleSlash & get & parameters("userId".?)) { maybeUserId =>
// ^ no more unmarshalling
complete {
eventService.getEventsBy(maybeUserId.map(functionToUnmarshallStringToUserId))
}
}
Any ideas? Is there something I'm missing in the original route definition?
EDIT:
I have narrowed down the problem using the following change to the route, trying to see exactly what Unmarshaller was being used:
override lazy val getManyRoute: Route = (pathEndOrSingleSlash & get & parameters("userId".?)) { userId =>
complete {
val um = implicitly[Unmarshaller[String, UserId]]
val t = userId map {
um.apply
}
val fuid = t.fold(Future.successful[Option[UserId]](None)) { _.map { a => Some(a) } }
fuid flatMap { eventService.getEventsBy }
}
}
The result was the same, but I got a bit more context in the error:
io.circe.ParsingFailure: expected whitespace or eof got 'be0f45...' (line 1, column 2)
The problem is that my chain of implicits has eventually led to circe being responsible for unmarshalling the query parameter. Because circe is a json library, it expects strings to be in quotes, and so the following curl request works as expected:
curl "localhost:8080/events/?userId=%226be0f45e-084e-41db-9e5b-e6bc8d208b54%22"
# note the URL-escaped quotes (%22) around the id
So now the problem seems to be that I need multiple unmarshallers for the same class in scope, one to unmarshall directly from plain strings for query parameters, and one to unmarshall from json payloads from request entities. Is there a way to do that using implicits? As suggested in comments, I could use “userId”.as(uidUnmarshaller), but I would prefer to take advantage of implicits if possible.
Related
I am writing unit tests for Play application using Scalamock and Scalatest.
My original code looks like:
// Here ws is an injected WSClient
val req = Json.toJson(someRequestObject)
val resp: Future[WSResponse] = ws.url(remoteURL).post(Json.toJson(req))
In a part I have to mock external calls to a web service, which I am trying to do using scalamock:
ws = stub[WSClient]
wsReq = stub[WSRequest]
wsResp = stub[WSResponse]
ws.url _ when(*) returns wsReq
wsReq.withRequestTimeout _ when(*) returns wsReq
(wsReq.post (_: java.io.File)).when(*) returns Future(wsResp)
I am successfully able to mock post requests using a file, but I cannot mock post requests using JSON.
I tried putting stub function references separately like:
val f: StubFunction1[java.io.File, Future[WSResponse]] = wsReq.post (_: java.io.File)
val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)
I get the compile error for second line: Unable to resolve overloaded method post
What am I missing here? Why cannot I mock one overloaded method but not the other one?
play.api.libs.ws.WSRequest has two post methods (https://www.playframework.com/documentation/2.4.x/api/scala/index.html#play.api.libs.ws.WSRequest), taking:
File
T (where T has an implicit bounds on Writeable)
The compiler is failing because you are trying to calling post with a single parameter, which only matches version 1. However, JsValue cannot be substituted with File.
You actually want to call the 2nd version, but this is a curried method that takes two sets of parameters (albeit the 2nd are implicit). Therefore you need to explicitly provide the mock value that you expect for the implicit, i.e.
val j: StubFunction1[JsValue, Future[WSResponse]] = wsReq.post(_: JsValue)(implicitly[Writeable[JsValue]])
Therefore a working solution would be:
(wsReq.post(_)(_)).when(*) returns Future(wsResp)
Old answer:
WSRequest provides 4 overloads of post method (https://www.playframework.com/documentation/2.5.8/api/java/play/libs/ws/WSRequest.html), taking:
String
JsonNode
InputStream
File
You can mock with a File because it matches overload 4, but JsValue does not match (this is part of the Play JSON model, whereas JsonNode is part of the Jackson JSON model). If you convert to a String or JsonNode, then it will resolve the correct overload and compile.
My best guess is that your WSRequest is actually a play.libs.ws.WSRequest which is part of the Java API, instead you should use play.api.libs.ws.WSRequest which is the Scala API.
The method WSRequest.post exists and BodyWritable[JsValue] is implicitly provided by WSBodyWritables in the Scala API but not in the Java API.
Another cause could be that your JsValue is not a play.api.libs.json.JsValue but something else (e.g. spray.json.JsValue).
I'll quote an example where I have successfully achieved what you are trying to do, the main difference is that I used mock instead of stub.
The important part is:
val ws = mock[WSClient]
val responseBody = "{...}"
...
"availableBooks" should {
"retrieve available books" in {
val expectedBooks = "BTC_DASH ETH_DASH USDT_LTC BNB_LTC".split(" ").map(Book.fromString).map(_.get).toList
val request = mock[WSRequest]
val response = mock[WSResponse]
val json = Json.parse(responseBody)
when(ws.url(anyString)).thenReturn(request)
when(response.status).thenReturn(200)
when(response.json).thenReturn(json)
when(request.get()).thenReturn(Future.successful(response))
whenReady(service.availableBooks()) { books =>
books.size mustEqual expectedBooks.size
books.sortBy(_.string) mustEqual expectedBooks.sortBy(_.string)
}
}
}
An you could see the complete test at: BinanceServiceSpec
I guess it should work fine, if you mock a response that is JsValue.
when(wsReq.post(Json.parse("""{...json request...}"""))).thenReturn(Future(wsResp))
Here Json.parse returns JsValue. Yo should pass the json string that you expect in the request body.
I am trying to set up a basic server using spray routing and would like to use a type to represent one of the forms PUTs.
I am using the formFields('some, 'fields).as(Thing) notation as described in the documentation
At first glance I assumed .as was taking taking a type and instantiating an object of that type using the input defined in fromFields.
My example was constructed like this:
type UpdatePasswordRequest = Tuple3[String, String, String]
startServer(interface, port) {
path("user" / IntNumber) { userEmail =>
put {
formFields('password, 'password2, 'key).as(UpdatePasswordRequest) { req =>
//...
}
}
}
}
After getting the rather confused error that it couldn't resolve the symbol UpdatePasswordRequest I changed it from a type to a val and it at least compiles.
What is happening here? Why is it expecting a val? Can you even pass a type as a parameter to a function?
These Json serializers in Play with Scala are driving me nuts.
I have read dozens of posts and the tutorials and the documentation. Tried four different ways of implementing Reads / Writes / Format overrides and all to no avail.
So I backed off the custom type and decided to go uber simple:
def suggest = Action(parse.json) {
request =>
request.body.validate[(String, String)].map {
case (suggestion, categories) => Ok("You suggested " + suggestion + " for categories " + categories)
}.recoverTotal {
e => BadRequest(JsError.toFlatJson(e))
}
}
And the error comes back as noted in the subject.
Do I really need to provide a custom Reads / Writes / Format implementation for such a basic body?
A sample input body could be:
{"suggestion":"add generics", "categories":"request;language;updates"}
What simple thing am I missing?
Play! gives you a LOT of ways to work with Json. From the looks of your code, you're going down the Tuple road. Which essentially lets you convert Json into a Tuple. You're also using 'reads' which has the downside that you don't get precise error reporting (ie: if invalid json was passed you would know its invalid... but you wouldn't necessarily know why it was invalid). If you wanted more error handling then you need to start using the 'validate' method (details here: http://www.playframework.com/documentation/2.1.1/ScalaJsonCombinators).
Another way you could go is to map Json to case classes doing something like:
import play.api.libs.json._
case class MyClass(
suggestion: String,
categories: String
)
object MyClass {
implicit val readsMyClass: Reads[MyClass] = new Reads[MyClass] {
def reads(json: JsValue): JsResult[MyClass] = {
for {
suggestion <- (json \ "suggestion").validate[String]
categories <- (json \ "categories").validate[String]
} yield MyClass(json,categories)
}
}
}
This seems like a lot of code so Play 2.1 introduced Json 'Inception' which I have yet to try (http://www.playframework.com/documentation/2.1.1/ScalaJsonInception).
Finally, if you want the Json validation but don't necessary need to marshall/unmarshall case classes, then you can try the 'coast-to-coast' method. This will keep all your json as JsObject types but still give you validation. Sample code here: https://github.com/mandubian/play2-json-demo/blob/master/json-coast-to-coast/app/controllers/Application.scala
Hope this helps.
So I added this:
implicit val rds = (
(__ \ 'suggestion).read[String] and
(__ \ 'categories).read[String]
) tupled
And that seems to work.
Curious, though, is this really the best way to do this? It seems like a LOT of code if you have many many types to serialize / deserialize.
I am trying to consume an external Java Service API in Scala by writing a wrapper around it. Each API call has a Request class and Response class, as well as a factory method to setup a call in the underlaying java client i.e.
val caller = serviceClient.newDoSomething()
val request = DoSomethingRequest(...)
val response = caller.call(request)
where response is of type DoSomethingResponse.
This pattern is repeated for a bunch of different operations only varying in the names of the request/response classes and the factory method. I would like to DRY up the code and use the type inference, while noting a key observation that java method
caller.call is of type Request => Response.
Given that I've attempted the following:
class ServiceAPI {
val client = Service.getDefaultClient
def serviceCall[Req, Resp](auth: String, caller: Req => Resp)
(func: Req => Unit)(implicit m:Manifest[Req]):Resp = {
val request = m.erasure.newInstance.asInstanceOf[Req]
request.setAuth(auth)
func(request)
caller(request)
}
}
What I would like to end up with is a consumable scala API that can be invoked like so:
serviceCall(myAuth, client.newEventCall.call(_)) {
_.setFoo("foo") // On request
_.setBar(2) // On request
}
I was hoping that by providing the partial function client.newEventCall.call(_) Scala would be able to fully infer Req and Resp from it and then properly invoke the rest.
However, this fails with:
error: missing parameter type for expanded function ((x$4) => client.newEventCall.call(x$4))
Obviously I can provide the type manually via:
serviceCall(myAuth, client.newEventCall.call(_:EventRequest)) { ... }
but I would like to avoid that. Any ideas?
Also bonus points if this can be further simplified.
I came across this issue already a couple of times and I wonder what the Lift-way is to perform such an action. Of course I could do my own error handling etc. but basically I wonder how I can turn a Box[Elem] into a LiftResponse. Ideally an XmlResponse of course.
The scenario is that I am using the RestHelper for an API and I have a function that returns me a Box[Elem]. I would like to make use of the async support.
The error I get is that Box[Elem] (or Box[NodeSeq], Box[Node]) cannot be converted to LiftResponse. However the exact same code without using RestContinuation works.
Note: I do not want Lift to do any template processing logic. Just output the XML the same way it would happen without using RestContinuation.
val userId = S.param("userId") map { _.toInt }
RestContinuation.async {
reply => {
reply(
for {
user <- userRepo.select(userId) ?~ "No such user." ~> 404
} yield {
<user>
<name>{user.name}</name>
</user>
}
)
}
}
I think there is an implicit declaration missing. You can bring this implicit (implicit def canNodeToResponse(in: Box[Seq[Node]]): LiftResponse in scope by mixing-in the trait XMLApiHelper in the surrounding class.