Upload
yeshwanth-kumar
View
1.010
Download
0
Embed Size (px)
Citation preview
Building a RESTful API with Scalaz
Yeshwanth KumarPlatform EngineerMegam 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” == 1Boolean = false
In scalaz..scala>import scala._
scala>“john” === 2Compilation 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”.showres4: 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
Ru
by A
PI
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 KumarPlatform EngineerMegam Systems(www.megam.io)
Twitter: @morpheyeshEmail: [email protected]
Docs: docs.megam.ioDevcenter: devcenter.megam.io