API記述言語cadlを試した


コードやドキュメントを生成するために、API の仕様を記述する目的に特化した言語 cadl を試した。

今のところ cadl を元に OpenAPI の生成ができ、README によると GraphQL, gRPC などの生成も視野に入っているようだ。OpenAPI を生成するくらいなら、直接 OpenAPI を使えばよいというのは、一理ある。違いはなんだろうか。

OpenAPI の利用例としてよく利用される PetStore というスキーマ定義と似たコードが出力されるように cadl で書いた API 仕様だ。

リンク先の json のスキーマ定義と見比べると、人間がより容易に読み書きできそうなのは cadl であるように思えないだろうか。

import "@cadl-lang/rest";
import "@cadl-lang/openapi";
import "@cadl-lang/openapi3";

@serviceVersion("1.0.0")
@serviceTitle("Swagger Petstore")
@serviceHost("petstore.swagger.io/v1")
namespace PetStore;

using Cadl.Http;

model Pet {
  id: int64;
  name: string;
  tag?: string;
}

@defaultResponse
@doc("unexpected error")
model Error {
  code: int32;
  message: string;
}

@route("/pets")
namespace Pets {
  @get
  @operationId("listPets")
  @tag("pets")
  op listPets(
    @query @doc("How many items to return at one time (max 100)") limit?: int32
  ): {
    @statusCode statusCode: 200;
    @header @doc("A link to the next page of responses") "x-next": string;
    @body body: Pet[];
  } | Error;

  @post
  @operationId("createPets")
  @tag("pets")
  op createPets(): CreatedResponse | Error;

  @get
  @operationId("showPetById")
  @tag("pets")
  op showPetById(
    @path @doc("The id of the pet to retrieve") petId: string
  ): OkResponse<Pet> | Error;
}
OpenAPI の定義( petstore.json )、cadl から生成した定義( openapi.json )を元に、差分が出すぎないように意味をかえない変更を加えたものの diff
--- petstore.json	2022-04-06 21:39:43.000000000 +0900
+++ openapi.json	2022-04-06 21:48:03.000000000 +0900
@@ -1,21 +1,22 @@
 {
   "openapi": "3.0.0",
   "info": {
-    "version": "1.0.0",
     "title": "Swagger Petstore",
-    "license": {
-      "name": "MIT"
-    }
+    "version": "1.0.0"
   },
   "servers": [
     {
-      "url": "http://petstore.swagger.io/v1"
+      "url": "https://petstore.swagger.io/v1"
+    }
+  ],
+  "tags": [
+    {
+      "name": "pets"
     }
   ],
   "paths": {
     "/pets": {
       "get": {
-        "summary": "List all pets",
         "operationId": "listPets",
         "tags": [
           "pets"
@@ -34,19 +35,24 @@
         ],
         "responses": {
           "200": {
-            "description": "A paged array of pets",
+            "description": "Ok",
             "headers": {
               "x-next": {
                 "description": "A link to the next page of responses",
                 "schema": {
-                  "type": "string"
+                  "type": "string",
+                  "description": "A link to the next page of responses"
                 }
               }
             },
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/Pets"
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/Pet"
+                  },
+                  "x-cadl-name": "PetStore.Pet[]"
                 }
               }
             }
@@ -64,14 +70,14 @@
         }
       },
       "post": {
-        "summary": "Create a pet",
         "operationId": "createPets",
         "tags": [
           "pets"
         ],
+        "parameters": [],
         "responses": {
           "201": {
-            "description": "Null response"
+            "description": "The request has succeeded and a new resource has been created as a result."
           },
           "default": {
             "description": "unexpected error",
@@ -88,7 +94,6 @@
     },
     "/pets/{petId}": {
       "get": {
-        "summary": "Info for a specific pet",
         "operationId": "showPetById",
         "tags": [
           "pets"
@@ -100,13 +105,14 @@
             "required": true,
             "description": "The id of the pet to retrieve",
             "schema": {
-              "type": "string"
+              "type": "string",
+              "description": "The id of the pet to retrieve"
             }
           }
         ],
         "responses": {
           "200": {
-            "description": "Expected response to a valid request",
+            "description": "The request has succeeded.",
             "content": {
               "application/json": {
                 "schema": {
@@ -150,12 +156,6 @@
           }
         }
       },
-      "Pets": {
-        "type": "array",
-        "items": {
-          "$ref": "#/components/schemas/Pet"
-        }
-      },
       "Error": {
         "type": "object",
         "required": [
@@ -170,7 +170,8 @@
           "message": {
             "type": "string"
           }
-        }
+        },
+        "description": "unexpected error"
       }
     }
   }

私が cadl に期待していることは 今さらProtocol Buffersと、手に馴染む道具の話 で yugui さんが Protobuf の良さについて話しているときの

簡素で可読で、割と何にでも使えて、しかしすべてをカバーしようとして膨れあがっておらず、ツールを拡張可能。とりわけ何かがすごく良いという訳でもないけれども、すこし使い込めばこの素朴さが手に馴染みやすい。

と同じだろう。

それでは Protobuf でいいとも思えるのだが、シリアライゼーション形式記述としての Protobuf とスキーマ形式記述の Protobuf を分けて捉えられるようになっていないと、タグの記述の見慣れなさや、タグに何を書いたらいいかわからなくなるところが Protobuf でスキーマを記述するときの難点だと思う。

cadl はシリアライゼーション形式記述を第一目的とした言語ではないので、スキーマ記述に特化できていて、なおかつ人間が半年くらいのブランクを経たあとでも読み書き可能そうな簡潔さを備えているように見える。