2

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?

EdgeCaseBerg
  • 2,761
  • 1
  • 23
  • 39

1 Answers1

5

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

Community
  • 1
  • 1
EdgeCaseBerg
  • 2,761
  • 1
  • 23
  • 39
  • If `Segment` would match literal slashes it would be a bug. What you probably experienced is that `path(Segment)` will only match `/`, i.e. a single segment at the end. You need to use `pathPrefix(Segment)` if you want to match a single segment in the middle of the path. The test you quote shows that everything is working as expected: in each of those examples only `abc` is extracted and the rest of the path remains in the `unmatchedPath`. – jrudolph Aug 03 '15 at 16:54
  • Or explaining it with your own example: `/v0/things/thingId/subthings` failed to match because `path(Segment)` already expected the end of the path after `thingId` not because it consumed all of the path. – jrudolph Aug 03 '15 at 16:56
  • It would be good if the documentation on spray's website was explicit about matching `` because it is not obvious at all that you need to use pathPrefix and not path. Or at least it wasn't to me. – EdgeCaseBerg Aug 03 '15 at 17:04
  • @jrudolph I've updated the answer, did I describe the reasoning well? I would like to make sure that other people aren't bit by this (feel free to edit it yourself if you can) – EdgeCaseBerg Aug 03 '15 at 17:07
  • Almost correct :) Actually, `` is matched by `path`. `path(x)` matches `` while `pathPrefix(x)` only matches ``. – jrudolph Aug 03 '15 at 19:52
  • 1
    Btw. by using `pathSuffix` you now match more than needed. E.g. `/v0/things//abc/def/ghi/subthings` will now be matched as well. The basic rule is: use `pathPrefix` for all outer matching and `path`, `pathEnd`, `pathSingleSlash`, or `pathEndOrSingleSlash` only for the most nested ones. – jrudolph Aug 03 '15 at 19:56
  • Thanks @jrudolph about the `pathSuffix`. I've updated the code. – EdgeCaseBerg Aug 03 '15 at 21:05