I have a route with a portion like this:
...
(pathEnd | path("summary")) {
parameters(...).as(Query) { query =>
onSuccess(model ? query) {
case MyResponse(list) =>
// at this point I would like to know if I hit pathEnd or
// path(summary) so I can complete with summary or full response.
if (???)
complete(OK, list)
else
complete(OK, list map (_.toSummary))
}
}
}
...
Essentially there's a lot of parameter wrangling and querying of the model that is identical, but I'm doing an extra transformation of the response to shed some data if the summary endpoint is hit. Is it possible to do this in some way?
I tried adding a ctx => after the (pathEnd | path("summary")) { ctx => but that didn't work at all. (The route didn't match, and never returned anything.)
I gave this custom directive a quick unit test and it seems to work:
def pathEndOr(p: String) =
pathEnd.hmap(true :: _) | path(p).hmap(false :: _)
You can use it in you example like so:
...
pathEndOr("summary") { isPathEnd =>
parameters(...).as(Query) { query =>
onSuccess(model ? query) {
case MyResponse(list) =>
// at this point I would like to know if I hit pathEnd or
// path(summary) so I can complete with summary or full response.
if (isPathEnd)
complete(OK, list)
else
complete(OK, list map (_.toSummary))
}
}
}
...
Related
I'm new to Akka HTTP, and trying to write my first API. The routing DSL seems a little confusing.
I managed to match the following:
/channel
/channel/channelName
But now I need to match the following:
/channel/channelName/channelAction
And I can't get it to work.
I currently have:
private val routes: Route =
path("channel") {
get {
reportAllChannelsStatus()
}
} ~
pathPrefix("channel" / Remaining) { channelName =>
get {
singleChannelRequest(channelName, status)
} ~
post {
entity(as[ChannelRequest]) { request =>
singleChannelRequest(channelName, request.channelAction)
}
}
} ~
completeWith404()
I want to add get and post for /channel/channelName/channelAction
Any idea how is this done? (extract both channelName and channelAction)
You can match
path("channel" / Segment / Segment){
(channelName, channelAction) => ...
}
Be aware that for different types of arguments, you'll have to match different things. Segment is for String, IntNumber would be for Int ...
I'm new to the reactive/observables concept.
I've defined a route for getting Products like this:
GET /products controllers.ProductController.findProducts
and the implementation in the controller:
def findProducts() = secured.async { implicit request =>
productDao.find()
.map(products => Ok(Json.toJson(products)))
// products: List[Product]
.recover(errHandler)
}
Then, on the client-side, I'm making the call and subscribing to it like this:
let sub = this.find()
.subscribe(
products => {
this.products = products;
this.productsBehaviorSubject.next( this.products );
if (!!sub)
sub.unsubscribe();
},
error => console.log("Error fetching units: " + error)
);
As soon as I get the data, I unsubscribe from it. (basically using it like Promises).
So I wonder, if this is the right way to do it. Instead of returning List[Product] in one single response, should I be returning in several responses??
def findProducts() = secured.async { implicit request =>
productDao.find()
.map(products => {
products.map(p => Ok(Json.toJson(p))) // haven't tried this yet
})
// products: List[Product]
.recover(errHandler)
}
then on the client side .. maybe .:
this.find()
.take( 50 )
.subscribe(
product => {
this.products.push( product )
},
error => console.log("Error fetching units: " + error),
() => this.productsBehaviorSubject.next( this.products )
);
Your first approach is correct, you will notice that the secured.async expects the code block to return Future[Result]. So if you try to map over the products then the signature will be Future[Seq[Result]] which will give you a compilation error.
Also in my experience, returning the full list in a single response is the standard way to code.
This code looks for faculties and compares it to received faculty from form.
If in DB it finds that faculty, than it adds this group to DB and after that - redirects it, actually, to the same page, but in GET (this is POST method).
The problem is that I need redirecting if it not added that group. Maybe, someone knows how to make this simple and tidily?
def addGroup() = Action.async {
implicit request =>
GroupForm.form.bindFromRequest.fold(
errorForm => ???,
data => {
(for {
seqOfFaculties <- FacultyService.getAllFaculties
future <- GroupService.addGroup(Group(0, data.nameGroup, data.faculty)) if seqOfFaculties.exists(_.name == data.faculty)
} yield future).map(_ => Redirect(routes.GroupController.get()))
})
}
Maybe something like this:
FacultyService.getAllFaculties.map { seqOfFaculties =>
if (seqOfFaculties.exists { _.name == "something" }) {
Ok // or something else...
} else {
Redirect(routes.GroupController.get())
}
}
You should be careful when using if statement inside a for-comprehension with Futures. You could get an exception... :) (with nothing that handles it)
I'm having some problems when trying to map some different objects so that I can extract some fields from it.
I've the function in my controller like this:
def index = SecuredAction.async { implicit request =>
transportService.allNonActive().map { transports =>
val sourceEmailsListBuffer = ListBuffer[String]()
val destinyEmailsListBuffer = ListBuffer[String]()
val sortingCenterStockListBuffer = ListBuffer[SortingCenterStock]()
val transportsListBuffer = ListBuffer[Transport]()
transports.map { transport =>
transportsListBuffer.append(transport)
// gets SC of this transport
sortingCenterStockService.retrieve(transport.idSCStock).map { sortingCenterStock =>
Logger.debug(s"Entry on SCS")
sortingCenterStockListBuffer.append(sortingCenterStock)
}
// gets email from source
userDAO.find(transport.idSourceUser).map { option =>
option.map { user =>
user.email.map { email =>
sourceEmailsListBuffer.append(email)
Logger.debug(s"Entry on Source Email")
}
}
}
// gets email from destiny
userDAO.find(transport.idDestinyUser).map { option =>
option.map { user =>
user.email.map { email =>
destinyEmailsListBuffer.append(email)
Logger.debug(s"Entry on Destiny Email")
}
}
}
}
Logger.debug(s"Size of source emails: ${sourceEmailsListBuffer.size}")
Logger.debug(s"Size of destiny emails: ${destinyEmailsListBuffer.size}")
Logger.debug(s"Size of scs: ${sortingCenterStockListBuffer.size}")
Logger.debug(s"Size of transp: ${transportsListBuffer.size}")
Ok(views.html.transports.index(request.identity, sourceEmailsListBuffer.toList, destinyEmailsListBuffer.toList, sortingCenterStockListBuffer.toList, transportsListBuffer.toList))
}
}
When I load the page for the first time (with any minor change, i.e. I change the string I use to indicate what I'm debugging), it gets the info from the last map userDAO.find(transport.idDestinyUser).map. When I refresh the page, the list's size destinyEmailsListBuffer is 0 and it is returned to the view before doing the map (at least I think so).
This is what I get after refreshing, after getting the correct output for the first time:
second load of the page
Thanks in advance, I hope you can help me!
I think your general structure is wrong. For instance:
userDAO.find(transport.idDestinyUser).map { option =>
option.map { user =>
user.email.map { email =>
destinyEmailsListBuffer.append(email)
Logger.debug(s"Entry on Destiny Email") // This returns Unit!
}
}
}
So you are using map operations and chaining those results to other functions, but instead if returning lists of items, you are incrementing an existing list that is never returned. Either return destinyEmailsListBuffer after logging or re-write to use forEach and to pick up the right values from somewhere.
I've a route as follows:
val route = {
logRequestResult("user-service") {
pathPrefix("user") {
get {
respondWithHeader(RawHeader("Content-Type", "application/json")) {
parameters("firstName".?, "lastName".?).as(Name) { name =>
findUserByName(name) match {
case Left(users) => complete(users)
case Right(error) => complete(error)
}
}
}
} ~
(put & entity(as[User])) { user =>
complete(Created -> s"Hello ${user.firstName} ${user.lastName}")
} ~
(post & entity(as[User])) { user =>
complete(s"Hello ${user.firstName} ${user.lastName}")
} ~
(delete & path(Segment)) { userId =>
complete(s"Hello $userId")
}
}
}
}
The content type of my response should always be application/json as I've it set for the get request. However, what I'm getting in my tests is text/plain. How do I set the content type correctly in the response?
On a side note, the akka-http documentation is one of the most worthless piece of garbage I've ever seen. Almost every link to example code is broken and their explanations merely state the obvious. Javadoc has no code example and I couldn't find their codebase on Github so learning from their unit tests is also out of the question.
I found this one post that says "In spray/akka-http some headers are treated specially". Apparently, content type is one of those and hence can't be set as in my code above. One has to instead create an HttpEntity with the desired content type and response body. With that knowledge, when I changed the get directive as follows, it worked.
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.model.MediaTypes.`application/json`
get {
parameters("firstName".?, "lastName".?).as(Name) { name =>
findUserByName(name) match {
case Left(users) => complete(users)
case Right(error) => complete(error._1, HttpEntity(`application/json`, error._2))
}
}
}