LoginSignup
6
2

More than 3 years have passed since last update.

スキーマ駆動開発は素晴らしいものだと思うがYAMLを書きたいわけじゃない(Scala編)

Last updated at Posted at 2020-12-23

この記事はこれは ただの集団 Advent Calendar 2020 の24日目の記事です。

はじめに

皆さん、Schema Driven Development(スキーマ駆動開発)を行っているでしょうか。
スキーマ駆動開発とは
「最初にサービスのスキーマを定義してから、その定義を使い、フロントエンド・サーバサイドが開発を進め、最後に結合する」という開発手法です。
DX Criteria1でもスキーマ駆動開発は推奨されています。

背景

スキーマ駆動開発(OpenAPI)を採用してから、スキーマが違うことによるエラーに悩まされることがなくなったことには大変満足していました。
しかし、ん?と思う場面もありました。

  • フロントエンド・サーバーサイドの両方のエンジニアが協力してスキーマの定義を行うが、スキーマ定義がDatabaseに強く依存するため、サーバーサイドの人が主に書いている
  • YAMLファイルを手で書いている(Stoplight Studio等を使えばある程度は軽減されるが選択することは必要)
  • YAMLファイルからScalaコードを生成しているが2、独自のclassやmethodを使いたい場合にtempleteの作成が必要

Scalaの場合、akka-httpのroutesにアノテーションをつけることで、yamlを生成する機能3がありますが、デバッグが困難など、アノテーション特有の問題をかかえることになります。

正直に言います。YAMLファイルを手書きで書きたくないです。

YAMLを書くのはサーバーサイドの人なのだから、サーバーサイドの言語で書けて、後でドキュメントが生成できればよいのではないでしょうか?
そこで登場するのがtapirです。

tapirとは

関数型プログラミングではプログラムの記述と実行の分離がよく行われている(MonixのTaskやZIOのIOなど)、この考え方をHTTPのエンドポイントにも当てはめたのがtapir(Typed API descRiptions)で、API定義の記述と実行をいい感じに分離してくれます。

sample実装

sample実装として、tapir + http4s + zioで実装しました。
全容はこちらになります。
tapirからはこの組み合わせのライブラリが提供されているので4実装は難しくありません。

API定義の記述と実行の分離

API定義の記述と実行の分離を記述すると下記になります。
APIのinputとoutputを型安全で記述できていることがわかると思います。

  // API定義の記述
  private val postUser: ZEndpoint[User, ClientError, User] =
    endpoint.post
      .in("users")
      .in(jsonBody[User])  // input body
      .out(jsonBody[User]) // output body
      .errorOut(httpErrors)

  // 実行
  private val postUserRoute: URIO[UserServiceEnv, HttpRoutes[Task]] =
    postUser
      .toRoutesR(postUser =>
        UserService
          .createUser(postUser)
          .mapToClientError
      )

routeの合成とドキュメント生成ロジック

routeの合成とドキュメント生成ロジックを記述すると下記になります。
routeの合成はドキュメント生成ロジックには必要ないのですが、routes作成時のsampleとして記述しています。
ドキュメント生成ロジックは
1. .toOpenAPIでrouteをオブジェクト化
2. .toYamlで1をyaml化(string)
3. new SwaggerHttp4s(userDocs).routes[Task]でswaggerで表示できるようにしています

UserRoutes.scala
  // routeの合成
  val routes: URIO[UserServiceEnv, HttpRoutes[Task]] = for {
    postUserRoute   <- postUserRoute
    getUserRoute    <- getUserRoute
    deleteUserRoute <- deleteUserRoute
  } yield postUserRoute <+> getUserRoute <+> deleteUserRoute

  // routeをOpenAPIのオブジェクトに変更
  val docs = List(postUser, getUser, deleteUser).toOpenAPI("User manager", "0.1")
Server.scala
object Server {

  // YAML化
  private val userDocs = UserRoutes.docs.toYaml

  private val appRoutes: URIO[UserServiceEnv with HealthCheck, HttpApp[Task]] =
    for {
      userRoutes        <- UserRoutes.routes
      healthCheckRoutes <- HealthCheckRoutes.routes
      docsRoutes         = new SwaggerHttp4s(userDocs).routes[Task] // SwaggerにYAMLを食わせてエンドポイント化
    } yield (userRoutes <+> healthCheckRoutes <+> docsRoutes).orNotFound

  // Server起動
  val runServer: ZIO[AppEnv, Throwable, Unit] =
    for {
      app                          <- appRoutes
      svConfig                     <- Config.httpServerConfig
      implicit0(r: Runtime[Clock]) <- ZIO.runtime[Clock]
      bec                          <- blocking.blocking(ZIO.descriptor.map(_.executor.asEC))
      _ <-
        BlazeServerBuilder[Task](bec)
          .bindHttp(svConfig.port.value, svConfig.host.value)
          .withoutBanner
          .withNio2(true)
          .withHttpApp(app)
          .serve
          .compile
          .drain
    } yield ()

}

ドキュメント

エンドポイントを叩くと下記のようにswaggerのドキュメント画面になります。
リンクをクリックするとYAMLをダウンロードできます(勿論ロジックを書いて内部に保存しても良いと思います)
あとはこのYAMLからopenapi-generator2を使い、フロントエンド用にTypeScriptを生成すればよいのではないでしょうか。

スクリーンショット 2020-12-22 19.46.21.png

おわりに

今回の執筆理由は、実際に筆者がスキーマ駆動開発でOpenAPIを採用し、スキーマを頑張って書いているときに自分は一体何をしているんだ?なんでYAML書いているんだ?という考えに至りました。
スキーマを書く作業が一段落したあとに、何かないかなーと調べたときに出会ったのがtapirです。
今後も疑問に思ったことは、解決手段を探し、なければ作成し、日々精進していきたいと思います。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2