SIRD routing and prefixes for dynamic urls

scala

(Iaco86) #1

Hi Play community,
I’m looking into solving a problem we have with routing in our scala play 2.5.x application.

Our app endpoints have this common dynamic prefix /v<versionInt>/<userId>/api.
The version is currently unused in the controllers, and the userId is extracted from the URL and propagated to all the controllers through action composition, so we don’t need to make it explicitly available to every controller.

Now, initially our routes file had a line like this for every controller:

GET     /v:version/:user/api/foo                  @api.foo.FooController.foo(version: Int, user: String)
POST    /v:version/:user/api/bar                  @api.bar.BarController.bar(version: Int, user: String)

But with around 40 endpoints the file is becoming hard to maintain.
So I started looking into SIRD router, and came up with something like this:

->      /v:version/:user/api                      api.ApiRouter

Now I am composing multiple routers inside the ApiRouter, and that works with some glitches, given the dynamic nature of the prefix: it allows me defining a router for different parts of the application, but I need to override the withPrefix function and still define the prefix in all the composed routers:

class ApiRouter @Inject()(fooRouter: FooApiRouter,
                          barRouter: BarApiRouter) extends SimpleRouter {
  private final val Prefix = "/v$version<[^/]+>/$user<[^/]+>/api"
  // looks like it won't work without this override
  override def withPrefix(prefix: String): Router = if (prefix == Prefix) this else super.withPrefix(prefix)

  override def routes: Routes = fooRouter.routes orElse barRouter.routes
}

private class FooApiRouter @Inject()(fooController: FooController) extends SimpleRouter {
  override def routes: Routes = {
    // it won't work without specifying the whole endpoint path
    case GET(p"/v$version/$user/api/foo") => fooController.foo
  }
}

private class BarApiRouter @Inject()(barController: BarController) extends SimpleRouter {
  override def routes: Routes = {
    // it won't work without specifying the whole endpoint path
    case GET(p"/v$version/$user/api/bar") => barController.bar
  }
}

I would like the ApiRouter to define the prefix common for all the Routers it composes - is there any way I can accomplish that using this approach? So that, for instance the BarApiRouter just has the route case GET(p"/bar") => barController.bar

Let me know if you want to know anything else!
Thanks!


(Iaco86) #2

I kinda found a solution to this and would like your feedback…

So, I started implementing a new Router, the DynamicRouter. It is based on the SimpleRouter but instead of matching prefixes with startsWith, uses the regexp built in the routes file. It also adds the parsed dynamic parameters as request tags (I’d love to hear if you have another idea to propagate these dynamic values):

abstract class DynamicRouter extends Router { self =>
  /** A sequence of String representing the group names parsed by the Dynamic Router.
    * They must appear in the same order and have the same length of the groups defined in the routes prefix.
    */
  val groupNames: Seq[String]

  override def documentation: Seq[(String, String, String)] = Seq.empty

  override def withPrefix(prefix: String): Router = {
    if (prefix == "/") {
      self
    } else {
      new Router {
        override def routes: Routes = {
          val prefixed: PartialFunction[RequestHeader, RequestHeader] = {
            case rh: RequestHeader if rh.path.matches(s"$prefix.*") =>
              val regexMatch = prefix.r(groupNames: _*).findFirstMatchIn(rh.path).get
              val tags = regexMatch.groupNames.zip(regexMatch.subgroups).toMap
              rh.copy(path = rh.path.split(prefix).tail.head, tags = tags)
          }
          Function.unlift(prefixed.lift.andThen(_.flatMap(self.routes.lift)))
        }
        override def withPrefix(prefix: String): Router = self.withPrefix(prefix)
        override def documentation: Seq[(String, String, String)] = self.documentation
      }
    }
  }
}

My implementation would be:

class ApiRouter @Inject()(fooRouter: FooApiRouter,
                          barRouter: BarApiRouter) extends DynamicRouter {
  override val groupNames: Seq[String] = Seq("version", "user")
  override val routes: Routes = fooRouter.routes orElse barRouter.routes
}

private class FooApiRouter @Inject()(fooController: FooController) extends SimpleRouter {
  override def routes: Routes = {
    case GET(p"/foo") => fooController.foo
  }
}

private class BarApiRouter @Inject()(barController: BarController) extends SimpleRouter {
  override def routes: Routes = {
    // Is there a way this can be done better? A partial function would be better here...
    case rh: RequestHeader => rh match {
        case GET(p"/bar") => barController.bar(rh.tags("version"))
    }
  }
}

The routes file would then contain this line:

->      /v([^/]+)/([^/]+)/api                       api.ApiRouter

Do you guys see any problem in this? Have any idea on how to make this better?

Thanks!