Nesting CRUD paths in akka-http directives - scala

I am just starting with Scala and Akka. I am writing a small REST service. I am trying to create following paths:
GET localhost:8080/api/my-service (return collection of resources)
GET localhost:8080/api/my-service/6 (return resource with specified id)
POST localhost:8080/api/my-service (create new resource with data in body)
UPDATE localhost:8080/api/my-service/4 (update requested resource with data in body)
DELETE localhost:8080/api/my-service/5 (deletes resource with specified id)
I have managed to create nested paths, but however only GET for fetching the collections (first example in bullet points) is returning the results. Example GET with id in the path is returning The requested resource could not be found. I have tried many different variations, but nothing seems to work.
Here is excerpt of my routes:
val routes = {
logRequestResult("my-service-api") {
pathPrefix("api") {
path("my-service") {
get {
pathEndOrSingleSlash {
complete("end of the story")
} ~
pathPrefix(IntNumber) { id =>
complete("Id: %d".format(id))
}
} ~
(post & pathEndOrSingleSlash & entity(as[MyResource])) { myResourceBody =>
// do something ...
}
}
}
}
}
I have already checked many solutions on the web and some tests from Akka itself, but somehow I am missing here something.

I found a solution. Problem was in path("my-service") part. I've changed it to pathPrefix("my-service") and now both GET paths are working.
So the correct route setup should look something like this.
val routes = {
logRequestResult("my-service-api") {
pathPrefix("api") {
pathPrefix("my-service") {
get {
pathEndOrSingleSlash {
complete("end of the story")
} ~
pathPrefix(IntNumber) { id =>
complete("Id: %d".format(id))
}
} ~
(post & pathEndOrSingleSlash & entity(as[MyResource])) { myResourceBody =>
// do something ...
}
}
}
}
}

Related

A little complex path matching in Akka HTTP

I'm new to Akka HTTP, and trying to write my first API. The routing DSL seems a little confusing.
I managed to match the following:
/channel
/channel/channelName
But now I need to match the following:
/channel/channelName/channelAction
And I can't get it to work.
I currently have:
private val routes: Route =
path("channel") {
get {
reportAllChannelsStatus()
}
} ~
pathPrefix("channel" / Remaining) { channelName =>
get {
singleChannelRequest(channelName, status)
} ~
post {
entity(as[ChannelRequest]) { request =>
singleChannelRequest(channelName, request.channelAction)
}
}
} ~
completeWith404()
I want to add get and post for /channel/channelName/channelAction
Any idea how is this done? (extract both channelName and channelAction)
You can match
path("channel" / Segment / Segment){
(channelName, channelAction) => ...
}
Be aware that for different types of arguments, you'll have to match different things. Segment is for String, IntNumber would be for Int ...

Nesting authorize directives in spray

It seems it is impossible to nest authorized directives in spray due to this line: https://github.com/spray/spray/blob/76ab89c25ce6d4ff2c4b286efcc92ee02ced6eff/spray-routing/src/main/scala/spray/routing/directives/SecurityDirectives.scala#L55
I'm referring to doing things like this:
val route = {
...
authorize(userIsAdmin) {
path("generic" / "admin" / "stuff") { ... } ~
path("users" / Segment) { u =>
authorize(canModifyUser) {
...
}
} ~
path("quotas") {
authorize(canModifyQuotas) {
...
}
}
}
}
One could of course refactor this to include userIsAdmin into the canModifyUser and canModifyQuota checks, but with orthogonal access rights, that could get out of hand fast.
What is the reason for that line? It doesn't seem logical to me why we are cancelling any further authorization failures.
Full disclosure: The route will actually get rejected if one of the nested checks fail, but it will give a 404 error (EmptyRejection) instead of the expected AuthorizationFailedRejection.

akka-http: How to set response headers

I've a route as follows:
val route = {
logRequestResult("user-service") {
pathPrefix("user") {
get {
respondWithHeader(RawHeader("Content-Type", "application/json")) {
parameters("firstName".?, "lastName".?).as(Name) { name =>
findUserByName(name) match {
case Left(users) => complete(users)
case Right(error) => complete(error)
}
}
}
} ~
(put & entity(as[User])) { user =>
complete(Created -> s"Hello ${user.firstName} ${user.lastName}")
} ~
(post & entity(as[User])) { user =>
complete(s"Hello ${user.firstName} ${user.lastName}")
} ~
(delete & path(Segment)) { userId =>
complete(s"Hello $userId")
}
}
}
}
The content type of my response should always be application/json as I've it set for the get request. However, what I'm getting in my tests is text/plain. How do I set the content type correctly in the response?
On a side note, the akka-http documentation is one of the most worthless piece of garbage I've ever seen. Almost every link to example code is broken and their explanations merely state the obvious. Javadoc has no code example and I couldn't find their codebase on Github so learning from their unit tests is also out of the question.
I found this one post that says "In spray/akka-http some headers are treated specially". Apparently, content type is one of those and hence can't be set as in my code above. One has to instead create an HttpEntity with the desired content type and response body. With that knowledge, when I changed the get directive as follows, it worked.
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.model.MediaTypes.`application/json`
get {
parameters("firstName".?, "lastName".?).as(Name) { name =>
findUserByName(name) match {
case Left(users) => complete(users)
case Right(error) => complete(error._1, HttpEntity(`application/json`, error._2))
}
}
}

How to use string directive extractor in a nested route in Spray

Answering my own question here because this took me over a day to figure out and it was a really simple gotcha that I think others might run into.
While working on a RESTful-esk service I'm creating using spray, I wanted to match routes that had an alphanumeric id as part of the path. This is what I originally started out with:
case class APIPagination(val page: Option[Int], val perPage: Option[Int])
get {
pathPrefix("v0" / "things") {
pathEndOrSingleSlash {
parameters('page ? 0, 'perPage ? 10).as(APIPagination) { pagination =>
respondWithMediaType(`application/json`) {
complete("things")
}
}
} ~
path(Segment) { thingStringId =>
pathEnd {
complete(thingStringId)
} ~
pathSuffix("subthings") {
pathEndOrSingleSlash {
complete("subthings")
}
} ~
pathSuffix("othersubthings") {
pathEndOrSingleSlash {
complete("othersubthings")
}
}
}
}
} ~ //more routes...
And this has no issue compiling, however when using scalatest to verify that the routing structure is correct, I was surprised to find this type of output:
"ThingServiceTests:"
"Thing Service Routes should not reject:"
- should /v0/things
- should /v0/things/thingId
- should /v0/things/thingId/subthings *** FAILED ***
Request was not handled (RouteTest.scala:64)
- should /v0/things/thingId/othersubthings *** FAILED ***
Request was not handled (RouteTest.scala:64)
What's wrong with my route?
I looked at a number of resources, like this SO Question and this blog post but couldn't seem to find anything about using string Id's as a toplevel part of a route structure. I looked through the spray scaladoc as well as beat my head against the documentation on Path matchers for a while before spotting this important test (duplicated below):
"pathPrefix(Segment)" should {
val test = testFor(pathPrefix(Segment) { echoCaptureAndUnmatchedPath })
"accept [/abc]" in test("abc:")
"accept [/abc/]" in test("abc:/")
"accept [/abc/def]" in test("abc:/def")
"reject [/]" in test()
}
This tipped me off to a couple things. That I should try out using pathPrefix instead of path. So I changed my route to look like this:
get {
pathPrefix("v0" / "things") {
pathEndOrSingleSlash {
parameters('page ? 0, 'perPage ? 10).as(APIPagination) { pagination =>
respondWithMediaType(`application/json`) {
listThings(pagination)
}
}
} ~
pathPrefix(Segment) { thingStringId =>
pathEnd {
showThing(thingStringId)
} ~
pathPrefix("subthings") {
pathEndOrSingleSlash {
listSubThingsForMasterThing(thingStringId)
}
} ~
pathPrefix("othersubthings") {
pathEndOrSingleSlash {
listOtherSubThingsForMasterThing(thingStringId)
}
}
}
}
} ~
And was happy to get all my tests passing and the route structure working properly. then I update it to use a Regex matcher instead:
pathPrefix(new scala.util.matching.Regex("[a-zA-Z0-9]*")) { thingStringId =>
and decided to post on SO for anyone else who runs into a similar issue. As jrudolph points out in the comments, this is because Segment is expecting to match <Segment><PathEnd> and not to be used in the middle of a path. Which is what pathPrefix is more useful for

How can I parse out get request parameters in spray-routing?

This is what the section of code looks like
get{
respondWithMediaType(MediaTypes.`application/json`){
entity(as[HttpRequest]){
obj => complete{
println(obj)
"ok"
}
}
}
}~
I can map the request to a spray.http.HttpRequest object and I can extract the uri from this object but I imagine there is an easier way to parse out the parameters in a get request than doing it manually.
For example if my get request is
http://localhost:8080/url?id=23434&age=24
I want to be able to get id and age out of this request
Actually you can do this much much better. In routing there are two directives: parameter and parameters, I guess the difference is clear, you can also use some modifiers: ! and ?. In case of !, it means that this parameter must be provided or the request is going to be rejected and ? returns an option, so you can provide a default parameter in this case. Example:
val route: Route = {
(path("search") & get) {
parameter("q"!) { query =>
....
}
}
}
val route: Route = {
(path("search") & get) {
parameters("q"!, "filter" ? "all") { (query, filter) =>
...
}
}
}