A little complex path matching in Akka HTTP - scala

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 ...

Related

Nesting CRUD paths in akka-http directives

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 ...
}
}
}
}
}

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) =>
...
}
}
}

How to match specific accept headers in a route?

I want to create a route that matches only if the client sends a specific Accept header. I use Spray 1.2-20130822.
I'd like to get the route working:
def receive = runRoute {
get {
path("") {
accept("application/json") {
complete(...)
}
}
}
}
Here I found a spec using an accept() function, but I can't figure out what to import in my Spray-Handler to make it work as directive. Also, I did not find other doc on header directives but these stubs.
I would do this way:
def acceptOnly(mr: MediaRange*): Directive0 =
extract(_.request.headers).flatMap[HNil] {
case headers if headers.contains(Accept(mr)) ⇒ pass
case _ ⇒ reject(MalformedHeaderRejection("Accept", s"Only the following media types are supported: ${mr.mkString(", ")}"))
} & cancelAllRejections(ofType[MalformedHeaderRejection])
Then just wrap your root:
path("") {
get {
acceptOnly(`application/json`) {
session { creds ⇒
complete(html.page(creds))
}
}
}
}
And by the way the latest spray 1.2 nightly is 1.2-20130928 if you can, update it
There is no pre-defined directive called accept directive. You can see the full list of available directives here.
However, you can use the headerValueByName directive to make a custom directive that does what you desire:
def accept(required: String) = headerValueByName("Accept").flatMap {
case actual if actual.split(",").contains(required) => pass
case _ => reject(MalformedHeaderRejection("Accept", "Accept must be equal to " + required))
}
Put this code in scope of your spray Route, then just use as you have shown in your question.