Building a RESTful API with
Scalaz
Yeshwanth Kumar
Platform Engineer
Megam Systems
1
Agenda
1. Quick intro to scalaz library
2. REST API - Gateway architecture
3. Real time usecase of scalaz
3
Why functional ?
● complex software - well structured
● immutable - no assignment statements
● no side effects - order of execution is irrelevant
● pure functions
● concise code
● increases readability and productivity
● fun
4
Some(FP) with scala
● map
● flatmap
def Mee(x: Int) = List(1+x)
val ListMe = List(1,2) -> ListMe: List[Int] = List(1,2)
ListMe.map(x => Mee(x)) -> ListMe: List[List[Int]] = List(List(2),List(3))
ListMe.flatMap(x => Mee(x)) -> List[Int] = List(1,2)
● First class functions
● Immutable collections library
● Supports pattern matching
Quick intro to scalaz
● A library to write functional code in scala
● It is not hard
● Does not require super human powers
6
scalaz - typeclasses
1. Equality:
scala> “Paul” == 1
Boolean = false
In scalaz..
scala>import scala._
scala>“john” === 2
Compilation Error
● Adding type-safety becomes a lot easy
2. Order typeclass:
scala> 3 ?|? 2
res0: scalaz.Ordering = GT
scala> 22 ?|? “hello”
<console>:17: error: type
mismatch;
found : String("hello")
required: Int
3 ?|? "hello"
3. Show typeclass:
scala> “vader”.show
res4: scalaz.Cord = "vader"
new Thread().show
implicit val showThread =
Show.shows[Thread]{_.getName}
new.Thread().show
//return the name
Type class A → defines some behaviour in the form of operations →supported by Type T
then Type T → member → typeclass A
7
Lens - the combination of Getter &
setter
case class Playlist(artist: String, ranking: Int)
val Paul = Playlist(“paul”, 9)
val Ringo = Playlist(“Ringo”, 3)
//now creating a lens
val rate = Lens.lensu[Playlist, int]((a, value) => a.copy(ranking =
value), a => a.ranking) //lens for changing the rate of the artist
val incrementSet = rate set (“paul”, 10) //setter, but not exactly
val increment = rate %= {_+1}
8
Lens compositions
def addressL: Lens[Person, Address] = …
def streetL: Lens[Address, String] = …
val personStreetL: Lens[Person, String] = streetL compose addressL
//getter
val str: String = personStreetL get person
//setter
val newP: Person = personStreetL set (person, "Bob St")
● bidirectional transformations between pairs of
connected structures.
● neat way of updating deeply nested data structure
9
Validation - fail fast
● Left
● Right
object FunTimes extends Essentials {
def Need(s: Things): Validation[String, String]
{
//a for comprehension
for {
step1 <- checkFood(s)
step2 <- checkBooze(s)
step3 <- checkGasoline(s)
} yield { “Alrighty! all set!” }
}
10
ValidationNel
● applicative builder |@|
● NonEmptyList - singly linked list to aggregate all errors
● toValidationNel - a helper method
def Need(s: Things) = {
(packFood.toValidationNel |@| packBooze.toValidationNel )
..
//will return NonEmptyList(no food, no liquor)
Overview of the architecture
1111
Request
RubyAPI
Response
Auth
HMAC
API Gateway server
FunnelResponse
FunnelRequest
Riak
Snowflake ID
How it all started….
package controller
import play.api.mvc._
object Logs extends Controller {
def list = Action {
Ok(views.html.index("Your new application is
ready."))
}
13
object Application extends Controller with LoginLogout with
AuthConfigImpl {}
Authentication
object Application extends Controller with HMACAccessElement with
Auth with AuthConfigImpl {}
Started abusing traits..
play-2 uses Stackable controller
Authentication
def post = StackAction(parse.tolerantText) { implicit request =>
val input = (request.body).toString()
val result = models.Accounts.create(input)
result match {
case Success(succ)
case Failure(err)
}
● Use a StackAction in your controller
● It first calls StackAction and composes with other actions
15
def Authenticated[A](req: FunnelRequestBuilder[A]): ValidationNel[Throwable,
Option[String]] = {
Logger.debug(("%-20s -->[%s]").format("SecurityActions", "Authenticated:
Entry"))
req.funneled match {
case Success(succ) => {
Logger.debug(("%-20s -->[%s]").format("FUNNLEDREQ-S", succ.toString))
(succ map (x => bazookaAtDataSource(x))).getOrElse(
Validation.failure[Throwable, Option[String]]
(CannotAuthenticateError("""Invalid content in header. API server couldn't
parse it""",
"Request can't be funneled.")).toValidationNel) }
Authentication
16
Validation - Usecase
● Try Catch is cumbersome
● Handle exceptions as values
def create(input: String): ValidationNel[Throwable, Option[AccountResult]]
17
ValidationNel
def create(email: String, input: String): ValidationNel[Throwable, Option[EventsResult]] = {
(mkGunnySack(email, input) leftMap { err: NonEmptyList[Throwable] =>
new ServiceUnavailableError(input, (err.list.map(m => m.getMessage)).mkString("n"))
}).toValidationNel.flatMap { gs: Option[GunnySack] =>
(riak.store(gs.get) leftMap { t: NonEmptyList[Throwable] => t }).
flatMap { maybeGS: Option[GunnySack] =>
maybeGS match {
case Some(thatGS) => (parse(thatGS.value).extract[EventsResult].some).successNel[Throwable]
case None => {
play.api.Logger.warn(("%-20s -->[%s]").format("Events created. success", "Scaliak returned =>
None. Thats OK."))
(parse(gs.get.value).extract[EventsResult].some).successNel[Throwable]; }}}}}
}
18
for-comprehension interaction with ValidationNels
private def mkGunnySack(email: String, input: String): ValidationNel[Throwable, Option[GunnySack]] = {
play.api.Logger.debug(("%-20s -->[%s]").format("models.tosca.Events", "mkGunnySack:Entry"))
play.api.Logger.debug(("%-20s -->[%s]").format("email", email))
play.api.Logger.debug(("%-20s -->[%s]").format("json", input))
val eventsInput: ValidationNel[Throwable, EventsInput] = (Validation.fromTryCatchThrowable[EventsInput,Throwable] {
parse(input).extract[EventsInput]
} leftMap { t: Throwable => new MalformedBodyError(input, t.getMessage) }).toValidationNel //capture failure
for {
event <- eventsInput
//aor <- (models.Accounts.findByEmail(email) leftMap { t: NonEmptyList[Throwable] => t })
uir <- (UID(MConfig.snowflakeHost, MConfig.snowflakePort, "evt").get leftMap { ut: NonEmptyList[Throwable] => ut })
} yield {
//val bvalue = Set(aor.get.id)
val bvalue = Set(event.a_id)
val json = new EventsResult(uir.get._1 + uir.get._2, event.a_id, event.a_name, event.command, event.launch_type, Time.
now.toString).toJson(false) //validated schema
new GunnySack(uir.get._1 + uir.get._2, json, RiakConstants.CTYPE_TEXT_UTF8, None,
Map(metadataKey -> metadataVal), Map((bindex, bvalue))).some
}}
JSON Serialization
case class EventsResult(id: String, a_id: String, a_name: String, command: String, launch_type: String,
created_at: String) {
def toJson(prettyPrint: Boolean = false): String = if (prettyPrint) {..}
import net.liftweb.json.scalaz.JsonScalaz.toJSON
import models.json.tosca.EventsResultSerialization
● Easy serialization
● Validated schema, hence no junk gets into your NoSQL
20
class EventsResultSerialization(charset: Charset = UTF8Charset) extends SerializationBase[EventsResult]
{ protected val IdKey = "id" ..}
override implicit val writer = new JSONW[EventsResult] {
override def write(h: EventsResult): JValue = {
JObject(
JField(IdKey, toJSON(h.id)) ::
JField(AssemblyIdKey, toJSON(h.a_id)) ::
Nil) }}
override implicit val reader = new JSONR[EventsResult] {
override def read(json: JValue): Result[EventsResult] = {
val idField = field[String](IdKey)(json)
val assemblyIdField = field[String](AssemblyIdKey)(json)
(idField |@|assemblyIdField |@|assemblyNameField |@|commandField |@| launchTypeField |@|
createdAtField) {
(id: String, a_id: String, a_name: String, command: String, launch_type: String, created_at:
String) =>
new EventsResult(id, a_id, a_name, command, launch_type, created_at) }}}
21
either[T,S] - the weird /
def getContent(url: String): Either[String, String] =
if (url == "google")
Left("Requested URL is blocked for the good of the people!")
else
Right("Nopey!))
● Either in scala
● Similar in scalaz, called /
isLeft, isRight, swap, getOrElse
22
(for {
resp <- eitherT[IO, NonEmptyList[Throwable], Option[AccountResult]] { //disjunction Throwable / Option with a
Function IO.
(Accounts.findByEmail(freq.maybeEmail.get).disjunction).pure[IO]
}
found <- eitherT[IO, NonEmptyList[Throwable], Option[String]] { / val fres = resp.get
val calculatedHMAC = GoofyCrypto.calculateHMAC(fres.api_key, freq.mkSign)
if (calculatedHMAC === freq.clientAPIHmac.get) {
(("""Authorization successful for 'email:' HMAC matches:
|%-10s -> %s
|%-10s -> %s
|%-10s -> %s""".format("email", fres.email, "api_key", fres.api_key, "authority", fres.authority).
stripMargin)
.some).right[NonEmptyList[Throwable]].pure[IO]
} else {
(nels((CannotAuthenticateError("""Authorization failure for 'email:' HMAC doesn't match: '%s'.""" .format
(fres.email).stripMargin, "", UNAUTHORIZED))): NonEmptyList[Throwable]).left[Option[String]].pure[IO]}}} yield
found).run.map(_.validation).unsafePerformIO()}}
Question?
Yeshwanth Kumar
Platform Engineer
Megam Systems
(www.megam.io)
Twitter: @morpheyesh
Email: getyesh@megam.io
Docs: docs.megam.io
Devcenter: devcenter.
megam.io

RESTful API using scalaz (3)

  • 1.
    Building a RESTfulAPI with Scalaz Yeshwanth Kumar Platform Engineer Megam Systems 1
  • 2.
    Agenda 1. Quick introto scalaz library 2. REST API - Gateway architecture 3. Real time usecase of scalaz
  • 3.
    3 Why functional ? ●complex software - well structured ● immutable - no assignment statements ● no side effects - order of execution is irrelevant ● pure functions ● concise code ● increases readability and productivity ● fun
  • 4.
    4 Some(FP) with scala ●map ● flatmap def Mee(x: Int) = List(1+x) val ListMe = List(1,2) -> ListMe: List[Int] = List(1,2) ListMe.map(x => Mee(x)) -> ListMe: List[List[Int]] = List(List(2),List(3)) ListMe.flatMap(x => Mee(x)) -> List[Int] = List(1,2) ● First class functions ● Immutable collections library ● Supports pattern matching
  • 5.
    Quick intro toscalaz ● A library to write functional code in scala ● It is not hard ● Does not require super human powers
  • 6.
    6 scalaz - typeclasses 1.Equality: scala> “Paul” == 1 Boolean = false In scalaz.. scala>import scala._ scala>“john” === 2 Compilation Error ● Adding type-safety becomes a lot easy 2. Order typeclass: scala> 3 ?|? 2 res0: scalaz.Ordering = GT scala> 22 ?|? “hello” <console>:17: error: type mismatch; found : String("hello") required: Int 3 ?|? "hello" 3. Show typeclass: scala> “vader”.show res4: scalaz.Cord = "vader" new Thread().show implicit val showThread = Show.shows[Thread]{_.getName} new.Thread().show //return the name Type class A → defines some behaviour in the form of operations →supported by Type T then Type T → member → typeclass A
  • 7.
    7 Lens - thecombination of Getter & setter case class Playlist(artist: String, ranking: Int) val Paul = Playlist(“paul”, 9) val Ringo = Playlist(“Ringo”, 3) //now creating a lens val rate = Lens.lensu[Playlist, int]((a, value) => a.copy(ranking = value), a => a.ranking) //lens for changing the rate of the artist val incrementSet = rate set (“paul”, 10) //setter, but not exactly val increment = rate %= {_+1}
  • 8.
    8 Lens compositions def addressL:Lens[Person, Address] = … def streetL: Lens[Address, String] = … val personStreetL: Lens[Person, String] = streetL compose addressL //getter val str: String = personStreetL get person //setter val newP: Person = personStreetL set (person, "Bob St") ● bidirectional transformations between pairs of connected structures. ● neat way of updating deeply nested data structure
  • 9.
    9 Validation - failfast ● Left ● Right object FunTimes extends Essentials { def Need(s: Things): Validation[String, String] { //a for comprehension for { step1 <- checkFood(s) step2 <- checkBooze(s) step3 <- checkGasoline(s) } yield { “Alrighty! all set!” } }
  • 10.
    10 ValidationNel ● applicative builder|@| ● NonEmptyList - singly linked list to aggregate all errors ● toValidationNel - a helper method def Need(s: Things) = { (packFood.toValidationNel |@| packBooze.toValidationNel ) .. //will return NonEmptyList(no food, no liquor)
  • 11.
    Overview of thearchitecture 1111 Request RubyAPI Response Auth HMAC API Gateway server FunnelResponse FunnelRequest Riak Snowflake ID
  • 12.
    How it allstarted…. package controller import play.api.mvc._ object Logs extends Controller { def list = Action { Ok(views.html.index("Your new application is ready.")) }
  • 13.
    13 object Application extendsController with LoginLogout with AuthConfigImpl {} Authentication object Application extends Controller with HMACAccessElement with Auth with AuthConfigImpl {} Started abusing traits.. play-2 uses Stackable controller
  • 14.
    Authentication def post =StackAction(parse.tolerantText) { implicit request => val input = (request.body).toString() val result = models.Accounts.create(input) result match { case Success(succ) case Failure(err) } ● Use a StackAction in your controller ● It first calls StackAction and composes with other actions
  • 15.
    15 def Authenticated[A](req: FunnelRequestBuilder[A]):ValidationNel[Throwable, Option[String]] = { Logger.debug(("%-20s -->[%s]").format("SecurityActions", "Authenticated: Entry")) req.funneled match { case Success(succ) => { Logger.debug(("%-20s -->[%s]").format("FUNNLEDREQ-S", succ.toString)) (succ map (x => bazookaAtDataSource(x))).getOrElse( Validation.failure[Throwable, Option[String]] (CannotAuthenticateError("""Invalid content in header. API server couldn't parse it""", "Request can't be funneled.")).toValidationNel) } Authentication
  • 16.
    16 Validation - Usecase ●Try Catch is cumbersome ● Handle exceptions as values def create(input: String): ValidationNel[Throwable, Option[AccountResult]]
  • 17.
    17 ValidationNel def create(email: String,input: String): ValidationNel[Throwable, Option[EventsResult]] = { (mkGunnySack(email, input) leftMap { err: NonEmptyList[Throwable] => new ServiceUnavailableError(input, (err.list.map(m => m.getMessage)).mkString("n")) }).toValidationNel.flatMap { gs: Option[GunnySack] => (riak.store(gs.get) leftMap { t: NonEmptyList[Throwable] => t }). flatMap { maybeGS: Option[GunnySack] => maybeGS match { case Some(thatGS) => (parse(thatGS.value).extract[EventsResult].some).successNel[Throwable] case None => { play.api.Logger.warn(("%-20s -->[%s]").format("Events created. success", "Scaliak returned => None. Thats OK.")) (parse(gs.get.value).extract[EventsResult].some).successNel[Throwable]; }}}}} }
  • 18.
    18 for-comprehension interaction withValidationNels private def mkGunnySack(email: String, input: String): ValidationNel[Throwable, Option[GunnySack]] = { play.api.Logger.debug(("%-20s -->[%s]").format("models.tosca.Events", "mkGunnySack:Entry")) play.api.Logger.debug(("%-20s -->[%s]").format("email", email)) play.api.Logger.debug(("%-20s -->[%s]").format("json", input)) val eventsInput: ValidationNel[Throwable, EventsInput] = (Validation.fromTryCatchThrowable[EventsInput,Throwable] { parse(input).extract[EventsInput] } leftMap { t: Throwable => new MalformedBodyError(input, t.getMessage) }).toValidationNel //capture failure for { event <- eventsInput //aor <- (models.Accounts.findByEmail(email) leftMap { t: NonEmptyList[Throwable] => t }) uir <- (UID(MConfig.snowflakeHost, MConfig.snowflakePort, "evt").get leftMap { ut: NonEmptyList[Throwable] => ut }) } yield { //val bvalue = Set(aor.get.id) val bvalue = Set(event.a_id) val json = new EventsResult(uir.get._1 + uir.get._2, event.a_id, event.a_name, event.command, event.launch_type, Time. now.toString).toJson(false) //validated schema new GunnySack(uir.get._1 + uir.get._2, json, RiakConstants.CTYPE_TEXT_UTF8, None, Map(metadataKey -> metadataVal), Map((bindex, bvalue))).some }}
  • 19.
    JSON Serialization case classEventsResult(id: String, a_id: String, a_name: String, command: String, launch_type: String, created_at: String) { def toJson(prettyPrint: Boolean = false): String = if (prettyPrint) {..} import net.liftweb.json.scalaz.JsonScalaz.toJSON import models.json.tosca.EventsResultSerialization ● Easy serialization ● Validated schema, hence no junk gets into your NoSQL
  • 20.
    20 class EventsResultSerialization(charset: Charset= UTF8Charset) extends SerializationBase[EventsResult] { protected val IdKey = "id" ..} override implicit val writer = new JSONW[EventsResult] { override def write(h: EventsResult): JValue = { JObject( JField(IdKey, toJSON(h.id)) :: JField(AssemblyIdKey, toJSON(h.a_id)) :: Nil) }} override implicit val reader = new JSONR[EventsResult] { override def read(json: JValue): Result[EventsResult] = { val idField = field[String](IdKey)(json) val assemblyIdField = field[String](AssemblyIdKey)(json) (idField |@|assemblyIdField |@|assemblyNameField |@|commandField |@| launchTypeField |@| createdAtField) { (id: String, a_id: String, a_name: String, command: String, launch_type: String, created_at: String) => new EventsResult(id, a_id, a_name, command, launch_type, created_at) }}}
  • 21.
    21 either[T,S] - theweird / def getContent(url: String): Either[String, String] = if (url == "google") Left("Requested URL is blocked for the good of the people!") else Right("Nopey!)) ● Either in scala ● Similar in scalaz, called / isLeft, isRight, swap, getOrElse
  • 22.
    22 (for { resp <-eitherT[IO, NonEmptyList[Throwable], Option[AccountResult]] { //disjunction Throwable / Option with a Function IO. (Accounts.findByEmail(freq.maybeEmail.get).disjunction).pure[IO] } found <- eitherT[IO, NonEmptyList[Throwable], Option[String]] { / val fres = resp.get val calculatedHMAC = GoofyCrypto.calculateHMAC(fres.api_key, freq.mkSign) if (calculatedHMAC === freq.clientAPIHmac.get) { (("""Authorization successful for 'email:' HMAC matches: |%-10s -> %s |%-10s -> %s |%-10s -> %s""".format("email", fres.email, "api_key", fres.api_key, "authority", fres.authority). stripMargin) .some).right[NonEmptyList[Throwable]].pure[IO] } else { (nels((CannotAuthenticateError("""Authorization failure for 'email:' HMAC doesn't match: '%s'.""" .format (fres.email).stripMargin, "", UNAUTHORIZED))): NonEmptyList[Throwable]).left[Option[String]].pure[IO]}}} yield found).run.map(_.validation).unsafePerformIO()}}
  • 23.
    Question? Yeshwanth Kumar Platform Engineer MegamSystems (www.megam.io) Twitter: @morpheyesh Email: getyesh@megam.io Docs: docs.megam.io Devcenter: devcenter. megam.io