23
Building a RESTful API with Scalaz Yeshwanth Kumar Platform Engineer Megam Systems 1

RESTful API using scalaz (3)

Embed Size (px)

Citation preview

Page 1: RESTful API using scalaz (3)

Building a RESTful API with Scalaz

Yeshwanth KumarPlatform EngineerMegam Systems

1

Page 2: RESTful API using scalaz (3)

Agenda

1. Quick intro to scalaz library

2. REST API - Gateway architecture

3. Real time usecase of scalaz

Page 3: RESTful API using 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

Page 4: RESTful API using scalaz (3)

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

Page 5: RESTful API using scalaz (3)

Quick intro to scalaz

● A library to write functional code in scala

● It is not hard

● Does not require super human powers

Page 6: RESTful API using scalaz (3)

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

Page 7: RESTful API using scalaz (3)

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}

Page 8: RESTful API using scalaz (3)

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

Page 9: RESTful API using scalaz (3)

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!” }

}

Page 10: RESTful API using scalaz (3)

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)

Page 11: RESTful API using scalaz (3)

Overview of the architecture

1111

Request

Ru

by A

PI

Response

Auth

HMAC

API Gateway server

FunnelResponse

FunnelRequest

Riak

Snowflake ID

Page 12: RESTful API using scalaz (3)

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."))

}

Page 13: RESTful API using scalaz (3)

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

Page 14: RESTful API using scalaz (3)

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

Page 15: RESTful API using scalaz (3)

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

Page 16: RESTful API using scalaz (3)

16

Validation - Usecase

● Try Catch is cumbersome

● Handle exceptions as values

def create(input: String): ValidationNel[Throwable, Option[AccountResult]]

Page 17: RESTful API using scalaz (3)

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]; }}}}}

}

Page 18: RESTful API using scalaz (3)

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

}}

Page 19: RESTful API using scalaz (3)

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

Page 20: RESTful API using scalaz (3)

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) }}}

Page 21: RESTful API using scalaz (3)

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

Page 22: RESTful API using scalaz (3)

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()}}

Page 23: RESTful API using scalaz (3)

Question?

Yeshwanth KumarPlatform EngineerMegam Systems(www.megam.io)

Twitter: @morpheyeshEmail: [email protected]

Docs: docs.megam.ioDevcenter: devcenter.megam.io