scala nested for comprehension with futures - scala

My case domain classes are as below
case class Account(randomId: String, accounts: List[String]) // for each of accounts i need to get AccountProfiles.
case class AccountProfiles(actId: String, profiles: List[String], additionalInfo: Map[String, String], ......)
case class AccountInfo(id: String, profiles:List[String]) // for each of AccountProfiles I need to construct AccountInfo
my access layer implementation signature to extract above domain classes look like below
getLinked(): Future[Account]
getAccountProfile(actId: String): Future[AccountProfiles]
Can I have a for comprehension to construct Future list of AccountInfo domain object with the help of getLinked and getAccountProfile methods ?

Yes you can. I think this is what you're looking for assuming AccountProfiles.actId and AccountInfo.id are supposed to be equivalent.
for {
account <- getLinked()
profiles <- Future.sequence(account.accounts map { id => getAccountProfile(id) })
} yield profiles map { p => AccountInfo(p.actId, p.profiles) }

Related

Scala generic case class with optional field

I have the following generic case class to model a resources in an HTTP API (we use Akka HTTP):
case class Job[Result](
id: String,
result: Option[Result] = None,
userId: String,
)
and want to specify multiple, named, variations of a Job, were some of them provide a result, while others don't:
case class FooResult(data: String)
type FooJob = Job[FooResult]
// BarJob does not have any result, thus result should be None
case class BarJob = Job[/*what do to?*/]
my question is, is there any way to define Job as a generic case class where the type parameter only needs to be specified when the field result is Some ? What I would like to do is something like:
// result is by default None in Job, so I don't want to specify any type here
case class BarJob = Job
Or perhaps there's better ways to do this, rather than using type aliases?
One option is to use base traits for Jobs with and without results:
trait Job {
def id: String
def userId: String
}
trait JobWithResult[T] extends Job {
def result: T
}
case class FooResult(data: String)
case class FooJob(id: String, userId: String, result: FooResult) extends JobWithResult[FooResult]
case class BarJob(id: String, userId: String) extends Job
You can then use match on a Job to see whether it has a result or not.
job match {
case FooJob(id, userId, foo) => println(s"FooJob with result $foo")
case j: JobWithResult[_] => println(s"Other job with id ${j.id} with result")
case j: Job => println(s"Job with id {$j.id} without result")
}
This assumes that the result is not actually optional.
As pointed out in the comments, the Option is "unnecessary".
I submit, it's not so much "unnecessary" as indicative ... of a way for you to implement what you want without repetitive declarations.
case class Job[Result](id: String, userId: String, result: Option[Result] = None)
object Job {
def void(id: String, userId: String) = Job[Nothing](id, userId)
def apply[R](id: String, userId: String, result: R) = Job(id, userId, Option(result))
}
type FooJob = Job[FooResult]
type VoidJob = Job[Nothing]

Scala, Either[_, Seq[Either[_, T]] to Either[_, Seq[T]]

Here's the scaste for the code below: https://scastie.scala-lang.org/bQMGrAKgRoOFaK1lwCy04g
I've a two JSON API endpoints. First, items.cgi, returns list of item objects in the following format
$ curl http://example.com/items.cgi
[
...
{ sn: "KXB1333", ownerId: 3, borrowerId: 0 },
{ sn: "KCB1200", ownerId: 1, borrowerId: 2 },
...
]
borrowerId == 0 means item has no borrower.
Second, users.cgi, returns user specified by id query parameter
$ curl http://example.com/user.cgi?id=1
{ id: 1, name: "frank" }
The API may be bad but I have to deal with it. Now in Scala I'd like to work with this nice data model
case class User(id: Int, name: String)
case class Item(sn: String, owner: User, borrower: Option[User])
I also have the following for doing HTTP requests
case class ApiFail(reason: String)
def get[T](url: String): Either[ApiFail, T] = ??? /* omitted for brevity */
The get() function uses some magic to fetch a JSON from an URL and construct a T out of it (it uses some libraries). On IO failure or bad HTTP status it returns Left.
I'd like to write the following function
def getItems: Either[ApiFail, Seq[Item]]
It should fetch the list of items, for each item fetch the linked users and return a new list of Items or fail on any HTTP request failure. (There may be redundant requests for users with a same ID but I don't care about memoization/caching yet.)
So far I only managed to write this function
def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]]
where a failure to retrieve some user is fatal only for the corresponding item and not the whole result. Here's the implementation
def getItems: Either[ApiFail, Seq[Either[ApiFail, Item]]] = {
case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int)
get[List[ItemRaw]]("items.cgi").flatMap(itemRawList => Right(
itemRawList.map(itemRaw => {
for {
owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}")
borrower <-
if (itemRaw.borrowerId > 0)
get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_))
else
Right(None)
} yield
Item(itemRaw.sn, owner, borrower)
})
))
}
This seems like a request for a homework but it occurs to me frequently that I want to switch from one wrapper thing (m-monad?) to another and I'm a bit puzzled as to how to do it with wrapper functions (c-combinators?) only. I could of course switch to an imperative implementation. I'm just curious.
There is a word for doing exactly this in the FP world - "Traverse" (link to cats implementation). It's used when you have an F[A] and a function A => G[B] and you want a G[F[B]]. Here, F is List, A is ItemRaw, G is Either[ApiFail, _], and B is Item. Of course there are some constraints on what F and G can be.
Using cats, you can change your method very slightly:
import cats._, cats.implicits._
def getItems: Either[ApiFail, Seq[Item]] = {
case class ItemRaw(sn: String, ownerId: Int, borrowerId: Int)
get[List[ItemRaw]]("items.cgi").flatMap(itemRawList =>
itemRawList.traverse[({type T[A]=Either[ApiFail, A]})#T, Item](itemRaw => {
for {
owner <- get[User](s"users.cgi?id=${itemRaw.ownerId}")
borrower <-
if (itemRaw.borrowerId > 0)
get[User](s"users.cgi?id=${itemRaw.borrowerId}").map(Some(_))
else
Right(None)
} yield
Item(itemRaw.sn, owner, borrower)
})
)
}
With that said, I can certainly understand being hesitant to go fully down that route. cats (and scalaz) are a lot to take in - though I recommend you do at some point!
Without them, you can always write your own utility methods for manipulating your commonly-used containers:
def seqEither2EitherSeq[A, B](s: Seq[Either[A, B]]): Either[A, Seq[B]] = {
val xs: Seq[Either[A, Seq[B]]] = s.map(_.map(b => Seq(b)))
xs.reduce{ (e1, e2) => for (x1 <- e1; x2 <- e2) yield x1 ++ x2 }
}
def flattenEither[A, B](e: Either[A, Either[A, B]]): Either[A, B] = e.flatMap(identity)
Then the result you want would be:
val result: Either[ApiFail, Seq[Item]] = flattenEither(getItems.map(seqEither2EitherSeq))

Is it possible to 'automatically'/implicitly enrich an anonymous function?

Given:
enum Car { Mustang, Camaro, Challenger }
enum Driver { Bob, John, Mike }
trait Config {
def car:Car
def driver:Driver
def collect[O](f:PartialFunction[(Car,Driver),O]):O
}
def canDrive(config:Config):Boolean = config collect {
//what should 'collect' accept in order to satisfy the following:
// (1) maintain the terseness
// (2) always have access to values of Car and Driver enums
case (Mustang, Bob) => false
case (Camaro, Mike) => false
case _ => true
}
I suppose all enum values can be tucked away into a separate trait:
trait AllEnumValues {
val (Mustang, Camaro, Challenger) = (Car.Mustang, Car.Camaro, Challenger)
val (Bob, John, Mike) = (Driver.Bob, Driver.John, Driver.Mike)
}
But, how would I enrich the arg to Config.collect such that when the anonymous partial function is created I have access to everything inside of AllEnumValues without an implicit import statement

Add Prefix to Reverse Routes

I am looking for a way to add a prefix to all reverse routes of an html template without wrapping them all in function applications. I planned to use an html base tag but the reverse routes all start with a slash and therefore are relative to the host of the base tag rather than the full URL. Is there any feature I might be missing for a more robust solution?
rename routes to context.routes
create empty routes file
add to routes the line:
-> /context context.Routes
To accomplish something like this, to facilitate communication to my app of my app's own URLs in a DRY way, to propagate query parameters debug settings used in development through internal links, and also because I find Play's reverse router to be super ugly, I created a URL abstraction I like a lot better. It works like this:
1) I created a type to represent URLs symbolically:
/** Represents a URL that can possibly be a webpage */
sealed trait PageUrl
case class SectionUrl(slug: String) extends PageUrl
case class StaticPageUrl(slug: String) extends PageUrl
case class ExternalUrl(url: String) extends PageUrl
2) I created a class for resolving these objects into full URLs:
/** Wrapper to propagate request override flags to internal links */
case class UrlWrapper(params: Seq[(String, String)]) {
def apply(url: PageUrl, additionalParams: Seq[(String, String)] = Seq.empty): String = {
url match {
case SectionUrl(slug) => urlAndParams(routes.PageRendererController.showSectionPage(slug).url)
case StaticPageUrl(slug) => urlAndParams(routes.PageRendererController.showStaticPage(slug).url)
case ExternalUrl(u) => u
}
}
def urlAndParams(url: String, additionalParams: Seq[(String, String)] = Seq.empty): String = {
def urlEncode = (u: String) => java.net.URLEncoder.encode(u, "UTF-8")
val formattedParams = (queryParams ++ additionalParams).map{ case (key, value) => s"$key=${urlEncode(value)}" }.mkString("&")
val paramOption = if (formattedParams.nonEmpty) Some(formattedParams) else None
(Seq(url) ++ paramOption).mkString(if (url.indexOf("?") > 0) "&" else "?")
}
}
You could easily modify this to provide a prefix always by default, or upon request via some other method.
3) In a trait/class that all my views extend, I declare an implicit field of type UrlWrapper, that will be available to my templates, so I can do:
#(option1: String, urlParam: PageUrl)(implicit url: UrlWrapper)
...
My link
...
As a bonus, since my pages all correspond to models in my app, I added to UrlWrapper additional methods for converting model objects to resolved URLs:
case class UrlWrapper(...) {
...
def forSection(section: Section, additionalParams: Seq[(String, String)] = Seq.empty): String = {
apply(SectionUrl(section.slug), additionalParams)
}
def forStaticPage(staticPage: StaticPage, additionalParams: Seq[(String, String)] = Seq.empty): String = {
apply(StaticPageUrl(staticPage.slug), additionalParams)
}
}

Using extractors to parse text files

I'm trying to improve a CSV parsing routine and feel that extractors could be useful here but can't figure them out. Suppose there's a file with user ids and emails:
1,alice#alice.com
2,bob#bob.com
3,carol#carol.com
If the User class is defined as case class User(id: Int, email: String) everything is pretty easy with something like
lines map { line =>
line split "," match {
case Array(id, email) => User(id.toInt, email)
}
}
What I don't understand is how to deal with the case where User class can have complex properties e.g
case class Email(username: String, host: string)
case class User(id: Int, email: Email)
You probably want to use a regular expression to extract the contents of the email address. Maybe something like this:
val lines = Vector(
"1,alice#alice.com",
"2,bob#bob.com",
"3,carol#carol.com")
case class Email(username: String, host: String)
case class User(id: Int, email: Email)
val EmailRe = """(.*)#(.*\.com)""".r // substitute a real email address regex here
lines.map { line =>
line.split(",") match {
case Array(id, EmailRe(uname, host)) => User(id.toInt, Email(uname, host))
}
}
Here you go, an example using a custom Extractor.
// 1,Alice,21212,Baltimore,MD" -> User(1, Alice, Address(21212, Baltimore, MD))
Define a custom Extractor that creates the objects out of given String:
object UserExtractor {
def unapply(s: String) : Option[User] = try {
Some( User(s) )
}
catch {
// bettor handling of bad cases
case e: Throwable => None
}
}
Case classes to hold the data with a custom apply on Comapnion object on User:
case class Address(code: String, cit: String, county: String)
case class User(id: Int, name: String, address: Address)
object User {
def apply(s: String) : User = s.split(",") match {
case Array(id, name, code, city, county) => User(id.toInt, name, Address(code, city, county) )
}
}
Unapplying on a valid string (in the example valid means the correct number of fields).
"1,Alice,21212,Baltimore,MD" match { case UserExtractor(u) => u }
res0: User = User(1,Alice,Address(21212,Baltimore,MD))
More tests could be added with more custom apply methods.
I'd use a single RegexExtractor :
val lines = List(
"1,alice#alice.com",
"2,bob#bob.com",
"3,carol#carol.com"
)
case class Email(username: String, host: String)
case class User(id: Int, email: Email)
val idAndEMail = """^([^,]+),([^#]+)#(.+)$""".r
and define a function that transforms a line to the an User :
def lineToUserWithMail(line: String) : Option[User] =
idAndEMail.findFirstMatchIn(line) map {
case userWithEmail(id,user,host) => User(id.toInt, Email(user,host) )
}
Applying the function to all lines
lines flatMap lineToUserWithMail
//res29: List[User] = List(User(1,Email(alice,alice.com)), User(2,Email(bob,bob.com)), User(3,Email(carol,carol.com)))
Alternatively you could implement custom Extractors on the case classe by adding an unnapply Method. But for that case it wouldn't be worth the pain.
Here is an example for unapply
class Email(user:String, host:String)
object Email {
def unapply(s: String) : Option[(String,String)] = s.split("#") match {
case Array(user, host) => Some( (user,host) )
case _ => None
}
}
"bob#bob.com" match {
case Email(u,h) => println( s"$u , $h" )
}
// prints bob , bob.com
A word of warning on using Regex to parse CSV-data. It's not as easy as you might think, i would recommend to use a CSV-Reader as http://supercsv.sourceforge.net/ which handles some nasty edge cases out of the box.