jakob
jakob

Reputation: 6005

How do I handle multiple play2 WS calls for a Set of objects and then return the successful calls to view

Let say that in my Controller I have a set of Sms objects. For each one of those objects I would like to call a web service and then for all the successful calls return the sms objects to view.

Controller:

def inviteBySms() = Action {
    implicit request => {
      val smsSet = getSet()
      Async {
        smsSet.map(sms => callSmsService(sms).map(response => {
          response.status match {
            case 200 => {
              // somehow add the sms object to a success set  
            }
            case _ => {
              // ignore
            }
          }
        }))
      // Return Ok() with the success Set[SMS]
      }
    } 

}

method in Controller:

def postToService(sms: Sms) = {

    val params = Map(Seq(current.configuration.getString("sms.service.user").getOrElse("")),
          "pass" -> Seq(current.configuration.getString("sms.service.password").getOrElse("")),
          "mobilephone" -> Seq(sms.number))
        )


    val futureResponse = WS.url(Play.current.configuration
          .getString("sms.service.url").getOrElse(""))
          .withHeaders("Content-Type" -> "application/x-www-form-urlencoded; charset=utf-8")
          .post(params)
}

Upvotes: 1

Views: 188

Answers (2)

mguillermin
mguillermin

Reputation: 4181

After further discussion, here is a most correct answer than my first attempt.

In my code I've just defined Sms as type Sms = String and I'm just calling directly WS.url().get on it.

First, the callSmsService(sms: Sms) should call the service and not return the Response but already map() it depending on whether the return is 200 or not. The idea is to return back the original Sms instance or None so that we can aggregate them later :

def callSmsService(sms: Sms): Future[Option[Sms]] = {
  WS.url(sms).get
    .map { 
      response => if (response.status == 200) Some(sms) else None
    }
}

You can now map() your initial Sms set with this function:

val futureOptionSet: Set[Future[Option[Sms]]] = smsSet.map { sms => callSmsService(sms) }

Then, you have to transform the Set[Future] into a global Future[Set] (you need a unique Future to construct a Future[Result] asked by Async{...}):

val optionSetFuture: Future[Set[Option[Sms]]] = Future.sequence(futureOptionSet)

Now, you can map the global Future to collect only successfull Sms

val smsSetFuture: Future[Set[Sms]] = optionSetFuture.map { set => set.collect { case Some(sms) => sms }}

With this last Future, you can now do a map() to build a Future[Response]

smsSetFuture.map( successfulSmsSet: Set[Sms] => {
  // do something with successfulSmsSet
  ok(...) 
})

I have detailed step by step with explicit return types but you can of course combine several steps without the intermediate values and explicit types.

Upvotes: 1

mguillermin
mguillermin

Reputation: 4181

First, I would call :

smsSet.map(sms => callSmsService(sms))

to get a Set[Future[Response]] and wrap it with Future.sequence to get a Future[Set[Response]] :

Future.sequence(smsSet.map(sms => callSmsService(sms)))

And then on the Future[Set[Response]] you can filter() and map() to get a Future[Set[SMS]]

val futureSetSms = Future.sequence(smsSet.map(sms => callSmsService(sms)))
   .filter{_.status == 200}
   .map(response => {...construct your SMS...})

You then just have to map that to a Future[Result] that can be used by Async{} :

futureSetSms.map { smsSet => Ok(...) }

Upvotes: 0

Related Questions