Finch + Finagle-OAuth2
=
Purely Functional REST API
Vladimir Kostyukov
@vkostyukov
Finagle-OAuth2: Highlights
• Asynchronous version of Nulab’s scala-oauth2-provider
!
• Finagle-friendly API: OAuth2Request, OAuth2Filter	
!
• Does not depend on Finch
2
1 trait DataHandler[U] {	
2 ... 	
3 def findUser(username: String, password: String): Future[Option[U]]	
4 def createAccessToken(authInfo: AuthInfo[U]): Future[AccessToken]	
5 def getStoredAccessToken(authInfo: AuthInfo[U]): Future[Option[AccessToken]]	
6 def findAccessToken(token: String): Future[Option[AccessToken]]	
7 ...	
8 }	
Finagle-OAuth2: Step #1
DataHandler
3
Finagle-OAuth2: Step #2
Typesafe Auth
1 import com.twitter.finagle.oauth2._	
2 import com.twitter.finagle.oauth2.{OAuth2Filter, OAuth2Request}	
3 	
4 val auth = new OAuth2Filter(dataHandler)	
5 	
6 val hello = new Service[OAuth2Request[User], Response] {	
7 def apply(req: OAuth2Request[User]) = {	
8 println(s"Hello, ${req.authInfo.user}!")	
9 Future.value(Response())	
10 }	
11 }	
12 	
13 val backend: Service[Request, Response] = auth andThen hello	
4
1 import com.twitter.finagle.oauth2._	
2 import com.twitter.finagle.oauth2.OAuth2Endpoint	
3 	
4 val tokens: Service[Request, Response] = 	
5 new OAuth2Endpoint(dataHandler) with OAuthErrorInJson with OAuthTokenInJson	
Finagle-OAuth2: Step #3
Issue Acces Token
5
Thin layer of purely-functional basic blocks
on top of Finagle for building composable
REST APIs
Finch
https://github.com/finagle/finch
6
Finch: Highlights
• Was #1 Scala trending repo on GitHub for 95 mins
!
!
!
!
!
!
!
• Happily used in production by 2 customers:
• Konfettin
• JusBrasil
!
• Super simple & lightweight (~ 1.5k SLOC)
7
Finch: Quickstart
1 def hello(name: String) = new Service[HttpRequest, HttpResponse] = {	
2 def apply(req: HttpRequest) = for {	
3 title <- OptionalParam("title")(req)	
4 } yield Ok(s"Hello, ${title.getOrElse("")} $name!")	
5 }	
6 	
7 val endpoint = new Endpoint[HttpRequest, HttpResponse] {	
8 def route = {	
9 // routes requests like '/hello/Bob?title=Mr.'	
10 case Method.Get -> Root / "hello" / name => 	
11 BasicallyAuthorize("user", "password") ! hello(name)	
12 }	
13 }	
14 	
15 val service: Service[HttpRequest, HttpResponse] = endpoint.toService 	
8
Finch: Request Reader
(Reader Monad)
1 trait RequestReader[A] {	
2 	
3 def apply(req: HttpRequest): Future[A]	
4 	
5 def flatMap[B](fn: A => RequestReader[B]): RequestReader[B] = ???	
6 	
7 def map[B](fn: A => B): RequestReader[B] = ???	
8 }	
9
Finch: Request Reader
1 val pagination: RequestReader[(Int, Int)] = for {	
2 offset <- OptionalIntParam("offset")	
3 limit <- OptionalIntParam("limit")	
4 } yield (offset.getOrElse(0), math.min(limit.getOrElse(50), 50))	
5 	
6 val service = new Service[HttpRequest, HttpResponse] {	
7 def apply(req: HttpRequest) = for {	
8 (offsetIt, limit) <- pagination(req)	
9 } yield Ok(s"Fetching items $offset..${offset+limit}")	
10 }	
10
Finch: Params Validation
1 case class User(age: Int)	
2 	
3 val user: RequestReader[User] = 	
4 for { age <- RequiredIntParam("age") } yield User(age)	
5 	
6 val adult: RequestReader[User] = for {	
7 u <- user	
8 _ <- ValidationRule("age", "should be greater then 18") { user.age > 18 }	
9 } yield u	
10 	
11 val u: Future[JsonResponse] = adult(request) map { 	
12 JsonObject("age" -> _.age) 	
13 } handle {	
14 case e: ParamNotFound =>	
15 JsonObject("error" -> e.getMessage, "param" -> e.param)	
16 case e: ValidationFailed => 	
17 JsonObject("error" -> e.getMessage, "param" -> e.param)	
18 }	
11
Finch:
Request Reader Highlights
• RequiredParam, RequiredIntParam, etc 	
• Future.value[A]	
• Future.exception[ParamNotFound]
!
• OptionalParam, OptionalIntParam, etc	
• Future.value[Option[A]]
!
• Multi-value Params: RequiredParams, OptionalIntParams, etc.
• Future.value[List[A]]
!
• Params Validation: ValidationRule	
• Future.value[Unit]	
• Future.exception[ValidationFailed]
12
Finch: Responses
1 // empty response with status 200	
2 val a = Ok()	
3 	
4 // 'plain/text' response with status 404	
5 val b = NotFound("body")	
6 	
7 // 'application/json' response with status 201	
8 val c = Created(JsonObject("id" -> 100))	
9 	
10 // ‘plain/text' response with custom header and status 403	
11 val d = Forbidden.withHeaders("Some-Header" -> "Secret")("body") 	
13
Finch: Endpoints Highlights
• Finch’s Endpoints are composable routers
!
• Endpoints might be treated as Scala’s
PartialFunctions[Request, Service[_, _]]
!
• Endpoints are convertible to Finagle Service’s
!
• Endpoints might be composed with Service’s, Filter’s or
other Endpoint’s.
14
Finch:
Endpoints Composition
1 val ab: Filter[A, C, B, C] = ???	
2 val bc: Endpoint[B, C] = ???	
3 val cd: Service[C, D]	
4 	
5 val ad1: Endpoint[A, D] = ab ! bc ! cd	
6 val ad2: Endpoint[A, D] = ???	
7 	
8 val ad3: Endpoint[A, D] = ad1 orElse ad2	
15
Finagle rocks!
16
• A better JSON
• Lightweight in-memory caching API
Finch: Further Steps
17
• https://github.com/finagle/finch
• https://github.com/finagle/finagle-oauth2
Resources
@vkostyukov
18

Finch + Finagle OAuth2

  • 1.
    Finch + Finagle-OAuth2 = PurelyFunctional REST API Vladimir Kostyukov @vkostyukov
  • 2.
    Finagle-OAuth2: Highlights • Asynchronousversion of Nulab’s scala-oauth2-provider ! • Finagle-friendly API: OAuth2Request, OAuth2Filter ! • Does not depend on Finch 2
  • 3.
    1 trait DataHandler[U]{ 2 ... 3 def findUser(username: String, password: String): Future[Option[U]] 4 def createAccessToken(authInfo: AuthInfo[U]): Future[AccessToken] 5 def getStoredAccessToken(authInfo: AuthInfo[U]): Future[Option[AccessToken]] 6 def findAccessToken(token: String): Future[Option[AccessToken]] 7 ... 8 } Finagle-OAuth2: Step #1 DataHandler 3
  • 4.
    Finagle-OAuth2: Step #2 TypesafeAuth 1 import com.twitter.finagle.oauth2._ 2 import com.twitter.finagle.oauth2.{OAuth2Filter, OAuth2Request} 3 4 val auth = new OAuth2Filter(dataHandler) 5 6 val hello = new Service[OAuth2Request[User], Response] { 7 def apply(req: OAuth2Request[User]) = { 8 println(s"Hello, ${req.authInfo.user}!") 9 Future.value(Response()) 10 } 11 } 12 13 val backend: Service[Request, Response] = auth andThen hello 4
  • 5.
    1 import com.twitter.finagle.oauth2._ 2import com.twitter.finagle.oauth2.OAuth2Endpoint 3 4 val tokens: Service[Request, Response] = 5 new OAuth2Endpoint(dataHandler) with OAuthErrorInJson with OAuthTokenInJson Finagle-OAuth2: Step #3 Issue Acces Token 5
  • 6.
    Thin layer ofpurely-functional basic blocks on top of Finagle for building composable REST APIs Finch https://github.com/finagle/finch 6
  • 7.
    Finch: Highlights • Was#1 Scala trending repo on GitHub for 95 mins ! ! ! ! ! ! ! • Happily used in production by 2 customers: • Konfettin • JusBrasil ! • Super simple & lightweight (~ 1.5k SLOC) 7
  • 8.
    Finch: Quickstart 1 defhello(name: String) = new Service[HttpRequest, HttpResponse] = { 2 def apply(req: HttpRequest) = for { 3 title <- OptionalParam("title")(req) 4 } yield Ok(s"Hello, ${title.getOrElse("")} $name!") 5 } 6 7 val endpoint = new Endpoint[HttpRequest, HttpResponse] { 8 def route = { 9 // routes requests like '/hello/Bob?title=Mr.' 10 case Method.Get -> Root / "hello" / name => 11 BasicallyAuthorize("user", "password") ! hello(name) 12 } 13 } 14 15 val service: Service[HttpRequest, HttpResponse] = endpoint.toService 8
  • 9.
    Finch: Request Reader (ReaderMonad) 1 trait RequestReader[A] { 2 3 def apply(req: HttpRequest): Future[A] 4 5 def flatMap[B](fn: A => RequestReader[B]): RequestReader[B] = ??? 6 7 def map[B](fn: A => B): RequestReader[B] = ??? 8 } 9
  • 10.
    Finch: Request Reader 1val pagination: RequestReader[(Int, Int)] = for { 2 offset <- OptionalIntParam("offset") 3 limit <- OptionalIntParam("limit") 4 } yield (offset.getOrElse(0), math.min(limit.getOrElse(50), 50)) 5 6 val service = new Service[HttpRequest, HttpResponse] { 7 def apply(req: HttpRequest) = for { 8 (offsetIt, limit) <- pagination(req) 9 } yield Ok(s"Fetching items $offset..${offset+limit}") 10 } 10
  • 11.
    Finch: Params Validation 1case class User(age: Int) 2 3 val user: RequestReader[User] = 4 for { age <- RequiredIntParam("age") } yield User(age) 5 6 val adult: RequestReader[User] = for { 7 u <- user 8 _ <- ValidationRule("age", "should be greater then 18") { user.age > 18 } 9 } yield u 10 11 val u: Future[JsonResponse] = adult(request) map { 12 JsonObject("age" -> _.age) 13 } handle { 14 case e: ParamNotFound => 15 JsonObject("error" -> e.getMessage, "param" -> e.param) 16 case e: ValidationFailed => 17 JsonObject("error" -> e.getMessage, "param" -> e.param) 18 } 11
  • 12.
    Finch: Request Reader Highlights •RequiredParam, RequiredIntParam, etc • Future.value[A] • Future.exception[ParamNotFound] ! • OptionalParam, OptionalIntParam, etc • Future.value[Option[A]] ! • Multi-value Params: RequiredParams, OptionalIntParams, etc. • Future.value[List[A]] ! • Params Validation: ValidationRule • Future.value[Unit] • Future.exception[ValidationFailed] 12
  • 13.
    Finch: Responses 1 //empty response with status 200 2 val a = Ok() 3 4 // 'plain/text' response with status 404 5 val b = NotFound("body") 6 7 // 'application/json' response with status 201 8 val c = Created(JsonObject("id" -> 100)) 9 10 // ‘plain/text' response with custom header and status 403 11 val d = Forbidden.withHeaders("Some-Header" -> "Secret")("body") 13
  • 14.
    Finch: Endpoints Highlights •Finch’s Endpoints are composable routers ! • Endpoints might be treated as Scala’s PartialFunctions[Request, Service[_, _]] ! • Endpoints are convertible to Finagle Service’s ! • Endpoints might be composed with Service’s, Filter’s or other Endpoint’s. 14
  • 15.
    Finch: Endpoints Composition 1 valab: Filter[A, C, B, C] = ??? 2 val bc: Endpoint[B, C] = ??? 3 val cd: Service[C, D] 4 5 val ad1: Endpoint[A, D] = ab ! bc ! cd 6 val ad2: Endpoint[A, D] = ??? 7 8 val ad3: Endpoint[A, D] = ad1 orElse ad2 15
  • 16.
  • 17.
    • A betterJSON • Lightweight in-memory caching API Finch: Further Steps 17
  • 18.