Can Spray.io routes be split into multiple "Controllers"?
Asked Answered
C

3

18

I haven't found a solid example or structure to splitting up Spray.io routes into multiple files. I am finding that the current structure of my routes are going to become very cumbersome, and it would be nice to abstract them into different "Controllers" for a very simple REST API app.

Docs don't seem to help too much: http://spray.io/documentation/spray-routing/key-concepts/directives/#directives

Here's what I have so far:

class AccountServiceActor extends Actor with AccountService {

  def actorRefFactory = context

  def receive = handleTimeouts orElse runRoute(demoRoute)

  def handleTimeouts: Receive = {
    case Timeout(x: HttpRequest) =>
      sender ! HttpResponse(StatusCodes.InternalServerError, "Request timed out.")
  }
}


// this trait defines our service behavior independently from the service actor
trait AccountService extends HttpService {

  val demoRoute = {
    get {
      path("") {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete(index)
        }
      } ~
      path("ping") {
        complete("PONG!")
      } ~
      path("timeout") { ctx =>
        // we simply let the request drop to provoke a timeout
      } ~
      path("crash") { ctx =>
        throw new RuntimeException("crash boom bang")
      } ~
      path("fail") {
        failWith(new RuntimeException("aaaahhh"))
      } ~
      path("riaktestsetup") {
        Test.setupTestData
        complete("SETUP!")
      } ~
      path("riaktestfetch" / Rest) { id =>
        complete(Test.read(id))
      }
    }
  }
}

Thanks for help on this!

Chromogen answered 1/2, 2013 at 19:22 Comment(0)
E
14

You can combine routes from different "Controllers" using ~ combinator.

class AccountServiceActor extends Actor with HttpService {

  def actorRefFactory = context

  def receive = handleTimeouts orElse runRoute(
  new AccountService1.accountService1 ~  new AccountService2.accountService2)

  def handleTimeouts: Receive = {
    case Timeout(x: HttpRequest) =>
      sender ! HttpResponse(StatusCodes.InternalServerError, "Request timed out.")
  }
}



class AccountService1 extends HttpService {

  val accountService1 = {
    get {
      path("") {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete(index)
        }
      }
    }
}


class AccountService2 extends HttpService {

  val accountService2 = {
    get {
      path("someotherpath") {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete(index)
        }
      }
    }
}
Erigena answered 1/2, 2013 at 19:55 Comment(4)
Looks like that does the trick. I wonder if I can compose some sort of implicit that can combine them automatically instead of manually writing service1 ~ service2 ~ service3. Thanks!Chromogen
Hmmm deselected it since it looks like it creates some sort of inheritance issue. type arguments [com.threetierlogic.AccountServ ice.AccountServiceActor] do not conform to method apply's type parameter bounds [T <: akka.actor.Actor]Chromogen
Ok made some progress with case class Base(actorRefFactory: ActorRefFactory) extends HttpService { Now the issue is HTTP requests fail because of the following: Cannot dispatch HttpResponse as response (part) for GET request to '/ ' since current response state is 'Completed' but should be 'Uncompleted'Chromogen
For some reason the classes I create by extending HttpService don't compile, they say: needs to be abstract, since method actorRefFactory in trait HttpService of type => akka.actor.ActorRefFactory is not defined class MyRouteRoute extends HttpService{ ^Precession
D
33

I personally use this for large APIs:

class ApiActor extends Actor with Api {
  override val actorRefFactory: ActorRefFactory = context

  def receive = runRoute(route)
}

/**
 * API endpoints
 *
 * Individual APIs are created in traits that are mixed here
 */
trait Api extends ApiService
  with AccountApi with SessionApi
  with ContactsApi with GroupsApi
  with GroupMessagesApi with OneToOneMessagesApi
  with PresenceApi
  with EventsApi
  with IosApi
  with TelephonyApi
  with TestsApi {
  val route = {
    presenceApiRouting ~
    oneToOneMessagesApiRouting ~
    groupMessagesApiRouting ~
    eventsApiRouting ~
    accountApiRouting ~
    groupsApiRouting ~
    sessionApiRouting ~
    contactsApiRouting ~
    iosApiRouting ~
    telephonyApiRouting ~
    testsApiRouting
  }
}

I would recommend putting the most common routes first, and use pathPrefix as soon as you can in the sub-routes, so that you reduce the number of tests that Spray runs for each incoming request.

You'll find below a route that I believe is optimized:

  val groupsApiRouting = {
    pathPrefix("v3" / "groups") {
      pathEnd {
        get {
          traceName("GROUPS - Get joined groups list") { listJoinedGroups }
        } ~
        post {
          traceName("GROUPS - Create group") { createGroup }
        }
      } ~
      pathPrefix(LongNumber) { groupId =>
        pathEnd {
          get {
            traceName("GROUPS - Get by ID") { getGroupInformation(groupId) }
          } ~
          put {
            traceName("GROUPS - Edit by ID") { editGroup(groupId) }
          } ~
          delete {
            traceName("GROUPS - Delete by ID") { deleteGroup(groupId) }
          }
        } ~
        post {
          path("invitations" / LongNumber) { invitedUserId =>
            traceName("GROUPS - Invite user to group") { inviteUserToGroup(groupId, invitedUserId) }
          } ~
          path("invitations") {
            traceName("GROUPS - Invite multiple users") { inviteUsersToGroup(groupId) }
          }
        } ~
        pathPrefix("members") {
          pathEnd {
            get {
              traceName("GROUPS - Get group members list") { listGroupMembers(groupId) }
            }
          } ~
          path("me") {
            post {
              traceName("GROUPS - Join group") { joinGroup(groupId) }
            } ~
            delete {
              traceName("GROUPS - Leave group") { leaveGroup(groupId) }
            }
          } ~
          delete {
            path(LongNumber) { removedUserId =>
              traceName("GROUPS - Remove group member") { removeGroupMember(groupId, removedUserId) }
            }
          }
        } ~
        path("coverPhoto") {
          get {
            traceName("GROUPS - Request a new cover photo upload") { getGroupCoverPhotoUploadUrl(groupId) }
          } ~
          put {
            traceName("GROUPS - Confirm a cover photo upload") { confirmCoverPhotoUpload(groupId) }
          }
        } ~
        get {
          path("attachments" / "new") {
            traceName("GROUPS - Request attachment upload") { getGroupAttachmentUploadUrl(groupId) }
          }
        }
      }
    }
  }
Disallow answered 11/8, 2014 at 20:59 Comment(5)
What type does inviteUserToGroup return? RequestContext => Unit?Nimbus
@Nimbus inviteUserToGroup is of type (Long, Long) ⇒ Route :)Disallow
Hi Adrien, maybe you will know if is that type of 'concatenation' still correct? I encounter in that problem #35615208 using spray 1.3.3.Vigorous
@AdrienAubel - for each of your, I'm assuming, interface ...Apis, example: IosApi, do you have implementations, i.e. IosApiImpl that contains the actual implementation? How do you mix those in - to keep the implementation separate from the interface?Palladian
Also - does each Api, such as AccountApi, extends HttpService?Palladian
E
14

You can combine routes from different "Controllers" using ~ combinator.

class AccountServiceActor extends Actor with HttpService {

  def actorRefFactory = context

  def receive = handleTimeouts orElse runRoute(
  new AccountService1.accountService1 ~  new AccountService2.accountService2)

  def handleTimeouts: Receive = {
    case Timeout(x: HttpRequest) =>
      sender ! HttpResponse(StatusCodes.InternalServerError, "Request timed out.")
  }
}



class AccountService1 extends HttpService {

  val accountService1 = {
    get {
      path("") {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete(index)
        }
      }
    }
}


class AccountService2 extends HttpService {

  val accountService2 = {
    get {
      path("someotherpath") {
        respondWithMediaType(`text/html`) { // XML is marshalled to `text/xml` by default, so we simply override here
          complete(index)
        }
      }
    }
}
Erigena answered 1/2, 2013 at 19:55 Comment(4)
Looks like that does the trick. I wonder if I can compose some sort of implicit that can combine them automatically instead of manually writing service1 ~ service2 ~ service3. Thanks!Chromogen
Hmmm deselected it since it looks like it creates some sort of inheritance issue. type arguments [com.threetierlogic.AccountServ ice.AccountServiceActor] do not conform to method apply's type parameter bounds [T <: akka.actor.Actor]Chromogen
Ok made some progress with case class Base(actorRefFactory: ActorRefFactory) extends HttpService { Now the issue is HTTP requests fail because of the following: Cannot dispatch HttpResponse as response (part) for GET request to '/ ' since current response state is 'Completed' but should be 'Uncompleted'Chromogen
For some reason the classes I create by extending HttpService don't compile, they say: needs to be abstract, since method actorRefFactory in trait HttpService of type => akka.actor.ActorRefFactory is not defined class MyRouteRoute extends HttpService{ ^Precession
R
1

I tried this way from the above code snippet, basic format and works.

import akka.actor.ActorSystem
import akka.actor.Props
import spray.can.Http
import akka.io.IO
import akka.actor.ActorRefFactory
import spray.routing.HttpService
import akka.actor.Actor


/**
 * API endpoints
 *
 * Individual APIs are created in traits that are mixed here
 */

trait Api extends ApiService
with UserAccountsService
{
  val route ={
    apiServiceRouting ~
    accountsServiceRouting
  }

}

trait ApiService extends HttpService{
  val apiServiceRouting={
    get{
      path("ping") {
       get {
        complete {
          <h1>pong</h1>
        }
       }
      }
    }
  }
}

trait UserAccountsService extends HttpService{
  val accountsServiceRouting={
     path("getAdmin") {
      get {
        complete {
          <h1>AdminUserName</h1>
        }
      }
    }
  }
}
class ApiActor extends Actor with Api {
  override val actorRefFactory: ActorRefFactory = context

  def receive = runRoute(this.route)
}


object MainTest extends App {

  // we need an ActorSystem to host our application in
  implicit val system = ActorSystem("UserInformaitonHTTPServer")

  // the handler actor replies to incoming HttpRequests
  val handler = system.actorOf(Props[ApiActor], name = "handler")

  // starting the server
  IO(Http) ! Http.Bind(handler, interface = "localhost", port = 8080)

}
Reader answered 11/10, 2015 at 23:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.