1. 序章

このチュートリアルでは、Finchを使用したRESTAPIの構築について説明します。

2. フィンチとフィナグル

フィンチとは? そのGitHubreadmeによると:

Finchは、構成可能なHTTPAPIを構築するためのFinagleの上にある純粋に機能的な基本ブロックの薄層です。 その使命は、開発者にベアメタルFinagleAPIに可能な限り近いシンプルで堅牢なHTTPプリミティブを提供することです。

言い換えれば、それは Finagleの純粋に機能的な*フロントエンド*であり、JVM上に高同時実行サーバーを構築するための強力なRPCシステムです。 Finagleの良いところは、さまざまなネットワークサーバーとクライアント(サービス)です。特定のサービスが使用するプロトコルに関係なく、共通のビルディングブロックを使用してプログラムできます。 Finchは、主にHTTPサービスの構築に重点を置いています。 HTTPサービスは、単に要求と応答のペアです。特定の種類の要求に対して、適切な応答を返します。

FinchとFinagleはどちらも、Twitterによって開発および保守されています。

3. 依存関係

Finchを使用するには、finch-coreおよびfinch-circeの依存関係を追加する必要があります。

libraryDependencies ++= Seq(
  "com.github.finagle" %% "finch-core" % "0.31.0",
  "com.github.finagle" %% "finch-circe" % "0.31.0",
  "io.circe" %% "circe-generic" % "0.9.0"
)

4. こんにちは、World API

簡単なHello、World!の例から始めましょう。

object Main extends App {
  val hello: Endpoint[String] = get("hello") { Ok("Hello, World!") }

  Await.ready(Http.server.serve(":8081", hello.toService))
}

2行目では、新しいFinchエンドポイントを定義しました。 Finchエンドポイントは単なる抽象化です。HTTPメソッドに関連付けられ、リクエストを受け入れ、定義した特定のレスポンスで応答するHTTPエンドポイントです。後で説明するように、さまざまなエンドポイントを非常に簡単に構成できます。 、豊富なAPIを構築できます。

ここで、FinchはFinagleの純粋な機能ラッパーであることを思い出してください。 それは、私たちがまだ内部でFinagleを扱っていることを意味します。 Httpサーバー自体はFinagleからのものであり、Finagleサービスのみを提供する方法を知っているため、FinchエンドポイントをFinagleサービスに変換する必要があります。 これはまさに、4行目のtoServiceメソッドの呼び出しと同じです。

最後に、これをHttpサーバーのserveメソッドへの引数として渡します。 serve メソッドは、Httpを介してサービスを提供する方法を知っています。

sbt run、を実行すると、 http:// localhost:8081 /helloに移動して結果を確認できます。

$ curl localhost:8081/hello
Hello, World!

4.1. JSON本文を受け入れる

別のエンドポイントを追加しましょう。 今回は、複合Finch Endpointを使用して名前と姓を含むJSON本文を受け入れます。 そのためには、パスとリクエストの本文を照合する必要があります。

case class FullName(first: String, last: String)

val helloName: Endpoint[String] = post("hello" :: jsonBody[FullName]) { name: FullName =>
  Ok(s"Hello, ${name.first} ${name.last}!")
}

最後に、次のようにHttpサーバーのserveメソッド呼び出しのサービス引数を変更します。

Await.ready(Http.server.serve(":8081", (hello :+: helloName).toService))

JSON本体を使用してAPIを呼び出しましょう。

curl -X POST -H "Content-Type: application/json" -d '{"first":"John", "last":"Doe"}' localhost:8081/hello
"Hello, John Doe!"

前述したように、フィンチエンドポイントを構成できます。その方法と構成の意味を確認するために、上記の例で使用した記号、つまり(::)と(:+:)。これらはコンビネーターと呼ばれます。

最初のもの、(::)、私たちは「そしてそれから」と読みます。 この例では、エンドポイントが文字列「hello」とフルネーム JSON本体。 つまり、新しいHTTPエンドポイントについて説明します。 パスが「/hello」で、firstlastの2つのフィールドを含むJSON本文を受け入れるエンドポイントと一致する必要があります。

(:+ :)を「またはelse」と読み、代替案を説明するために使用されます。エンドポイントhelloまたはhelloNameのいずれかに一致します。

4.2. JSONのシリアル化と解析に関する注意

JSONデータを処理するためのScalaライブラリはたくさんあります。 このチュートリアルでは、CatsとShapelessの上に構築された純粋に機能的なライブラリであるCirceを使用しています。

最も注目に値するのは、私たちの側で特別な努力をすることなくプレーンなScalaケースクラスとの間でJSONを自動的にマッピングする機能を備えていることです

Finchは、Circeだけでなく、Argonaut、Jackson、JSON4などの他のライブラリにもすぐに使用できる統合を提供します。

5. 完全なRESTAPIの例

次に、単純なtodoアプリのREST原則に基づいてより完全なAPIを構築しようとします。 ToDoデータを保存するには、SQLiteデータベースを使用します。 クエリを実行するために、Scala用の純粋に機能的で強く型付けされたJDBCレイヤーであるDoobieを使用します。

これまで使用してきた標準のEndpointの代わりに、CatsIOエフェクトを使用してリアクティブな方法でリクエストを処理したいと考えています。 この目的のために、 Endpoint [IO、_]が必要です。 これにより、安全なシーケンシャルな方法でインターリーブしてデータベース呼び出しを行うことができます。

5.1. 依存関係

今回必要な依存関係は少し異なります。 finch-coreおよびfinchの代わりに、 finchx- *に切り替えます。これにより、Endpoint [IO、_などの多形エンドポイントを使用できるようになります。 ]。

さらに、IOを処理するためのいくつかのヘルパー依存関係とデータベースを処理するための依存関係を含めます。

libraryDependencies ++= Seq(
  "com.github.finagle" %% "finchx-core" % "0.31.0",
  "com.github.finagle" %% "finchx-circe" % "0.31.0",
  
  "io.circe" %% "circe-generic" % "0.9.0",
  "org.typelevel" %% "cats-effect" % "2.1.3",
  "org.typelevel" %% "cats-core" % "2.1.1",
  "org.xerial" % "sqlite-jdbc" % "3.31.1",
  "org.tpolecat" %% "doobie-core" % "0.8.8",
)

5.2. 新しいTodoを作成します

Todoモデルを作成しましょう。 CirceはJSON変換を自動的に把握できるため、ケースクラスを使用します。

case class Todo(
  id: Option[Int],
  name: String,
  description: String,
  done: Boolean
)

次に、対応するエンドポイントを定義します。

val createTodo: Endpoint[IO, Todo] = post(todosPath :: jsonBody[Todo]) { todo: Todo =>
  for {
    id <- sql"insert into todo (name, description, done) values (${todo.name}, ${todo.description}, ${todo.done})"
      .update
      .withUniqueGeneratedKeys[Int]("id")
      .transact(xa)
    created <- sql"select * from todo where id = $id"
      .query[Todo]
      .unique
      .transact(xa)
  } yield Created(created)
}

ご覧のとおり、エンドポイントは、Todoモデルに準拠するJSON本文を受け入れるpostリクエストで構成されています。

本体にあるものを取得し、データベースに新しいレコードを作成して、応答で返します。

実際、新しいエンドポイントに何かをPOSTすると、新しく作成されたレコードが返されることを確認できます。

$ curl -X POST -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": false}' \
localhost:8081/todos

{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}

5.3. 特定のTodoを入手する

データベースからオブジェクトを取得することもかなり簡単です。

val getTodo: Endpoint[IO, Todo] = get(todosPath :: path[Int]) { id: Int =>
  for {
    todos <- sql"select * from todo where id = $id"
      .query[Todo]
      .to[Set]
      .transact(xa)
  } yield todos.headOption match {
    case None => NotFound(new Exception("Record not found"))
    case Some(todo) => Ok(todo)
  }
}

これはgetリクエストのエンドポイントであり、整数パラメーター(id)を受け入れ、応答でTodoレコードを返します。

探しているtodoがデータベースにない可能性があるため、オプション値と照合し、クエリの結果に従って応答します。

$ curl localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}

存在しないtodoを要求すると、404が返されます。

$ curl -i localhost:8081/todos/0
HTTP/1.1 404 Not Found
Date: Wed, 27 May 2020 00:33:28 GMT
Server: Finch
Content-Length: 0

5.4. すべてのTodoを取得

同様に、APIユーザーがToDoの完全なリストを取得できるようにします。

val getTodos: Endpoint[IO, Seq[Todo]] = get(todosPath) {
  for {
    todos <- sql"select * from todo"
      .query[Todo]
      .to[Seq]
      .transact(xa)
  } yield Ok(todos)
}

今回は、すべてのレコードと照合するため、パラメータを受け入れません。

$ curl localhost:8081/todos
[{"id":1,"name":"Hello, world","description":"From Baeldung","done":false},
{"id":2,"name":"Update Endpoint","description":"To be able to mark as completed","done":false},
{"id":3,"name":"Delete Todo Endpoint","description":"To be able to delete todos","done":false}]

5.5. 完了としてマークする

また、todoを更新できるようにしたいと考えています。 ToDoに完了のマークを付けたり、名前や説明を変更したりするとします。 この目的のために、次のエンドポイントを追加しましょう。

val updateTodo: Endpoint[IO, Todo] = put(todosPath :: path[Int] :: jsonBody[Todo]) { (id: Int, todo: Todo) => 
  for {
    _ <- sql"update todo set name = ${todo.name}, description = ${todo.description}, done = ${todo.done} where id = $id"
      .update
      .run
      .transact(xa)
       
    todo <- sql"select * from todo where id = $id"
      .query[Todo]
      .unique
      .transact(xa)
  } yield Ok(todo)
}

この特定のエンドポイントは、以前のエンドポイントの作成に似ています。 違いは方法にあります。postではなくputであり、既存のデータを更新する意味があります。

ToDoエントリの1つを更新し、完了としてマークします。

$ curl -X PUT -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": true}' \
localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":true}

5.6. Todoを削除します

最後に、todoも削除できるようにしましょう。

val deleteTodo: Endpoint[IO, Unit] = delete(todosPath :: path[Int]) { id: Int =>
  for {
    _ <- sql"delete from todo where id = $id"
      .update
      .run
      .transact(xa)
  } yield NoContent
}

ここでも、類似点に注意してください。 このエンドポイントは、idパラメーターを受け入れるため、getエンドポイントと非常によく似ています。 違いは、getの代わりにdeleteを使用することです。

レコードを削除したときの応答には内容がありません。

$ curl -i -X DELETE localhost:8081/todos/3
HTTP/1.1 204 No Content
Date: Wed, 27 May 2020 01:05:08 GMT
Server: Finch

6. 結論

前のセクションでは、関数型プログラミングの原則を使用してTwitterのFinchでRESTAPIを構築する方法について説明しました。

より詳細なセットアップが必要な場合は、Finchの公式ドキュメントを確認してください。

また、SQLステートメントを実行するためにDoobieと統合するのがいかに簡単かを示しました。

いつものように、すべてのコード例はGitHubから入手できます。