How to create custom directive to reuse routes? - scala

I have a route snippet that I want to reuse in multiple scenarios:
val dirSegment = "licenses"
path( dirSegment ~ PathEnd ) {
redirect( dirSegment + "/", StatusCodes.MovedPermanently )
} ~
pathPrefix(dirSegment) {
path("") {
/* do something */
}
}
I'd like to turn this in to a directive (or parameterizable route?) where I can specify the value of the dirSegment val and arbitrary further routing/code in place of path("") { /* do something */ } white retaining the redirect behavior, looking something like the following:
directoryPath("licenses") {
path("") {
/* do something */
}
} ~
directoryPath("about") {
path("") {
/* do somthing else */
}
}
Whereas that would have equivalent behavior to the following without all the repetition:
val dirSegment = "licenses"
val anotherDir = "About"
path( dirSegment ~ PathEnd ) {
redirect(dirSegment + "/", StatusCodes.MovedPermanently )
} ~
pathPrefix(dirSegment) {
path("") {
/* do something */
}
} ~
path( anotherDir ~ PathEnd ) {
redirect(anotherDir + "/", StatusCodes.MovedPermanently )
} ~
pathPrefix(anotherDir) {
path("") {
/* do something else */
}
}
Note that this question was inspired by some of the discussion in How do I automatically add slash to end of a url in spray routing?

You'll need to write a custom directive for this.
// additional imports you may need
import shapeless.HNil
import spray.http.StatusCodes
import spray.routing.Directive0
import spray.routing.PathMatcher
Now that that's out of the way:
/**
* Spray's PathEnd matches trailing optional slashes... we can't have that
* otherwise it will cause a redirect loop.
*/
object PathEndNoSlash extends PathMatcher[HNil] {
def apply(path: Path) = path match {
case Path.Empty ⇒ PathMatcher.Matched.Empty
case _ ⇒ PathMatcher.Unmatched
}
}
/**
* Custom directive that uses a redirect to add a trailing slashe to segment
* if the slash isn't present.
*/
def directoryPath(segment: String) = new Directive0 {
def happly(f: HNil ⇒ Route) =
// first, the redirect
pathPrefix(segment ~ PathEndNoSlash) {
redirect("/" + segment + "/", StatusCodes.MovedPermanently) } ~
// delegate actual work to nested directives
pathPrefix(segment).happly(f)
}
Usage:
directoryPath("about") {
path("a") {
complete {
"this is /about/a"
}
} ~ path("b") {
complete {
"this is /about/b"
}
} ~ path(PathEnd) {
complete {
"this is /about/"
}
}
}
If a user accesses /about, they will be forwarded to /about/ and see "this is /about/". The nested paths a and b work as expected (that is, without redirects of their own).
Note: This solution is for Spray 1.2.

Related

Jetpack Compose Snackbar above Dialog

How can a Snackbar be shown above a Dialog or AlertDialog in Jetpack Compose? Everything I have tried has resulted in the snack bar being below the scrim of the dialog not to mention the dialog itself.
According to Can I display material design Snackbar in dialog? it is possible in non-Compose Android by using a custom or special (like getDialog().getWindow().getDecorView()) view, but that isn't accessible from Compose I believe (at least not without a lot of effort).
I came up with a solution that mostly works. It uses the built-in Snackbar() composable for the rendering but handles the role of SnackbarHost() with a new function SnackbarInDialogContainer().
Usage example:
var error by remember { mutableStateOf<String?>(null) }
AlertDialog(
...
text = {
...
if (error !== null) {
SnackbarInDialogContainer(error, dismiss = { error = null }) {
Snackbar(it, Modifier.padding(WindowInsets.ime.asPaddingValues()))
}
}
}
...
)
It has the following limitations:
Has to be used in place within the dialog instead of at the top level
There is no host to queue messages, instead that has to be handled elsewhere if desired
Dismissal is done with a callback (i.e. { error = null} above) instead of automatically
Actions currently do nothing at all, but that could be fixed (I had no use for them, the code do include everything necessary to render the actions I believe, but none of the interaction).
This has built-in support for avoiding the IME (software keyboard), but you may still need to follow https://stackoverflow.com/a/73889690/582298 to make it fully work.
Code for the Composable:
#Composable
fun SnackbarInDialogContainer(
text: String,
actionLabel: String? = null,
duration: SnackbarDuration =
if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite,
dismiss: () -> Unit,
content: #Composable (SnackbarData) -> Unit
) {
val snackbarData = remember {
SnackbarDataImpl(
SnackbarVisualsImpl(text, actionLabel, true, duration),
dismiss
)
}
val dur = getDuration(duration, actionLabel)
if (dur != Long.MAX_VALUE) {
LaunchedEffect(snackbarData) {
delay(dur)
snackbarData.dismiss()
}
}
val popupPosProvider by imeMonitor()
Popup(
popupPositionProvider = popupPosProvider,
properties = PopupProperties(clippingEnabled = false),
) {
content(snackbarData)
}
}
#Composable
private fun getDuration(duration: SnackbarDuration, actionLabel: String?): Long {
val accessibilityManager = LocalAccessibilityManager.current
return remember(duration, actionLabel, accessibilityManager) {
val orig = when (duration) {
SnackbarDuration.Short -> 4000L
SnackbarDuration.Long -> 10000L
SnackbarDuration.Indefinite -> Long.MAX_VALUE
}
accessibilityManager?.calculateRecommendedTimeoutMillis(
orig, containsIcons = true, containsText = true, containsControls = actionLabel != null
) ?: orig
}
}
/**
* Monitors the size of the IME (software keyboard) and provides an updating
* PopupPositionProvider.
*/
#Composable
private fun imeMonitor(): State<PopupPositionProvider> {
val provider = remember { mutableStateOf(ImePopupPositionProvider(0)) }
val context = LocalContext.current
val decorView = remember(context) { context.getActivity()?.window?.decorView }
if (decorView != null) {
val ime = remember { WindowInsetsCompat.Type.ime() }
val bottom = remember { MutableStateFlow(0) }
LaunchedEffect(Unit) {
while (true) {
bottom.value = ViewCompat.getRootWindowInsets(decorView)?.getInsets(ime)?.bottom ?: 0
delay(33)
}
}
LaunchedEffect(Unit) {
bottom.collect { provider.value = ImePopupPositionProvider(it) }
}
}
return provider
}
/**
* Places the popup at the bottom of the screen but above the keyboard.
* This assumes that the anchor for the popup is in the middle of the screen.
*/
private data class ImePopupPositionProvider(val imeSize: Int): PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect, windowSize: IntSize,
layoutDirection: LayoutDirection, popupContentSize: IntSize
) = IntOffset(
anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, // centered on screen
anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2 + // centered on screen
(windowSize.height - imeSize) / 2 // move to the bottom of the screen
)
}
private fun Context.getActivity(): Activity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is Activity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}
private data class SnackbarDataImpl(
override val visuals: SnackbarVisuals,
val onDismiss: () -> Unit,
) : SnackbarData {
override fun performAction() { /* TODO() */ }
override fun dismiss() { onDismiss() }
}
private data class SnackbarVisualsImpl(
override val message: String,
override val actionLabel: String?,
override val withDismissAction: Boolean,
override val duration: SnackbarDuration
) : SnackbarVisuals

Angular 5 prevent adding props to class

I have this class
export class Alpha {
propAlpha?:string;
constructor(){
}
setProp(key: string, value: string) {
this[key] = value;
}
}
Some rest call gives me an object (response) like this:
{
propAlpha: "foo",
_someUnwanted: "bar"
}
I need to push only valid props of this object into Alpha, so I did
let myAlpha = new Alpha();
_.each(Object.keys(response), key => {
validProp(response[key]) && myAlpha.setProp(key, response[key]);
/**
* validProp() checks if value matches some criteria.
* So even if "propAlpha" is in "Alpha" it may be
* excluded for some other reason!
*/
});
the problem is that _someUnwanted is added to my class.
How can I prevent that?
I'd need to check which keys are in Alpha..
maybe like this?
if(key in myAlpha) {
myAlpha.setProp(key, response[key]);
}
Different approach that doesn't require declaring separate enum is to declare class members inside it's constructor. Then your Alpha class would look like this:
export class Alpha {
constructor(public propAlpha?: string){}
setProp(key: string, value: string) {
if (key in this) this[key] = value;
}
}
assigning properties wouldn't change
_.each(Object.keys(response), key => {
validProp(response[key]) && myAlpha.setProp(key, response[key]);
});
Really awful but I solved by doing this:
export enum AlphaPublicKeys {
propAlpha
}
let myAlpha = new Alpha();
_.each(Object.keys(response), key => {
if(key in AlphaPublicKeys){
validProp(response[key]) && myAlpha.setProp(key, response[key]);
}
/**
* validProp() checks if value matches some criteria.
* So even if "propAlpha" is in "Alpha" it may be
* excluded for some other reason!
*/
});
Hope this helps!

Spray Routing Doesn't match anything

I have tried many things, but no matter what I do in my tests (which simply sends a PUT request to "create a user") the logs do not enter any of the pathPrefix and just go to the end and fail to match anything.
Can anyone offer insight? Below is the class I have written as well as the simple test (which doesn't even check anything yet)
I know overall it is rather rudimentary and I am not doing things great, but it's just something I threw together to the point where I felt i could do a few quick tests on the routing
package system
import akka.actor.{Props, ActorRef, ActorLogging}
import akka.util.Timeout
import spray.http.HttpHeader
import spray.routing.directives.OnCompleteFutureMagnet
import spray.routing.{RequestContext, HttpServiceActor, Route}
import spray.http.StatusCodes._
import scala.concurrent.duration._
import akka.pattern.ask
import system.F_BackBone._
import scala.util.{Failure, Success}
import language.postfixOps
class F_Listener(backbone: ActorRef) extends HttpServiceActor with ActorLogging {
import context.dispatcher
implicit val timeout = Timeout(5 seconds)
log.debug("beginning user log\n")
//TODO write the main listener spawner that acts as the main spray server and binds these listeners to connections
//TODO must watch the spray side http server actor and die with it
val route: Route = { uri =>
log.debug("request received " + uri.unmatchedPath)
pathPrefix("data") { req =>
log.debug("data path detected " + req.unmatchedPath + "\n")
get {
genericGet(req, GetImage) //URIs for actual data like pictures
} ~
path("uploadimage") {
put { req =>
genericPut(PutImage(req.request.entity))
}
}
} ~
pathPrefix("user") { req =>
log.debug("user path detected" + req.unmatchedPath + "\n")
get {
genericGet(req, GetUserInfo)
} ~
post {
genericPost(req, req.request.headers, UpdateUserData)
} ~
delete {
genericDelete(req, DeleteUser)
}
pathPrefix("newuser") {
put { req =>
genericPut(CreateUser(req.request))
}
}
} ~
pathPrefix("page") { req =>
log.debug("page path detected" + "\n")
get {
genericGet(req, GetPageInfo)
} ~
post {
genericPost(req, req.request.headers, UpdatePageData)
} ~
delete {
genericDelete(req, DeletePage)
}
path("newpage") {
put { req =>
genericPut(CreatePage(req.request))
}
}
} ~
pathPrefix("profile") { req =>
log.debug("profile path detected" + "\n")
get {
genericGet(req, GetProfileInfo)
} ~ //no need for "createprofile" they are created with the user and can be accessed through the JSON returned with that creation
post {
genericPost(req, req.request.headers, UpdateProfileData)
} //no need to delete profile, deleted with user
} ~
pathPrefix("picture") { req =>
log.debug("picture path detected" + "\n")
get {
genericGet(req, GetPictureInfo)
} ~ //same as for profile, when you upload an image the picture JSON is created
post {
genericPost(req, req.request.headers, UpdateImageData)
} ~
delete {
genericDelete(req, DeletePicture)
}
} ~
pathPrefix("album") { req =>
log.debug("album path detected" + "\n")
get {
genericGet(req, GetAlbumInfo)
} ~
post {
genericPost(req, req.request.headers, UpdateAlbumData)
} ~
delete {
genericDelete(req, DeleteAlbum)
}
path("createalbum") {
put { req =>
genericPut(CreateAlbum(req.request))
}
}
}
log.error("no path matched" + "\n")
complete(NotFound, "That resource does not exist")
}
/**
* goes inside of a spray routing "get" and completes the passed message to the backbone given the id
* it internally converts the remaining request path to a bigint, if this fails it completes with a failure
* #param req the reques who contains the string to be converted to bigint as an ID
* #param messageConstructor the case class message (constructor but can be passed with sugar) to compose the bigint into
* #return returns a route for thed spray routing to go through and side effects the complete needed
*/
def genericGet(req: RequestContext, messageConstructor: (BigInt) => GetInfo): Route = {
val id = req.unmatchedPath.toString
if (!id.contains("/")) { //if this is the last element only
try {
val idBig = BigInt(id)
onComplete(OnCompleteFutureMagnet((backbone ? messageConstructor.apply(idBig)).mapTo[String])) {
case Success(entityJson) =>
log.info("get completed successfully: " + messageConstructor + " " + "for " + idBig)
complete(entityJson)
case Failure(ex) =>
log.error(ex, "get failed: " + messageConstructor + " for " + idBig)
complete(InternalServerError, "Request could not be completed: \n" + ex.getMessage)
}
} catch {
case e: NumberFormatException =>
log.info("illegally formatted id requested: " + id)
complete(BadRequest, "Numbers are formatted incorrectly")
}
}
else reject
}
/**
* same as above but for posts, I treid to write a more generic function to repeat rewriting code but it ended up just not being worth the thought
* #param req request who contains id to parse to bigint
* #param args arguments to change
* #param messageConstructor the message to send
* #return
*/
def genericPost(req: RequestContext, args: List[HttpHeader], messageConstructor: (BigInt, List[HttpHeader]) => PostInfo) = {
val id = req.unmatchedPath.toString
if (!id.contains("/")) {
try {
val idBig = BigInt(id)
onComplete(OnCompleteFutureMagnet((backbone ? messageConstructor.apply(idBig, args)).mapTo[String])) {
case Success(newEntityJson) =>
log.info("post completed successfully: " + messageConstructor + " for " + idBig)
complete(newEntityJson)
case Failure(ex) =>
log.error(ex, "get failed: " + messageConstructor + " for " + idBig)
complete(InternalServerError, "Request could not be completed: \n" + ex.getMessage)
}
} catch {
case e: NumberFormatException =>
log.info("illegally formatted id requested: " + id)
complete(BadRequest, "Numbers are formatted incorrectly")
}
}
else reject
}
/**
* same as above except no need to parse a special message since the path tells all and this is for putting, so the fully constructed message gets passed here
* #param message constructed message to send to the backbone for handling
* #return
*/
def genericPut(message: PutInfo) = {
pathEnd {
onComplete(OnCompleteFutureMagnet((backbone ? message).mapTo[String])) {
case Success(newEntityJson) =>
log.info("put completed successfully: " + message)
complete(newEntityJson)
case Failure(ex) =>
log.error(ex, "put failed: " + message)
complete(InternalServerError, "Error putting entity: " + ex.getMessage)
}
}
}
/**
* almost identical to get, should probobly only be one function
* #param req identical
* #param messageConstructor identical
* #return route
*/
def genericDelete(req: RequestContext, messageConstructor: (BigInt) => DeleteInfo) = {
val id = req.unmatchedPath.toString
if (!id.contains("/")) {
try {
val idBig = BigInt(id)
onComplete(OnCompleteFutureMagnet((backbone ? messageConstructor.apply(idBig)).mapTo[String])) {
case Success(newEntityJson) =>
log.info("delete completed successfully: " + messageConstructor + " for " + idBig)
complete(newEntityJson)
case Failure(ex) =>
log.error(ex, "get failed: " + messageConstructor + " for " + idBig)
complete(InternalServerError, "Request could not be completed: \n" + ex.getMessage)
}
} catch {
case e: NumberFormatException =>
log.info("illegally formatted id requested: " + id)
complete(BadRequest, "Numbers are formatted incorrectly")
}
}
else reject
}
override def receive = runRoute(route)
}
object F_Listener {
def props(backbone: ActorRef) = Props(new F_Listener(backbone))
}
/*ideas for speed improvements:
parse out arguments before passing to backbone (might help with scaling to distributed system)
genericDelete, Post, Put, and Get are all pretty similar, some functional composition is probobly possible, esp for delete and get
*/
import akka.testkit.TestActorRef
import org.scalatest.{WordSpec}
import spray.http.HttpHeaders
import spray.testkit.ScalatestRouteTest
import system.F_Listener
class FakebookRoutingTests extends WordSpec with ScalatestRouteTest {
val route = TestActorRef(new F_Listener(null)).underlyingActor.route
//implicit val host = DefaultHostInfo(HttpHeaders.Host("fake.fakebook.com"), false)
"the route" should {
"succeed on path to create a user" in {
Put("/user/newuser") ~> route ~> check {
}
}
}
}
Removing req => helps. If I remember correctly, put, get, post etc. don't provide any arguments for directives. Same goes for path. Removing uri => may also be needed.
Update:
If req => is to stay, then all calls finalizing the request should be called on that object. So, for example, instead of complete(entityJson) there should be req.complete(entityJson) on the top-most context object.
For given code top-most block with context is the first one containing uri so somewhere in the code there should be a call to uri.complete.

How to structure a RESTful API with Spray.io?

When I'm using Spray.io to develop a RESTful API, how should I structure my application?
I already saw this answer on how to split a Spray application, but I'm not satisfied with it, since it doesn't seem to use the "one actor per request" approach. Can I forward requests from the root actor to other actors in my application based on paths and, inside these actors, define the related routes?
Thanks
You can certainly forward requests from one actor to another, based on paths or whatever else. Check out my example project (which is a fork of a fork of an example project):
https://github.com/gangstead/spray-moviedb/blob/master/src/main/scala/com/example/routes/ApiRouter.scala
Relavent code from the main actor that receives all requests and routes them to other actors that handle each service:
def receive = runRoute {
compressResponseIfRequested(){
alwaysCache(simpleCache) {
pathPrefix("movies") { ctx => asb.moviesRoute ! ctx } ~
pathPrefix("people") { ctx => asb.peopleRoute ! ctx }
} ~
pathPrefix("login") { ctx => asb.loginRoute ! ctx } ~
pathPrefix("account") { ctx => asb.accountRoute ! ctx }
}
}
And for example the movies route:
def receive = runRoute {
get {
parameters('query, 'page ? 1).as(TitleSearchQuery) { query =>
val titleSearchResults = ms.getTitleSearchResults(query)
complete(titleSearchResults)
}~
path(LongNumber) { movieId =>
val movie = ms.getMovie(movieId)
complete(movie)
}~
path(LongNumber / "cast") { movieId =>
val movieCast = ms.getMovieCast(movieId)
complete(movieCast)
}~
path(LongNumber / "trailers") { movieId =>
val trailers = ms.getTrailers(movieId)
complete(trailers)
}
}
}
I was struggling a lot with creating first full REST project. The examples I've found was on hello world level... I've read few blogs, few comments and I decided to create example project. It is based on scala/akka/spray/mysql
It full working example with websocket to notify clients that data was changed etc. You can check it out on https://github.com/vixxx123/scalasprayslickexample
Here is sample code of routing from that project:
val personCreateHandler = actorRefFactory.actorOf(RoundRobinPool(2).props(Props[CreateActor]), s"${TableName}CreateRouter")
val personPutHandler = actorRefFactory.actorOf(RoundRobinPool(5).props(Props[UpdateActor]), s"${TableName}PutRouter")
val personGetHandler = actorRefFactory.actorOf(RoundRobinPool(20).props(Props[GetActor]), s"${TableName}GetRouter")
val personDeleteHandler = actorRefFactory.actorOf(RoundRobinPool(2).props(Props[DeleteActor]), s"${TableName}DeleteRouter")
val userRoute =
pathPrefix("person") {
pathEnd {
get {
ctx => personGetHandler ! GetMessage(ctx, None)
} ~
post {
entity(as[Person]) {
entity =>
ctx => personCreateHandler ! CreateMessage(ctx, entity)
}
}
} ~
pathPrefix (IntNumber){
entityId => {
pathEnd {
get {
ctx => personGetHandler ! GetMessage(ctx, Some(entityId))
} ~ put {
entity(as[Person]) { entity =>
ctx => personPutHandler ! PutMessage(ctx, entity.copy(id = Some(entityId)))
}
} ~ delete {
ctx => personDeleteHandler ! DeleteMessage(ctx, entityId)
} ~ patch {
ctx => personPutHandler ! PatchMessage(ctx, entityId)
}
}
}
}
}
And sample from create actor handler:
override def receive: Receive = {
case CreateMessage(ctx, person) =>
val localCtx = ctx
connectionPool withSession {
implicit session =>
try {
val resId = PersonsIdReturning += person
val addedPerson = person.copy(id = Some(resId.asInstanceOf[Int]))
localCtx.complete(addedPerson)
publishAll(CreatePublishMessage(TableName, localCtx.request.uri + "/" + addedPerson.id.get, addedPerson))
L.debug(s"Person create success")
} catch {
case e: Exception =>
L.error(s"Ups cannot create person: ${e.getMessage}", e)
localCtx.complete(e)
}
}
}
There are still two important things missing: oauth2 and push notifications to specific user/connection via websocket

Spray: How to apply respondWithHeaders to all the routes instead of each one

I'm learning Scala that not yet familiar with using Spray framework to build some REST-API application and faced with the issue: all my HTTP responses should have specific header (Access-Control-Allow-Origin). So I can't find out how to set it to all application's responses one time, not to each one.
My route looks like this:
trait Statistics extends HttpService { self: StatisticModuleLike =>
implicit def MM: MarshallerM[Future]
lazy val statisticsRoute =
path("statistics" / "applications" / Segment / Segment ) { (measure, period) =>
get {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getAppCount(MeasureType.withName(measure), period.toInt)
}
}
}
} ~
path("statistics" / "approvals" / Segment / Segment ) { (measure, period) =>
get {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getApproval(MeasureType.withName(measure), period.toInt)
}
}
}
} ~
path("statistics" / "amounts" / Segment / Segment ) { (measure, period) =>
get {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getAmount(MeasureType.withName(measure), period.toInt)
}
}
}
} ~
path("statistics" / "sellers" / "snooze") {
get {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getSellerSnooze(MeasureType.withName("Month"), 100)
}
}
}
} ~
path("statistics" / "sellers" / "snooze" / Segment / Segment ) { (measure, period) =>
get {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getSellerSnooze(MeasureType.withName(measure), period.toInt)
}
}
}
} ~
path("statistics" / "sellers" / "growing" / Segment / Segment ) { (measure, period) =>
get {
parameter('percent.as[Int] ? 0) { percent =>
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getSellerDynamic(MeasureType.withName(measure), period.toInt, DynamicTrendType.withName("Growing"), percent)
}
}
}
}
} ~
path("statistics" / "sellers" / "falling" / Segment / Segment ) { (measure, period) =>
get {
parameters('percent.as[Int] ? 0, 'average.as[Int] ? 0) { (percent, average) =>
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
complete {
getSellerDynamic(MeasureType.withName(measure), period.toInt, DynamicTrendType.withName("Falling"), percent)
}
}
}
}
}
}
As you can see, adding
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*"))
to each path is inconvenient...
Is any cute way to solve this? Say, for example, to extend HttpService with some customization and use it instead of base one?
That's a big route =). Actually spray directives are perfectly composable, so no need in this duplication, you can reduce you structure to something like this:
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
(pathPrefix("statistics") & get) {
pathPrefix("applications") {
path(Segment / Segment) { (measure, period) =>
complete { getAppCount(MeasureType.withName(measure), period.toInt) }
}
} ~
pathPrefix("applications") { ... } ~
path("amounts") { ... } ~
...
}
}
where PathPrefix checks that path starts with the given prefix, Path simple matches against the rest of the route, there is also pathSuffix, pathEnd, etc...
Also in order to simplify large structures, i've found useful to compose Spray with Cake Pattern and making so called Handlers, which would handle your logic, this solution is more flexible and much easier to test:
trait SprayRoute extends CounterHandler with ... {
val service: Route = {
respondWithHeader(RawHeader("Access-Control-Allow-Origin", "*")) {
(pathPrefix("statistics") & get) {
pathPrefix("applications") {
path(Segment / Segment) { CounterHandler }
} ~
pathPrefix("applications") { ... } ~
path("amounts") { ... } ~
...
}
}
}
}
trait CounterHandler {
def CounterHandler: (String, String) => Route = { (measure, period) =>
complete { getAppCount(MeasureType.withName(measure), period.toInt) }
}
}
You simply can wrap your main route around the respondWithHeader directive. See the example (Spray 1.1.:
object HelloRouting extends SimpleRoutingApp with App{
implicit val system = ActorSystem("test")
import system.dispatcher
startServer(interface= "localhost", port= 8082){
lazy val api = pathPrefix("api"){
path("hello"){
get{
complete( "Hello, World" )
}
}
}
respondWithHeader(RawHeader("X-My-Header", "My Header is awesome!")) {
api
}
}
}