Reputation: 2166
Can anyone provide some good pointers on how to structure the routing in spray? My routes got extremely verbose, and even IDEA got very slow (5-10 seconds for autocomplete) when editing the file that contains the routing....
pathPrefix("customers") {
pathEnd {
get {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.getCustomers
}
}
}
}
}
} ~
path(IntNumber) { id =>
post {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
entity(as[Customer]) { c =>
complete {
m.updateCustomer(c).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
}
}
} ~
delete {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.deleteCustomer(id).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
}
} ~
path("new") {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
post {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
entity(as[Customer]) { c =>
complete {
m.insertCustomer(c).handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
}
}
}
}
} ~
pathPrefix("groups") {
pathEnd {
get {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.getGroups
}
}
}
}
}
} ~
pathPrefix(IntNumber) { groupId =>
pathEnd {
complete {
m.getGroupById(groupId)
}
} ~
path("users") {
complete {
m.getGroupById(groupId).flatMap { groupO =>
groupO.map { group =>
m.getUsers(group)
}.getOrElse(Future.successful(Seq()))
}
}
}
} ~
pathPrefix(Segment) { groupName =>
pathEnd {
complete {
m.getGroupByName(groupName)
}
} ~
path("users") {
complete {
m.getGroupByName(groupName).flatMap { groupO =>
groupO.map { group =>
m.getUsers(group)
}.getOrElse(Future.successful(Seq()))
}
}
}
}
} ~
pathPrefix("users") {
path("me") {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
withSessionKey[String]("userId") { uid =>
complete {
m.getUserById(uid.toInt).map(_.get)
}
}
}
} ~
path("new") {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized orElse RejectionHandler.Default)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
entity(as[NewUser]) { r =>
complete {
m.addUser(r).handleCompletionWith{ _ => siblingWorkers ! Push("users", None)}
}
}
}
}
}
} ~
pathPrefix(IntNumber) { uid =>
pathEnd {
get {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized orElse RejectionHandler.Default)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.getUserById(uid)
}
}
}
}
} ~
post {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized orElse RejectionHandler.Default)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
entity(as[User]) { u =>
complete {
m.updateUser(u).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleCompletionWith { _ => siblingWorkers ! Push("users", None) }
}
}
}
}
}
} ~
delete {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized orElse RejectionHandler.Default)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.deleteUserById(uid).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleCompletionWith{ _ => siblingWorkers ! Push("groups", None)}
}
}
}
}
}
} ~
pathPrefix("groups") {
path("new") {
entity(as[NewUserGroup]) { g =>
complete {
m.addUserGroup(UserGroup(uid, g.id)).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleCompletionWith{ _ => siblingWorkers ! Push("groups", None)}
}
}
} ~
pathEnd {
get {
complete {
m.getUserGroups(uid)
}
} ~
post {
entity(as[Seq[Int]]) { groups =>
complete {
m.setUserGroups(uid, groups).map {
case Some(x) if x < groups.length => StatusCodes.UnprocessableEntity
case Some(x) if x == groups.length => StatusCodes.OK
case _ => StatusCodes.InternalServerError
}.handleCompletionWith{ _ => siblingWorkers ! Push("users", None)}
}
}
}
}
}
} ~
pathEnd {
get {
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
complete {
m.getUsers
}
}
}
}
}
}
Upvotes: 2
Views: 123
Reputation: 106
In addition to what others have mentioned already, I'd take advantage of the capability to compose directives and create custom directives to cut down on repetitive code and make the route structure a little more readable.
One thing that's repeated frequently in the code is this:
handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized orElse RejectionHandler.Default)) {
withSessionKey[String]("groups") { g =>
validate(g.contains("admin"), "Not authorized") {
…
}
}
}
You might consider factoring that out into a custom directive. Something like this (untested):
def handleSessionKeyValidation(key: String, requiredValue: String) = {
val rejectionHandlers = handleRejections(RejectionHandler.apply(handleMissingAuthSessionKey orElse handleValidationErrorAsUnauthorized))
val sessionKeyDir = withSessionKey[String](key)
(rejectionHandlers & sessionKeyDir).flatMap[HNil](value =>
if (value.contains(requiredValue)) pass
else reject(AuthorizationFailedRejection)
)
}
Now, start combining your HTTP method and path directives and also use the custom directive, and the first part of your route may look something more like this:
(get & path("customers") & pathEnd) {
handleSessionKeyValidation("groups", "admin"){
complete{
m.getCustomers
}
}
} ~
(post & path("customers" / IntNumber)) { id =>
handleSessionKeyValidation("groups", "admin"){
entity(as[Customer]) { c =>
complete {
m.updateCustomer(c).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
} ~
(delete & path("customers" / IntNumber)) { id =>
handleSessionKeyValidation("groups", "admin") {
complete {
m.deleteCustomer(id).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
} ~
(post & path("customers" / "new")) {
handleSessionKeyValidation("groups", "admin"){
entity(as[Customer]) { c =>
complete {
m.insertCustomer(c).handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
}
Even in the example above, usage of the handleSessionKeyValidation directive is somewhat repetitive. We could cut down on the repetition by increasing the scope of handleSessionKeyValidation and wrapping a larger portion of the route. Something like this.
pathPrefixTest("customers"){
handleSessionKeyValidation("groups", "admin") {
(get & path("customers") & pathEnd) {
complete{
m.getCustomers
}
} ~
(post & path("customers" / IntNumber)) { id =>
entity(as[Customer]) { c =>
complete {
m.updateCustomer(c).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
} ~
(delete & path("customers" / IntNumber)) { id =>
complete {
m.deleteCustomer(id).map {
case 0 => StatusCodes.UnprocessableEntity
case 1 => StatusCodes.Accepted
case _ => StatusCodes.InternalServerError
}.handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
} ~
(post & path("customers" / "new")) {
entity(as[Customer]) { c =>
complete {
m.insertCustomer(c).handleSuccessWith { case _ =>
siblingWorkers ! Push("customers", None)
}
}
}
}
}
} ~
Upvotes: 1
Reputation: 716
You can split your resources into separated traits, for example UserRoutes.scala
, ContentRoutes.scala
, AdminRoutes.scala
and have them all extend HttpService
. You can now have a new class, that forms a composite of all your routes and extends HttpServiceActor
the composite class can chain your split routes using the ~
operator. E.g. val routes = userRoutes.routes ~ adminRoutes.routes ~ contentRoutes.routes
. It also gives you a nice place to inject dependencies.
Upvotes: 1
Reputation: 8866
You can split your routing into several routes. In your case: customers, groups, users may be extracted to corresponding routes.
class CustomersRoute extends ... {
def route: Route = pathPrefix("customers") {
...
}
}
Then you combine them:
val routes = pathPrefix("v1") {
customers.route ~
groups.route ~
users.route
}
Upvotes: 1