Finagle / Finchの純粋に機能的なREST API

フィンチ








Finchライブラリの歴史は約1年前にConfettinの地下で始まり、そこでFinagleで REST APIを作成しようとしました 。 finagle-http自体は非常に優れたツールであるという事実にもかかわらず、より豊かな抽象化の深刻な不足を感じ始めました。 さらに、これらの抽象化には特別な要件がありました。 それらは不変で、簡単に構成可能で、同時に非常に単純であると想定されていました。 関数としてシンプル。 そこで、Finchライブラリが登場しました。これは、finagle-httpの上にある非常に薄い関数と型の層であり、finagle-httpでのHTTP(マイクロ|ナノ)サービスの開発をより楽しく簡単にします。



6か月前ライブラリの最初の安定バージョンがリリースされ、先日、バージョン0.5.0がリリースされました 。 この間、 6社 (うち3 はまだ公式リストにありません: MesosphereShponicGlobo.com )がFinchの生産を開始し、そのうちのいくつかは積極的な貢献者にさえなりました。



この投稿では、Finchが構築する3つの柱、 Router



RequestReader



およびResponseBuilder



ます。



Router





io.finch.route



パッケージは、ルートコンビio.finch.route



APIを実装します。これにより、ボックスから利用可能なプリミティブルーターからルーターを組み合わせて、無限の数のルーターを構築できます。 Parser Combinatorsscodecは同じアプローチを使用します。



ある意味では、 Router[A]



は機能Route => Option[(Route, A)]



です。 Router



は抽象ルートRoute



を取り、残りのルートと取得したタイプA



値からOption



を返しますA



つまり、 Router



は、成功した場合(要求をルーティングできる場合Some(...)



Some(...)



返します。



基本的なルーターは、 int



long



string



、およびboolean



4つだけです。 さらに、ルートから値を抽出せず、単純にサンプルにマッピングするルーターがあります(たとえば、HTTPメソッドのルーター: Get



Post



)。



次の例は、ルーターを構成するためのAPIを示しています。 ルーターrouter



は、フォームGET /(users|user)/:id



リクエストをルーティングし、ルートから整数id



値を抽出します。 演算子/



(またはandThen



)に注意してください。この助けを借りて、2つのルーターと演算子|



(またはorElse



)。これにより、論理orElse



or







 val router: Router[Int] => Get / ("users" | "user") / int("id")
      
      





ルーターが複数の値を抽出する必要がある場合は、特別なタイプ/



使用できます。



 case class Ticket(userId: Int, ticketId: Int) val r0: Router[Int / Int] = Get / "users" / int / "tickets" / int val r1: Router[Ticket] = r0 map { case a / b => Ticket(a, b) }
      
      





ルートからサービス(Finagle Service



)を抽出する特別なタイプのルーターがあります。 これらのルーターはエンドポイントと呼ばれます(実際、 Endpoint[Req, Rep]



Router[Service[Req, Rep]]



単なるタイプエイリアスRouter[Service[Req, Rep]]



)。 Endpoint



は暗黙的にFinagleサービスに変換できるため、Finagle HTTP APIでEndpoint



を透過的に使用できます。



 val users: Endpoint[HttpRequest, HttpResponse] = (Get / "users" / long /> GetUser) | (Post / "users" /> PostUser) | (Get / "users" /> GetAllUsers) Httpx.serve(":8081", users)
      
      





RequestReader





抽象化io.finch.request.RequestReader



はFinchのキーです。 明らかに、ほとんどのREST API(ビジネスロジックを除く)は、クエリパラメーターの読み取りと検証を行っています。 これがRequestReader



です。 Finchのすべてのものと同様に、 RequestReader[A]



HttpRequest => Future[A]



関数HttpRequest => Future[A]



。 したがって、 RequestReader[A]



はHTTP要求を受信し、そこからタイプA



値を読み取りますA



主にデータフローサービスの追加のFuture



変換(通常は最初)としてパラメーターを読み取る段階を表すために、結果はFuture



に配置されます。 したがって、 RequestReader



RequestReader



返すFuture.exception



、それ以上の変換は実行されません。 この動作は、パラメータの1つが無効な場合にサービスが実際の作業を行うべきではない場合の99%で非常に便利です。



次の例では、 RequestReader



title



は必要なクエリ文字列パラメーター「title」をNotPresent



か、パラメーターが要求にない場合はNotPresent



例外を返します。



 val title: RequestReader[String] = RequiredParam("title") def hello(name: String) = new Service[HttpRequest, HttpResponse] { def apply(req: HttpRequest) = for { t <- title(req) } yield Ok(s"Hello, $t $name!") }
      
      





io.finch.request



パッケージは、HTTPリクエストからさまざまな情報を読み取るための組み込みのRequestReader



豊富なセットを提供します。クエリ文字列パラメーターからCookieまで。 使用可能なすべてのRequestReader



は、必須(必須)とオプション(オプション)の2つのグループに分けられます。 必須のリーダーはNotPresent



値または例外をNotPresent



、オプションのリーダーはOption[A]



読み取ります。



 val firstName: RequestReader[String] = RequiredParam("fname") val secondName: RequestReader[Option[String]] = OptionalParam("sname")
      
      





ルートコンビネータとRequestReader



RequestReader



は、2つのリーダーを1つに構成するために使用できるAPIを提供します。 そのような2つのAPIがあります:モナド( flatMap



を使用)および適用( ~



を使用)。 モナド構文はおなじみに見えますが、適用可能な構文を使用することを強くお勧めします。これにより、エラーを蓄積できますが、モナドのフェイルファーストの性質は最初のエラーのみを返します。 以下の例は、リーダーを作成するための両方の方法を示しています。



 case class User(firstName: String, secondName: String) // the monadic style val monadicUser: RequestReader[User] = for { firstName <- RequiredParam("fname") secondName <- OptionalParam("sname") } yield User(firstName, secondName.getOrElse("")) // the applicate style val applicativeUser: RequestReader[User] = RequiredParam("fname") ~ OptionalParam("sname") map { case fname ~ sname => User(fname, sname.getOrElse("")) }
      
      





特に、 RequestReader



使用RequestReader



と、リクエストからString以外の型の値を読み取ることができRequestReader



RequestReader.as[A]



メソッドを使用して、読み取り値を変換できます。



 case class User(name: String, age: Int) val user: RequestReader[User] = RequiredParam("name") ~ OptionalParam("age").as[Int] map { case name ~ age => User(fname, age.getOrElse(100)) }
      
      





as[A]



メソッドの魔法は、 DecodeRequest[A]



型の暗黙的なパラメーターに基づいています。 型クラスDecodeRequest[A]



は、 String



から型A



を取得する方法に関する情報を保持します。 変換エラーの場合、 RequestReader



NotParsed



例外を読み取ります。 Int



Long



Float



Double



およびBoolean



型へのそのままの変換がサポートされています。



RequestReader



のJSONサポートは同じ方法で実装されますas[Json]



場合、現在のスコープにDecodeRequest[Json]



実装がある場合、 as[Json]



メソッドを使用できます。 次の例では、 RequestReader



user



は、HTTP要求の本文でJSON形式でシリアル化されたユーザーを読み取ります。



 val user: RequestReader[Json] = RequiredBody.as[Json]
      
      





Jackson JSONライブラリのサポートを考慮すると、 RequestReader



を使用したJSONオブジェクトのRequestReader



大幅に簡素化されます。



 import io.finch.jackson._ case class User(name: String, age: Int) val user: RequestReader[User] = RequiredBody.as[User]
      
      





要求パラメーターは、 RequestReader.should



およびRequestReader.shouldNot



を使用してRequestReader.should



RequestReader.shouldNot



。 検証には2つの方法があります。インラインルールを使用する方法と、既製のValidationRule



を使用する方法です。 以下の例では、 age



リーダーはage



パラメーターを読み取ります。ただし、0より大きく120より小さい場合は、リーダーはNotValid



例外を読み取ります。



 val age: RequestReader[Int] = RequiredParam("age").as[Int] should("be > than 0") { _ > 0 } should("be < than 120") { _ < 120 }
      
      





io.finch.request



パッケージの既製のルールand



ValidationRule



or



/ and



composersを使用して、上記の例をより簡潔なスタイルに書き換えることができます。



 val age: RequestReader[Int] = RequiredParam("age").as[Int] should (beGreaterThan(0) and beLessThan(120))
      
      





ResponseBuilder





io.finch.response



パッケージは、HTTP応答を構築するためのシンプルなAPIを提供します。 Ok



またはCreated



など、応答ステータスコードに対応する特定のResponseBuilder



を使用するのが一般的な方法です。



 val response: HttpResponse = Created("User 1 has been created") // plain/text response
      
      





io.finch.response



パッケージの重要な抽象化は、タイプクラスEncodeResponse[A]



です。 ResponseBuilder



は、現在のスコープに暗黙的な値EncodeResponse[A]



がある場合、任意のタイプA



からHTTP応答を構築できます。 したがって、JSONサポートはResponseBuilder



で実装されます。サポートされる各ライブラリには、 EncodeResponse[A]



実装があります。 次のコードは、 finch-json



モジュールの標準JSON実装との統合を示しています。



 import io.finch.json._ val response = Ok(Json.obj("name" -> "John", "id" -> 0)) // application/json response
      
      





したがって、値EncodeResponse[A]



を目的のタイプの現在のスコープに追加することResponseBuilder



ResponseBuilder



の機能を拡張できます。 たとえば、タイプがUser







 case class User(id: Int, name: String) implicit val encodeUser = EncodeResponse[User]("applciation/json") { u => s"{\"name\" : ${u.name}, \"id\" : ${u.id}}" } val response = Ok(User(10, "Bob")) // application/json response
      
      





おわりに



フィンチは非常に若いプロジェクトであり、「重大な欠陥」のない「銀の弾丸」ではありません。 これは、一部の開発者が作業中のタスクに効果的であると感じるツールにすぎません。 この出版物が、プロジェクトでフィンチを使用/試してみることにしたロシア語を話すプログラマーの出発点になることを願っています。






All Articles