Ákos Vandra-Meyer
Ákos Vandra-Meyer

Reputation: 2166

Spray REST routing - overly verbose

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

Answers (3)

Eric Woods
Eric Woods

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

rahilb
rahilb

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

Nyavro
Nyavro

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

Related Questions