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