Open10

おためしcadl

nikuniku

cadl とはなにか。
cadl は GitHub の microsoft リポジトリにいる https://github.com/microsoft/cadl
2022-03-27 の時点では GitHub の starred が 62

cadl の README には以下のように書いてある。

Cadl is a language for describing cloud service APIs and generating other API description languages, client and service code, documentation, and other assets.

cadlはクラウドサービスのAPI群を記述して、別のAPI記述言語、クライアントやサービスのコード、ドキュメント、その他のアセットを生成するもの。と書いてある。
(別にクラウドサービスのAPIに限らないとおもうんだけど、どうしてクラウドって書いてあるんだろ)

Cadl provides highly extensible core language primitives that can describe API shapes common among REST, GraphQL, gRPC, and other protocols.

Cadl は拡張性の高い言語コアを提供していて、REST、GraphQL、gRPCなどのプロトコルと共通したAPIの枠組みを記述できる。

nikuniku

cadl では定義をどんなふうに書くのか。

Introduction to API Definition Language (Cadl) をみると感じがつかめそう。

string 型の name と、 string 型か null を持つ Dog のモデルと、dog を取得するための operations getDog を書くと以下のようになる。

model Dog {
  name: string;
  favoriteToy?: string;
}

op getDog(name: string): Dog;

他の要素も TypeScript に似ているかもしれないなという印象。

nikuniku

main.cadl を以下のようにつくって

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

using Cadl.Http;

model Dog {
  name: string;
  favoriteToy?: string;
}

@get
@route("/dog")
op getDog(name: string): Dog;

npx cadl compile . --emit @cadl-lang/openapi3

すると cadl-output/openapi.json が以下のように出力された

{
  "openapi": "3.0.0",
  "info": {
    "title": "(title)",
    "version": "0000-00-00"
  },
  "tags": [],
  "paths": {
    "/dog": {
      "get": {
        "operationId": "getDog",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Ok",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Dog"
                }
              }
            }
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "string"
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Dog": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "favoriteToy": {
            "type": "string"
          }
        },
        "required": [
          "name"
        ]
      }
    }
  }
}
nikuniku

Cadl for the OpenAPI developerというドキュメントがあるな。

This guide is an introduction to Cadl using concepts that will be familiar to developers that either build or use API definitions in OpenAPI v2 or v3.

In many case, this will also describe how the cadl-autorest and openapi3 emitters translate Cadl designs into OpenAPI.

nikuniku

OpenAPI の json 形式なのかあ。yaml 形式に出力できないかなと思ったけど、そういったオプションを探すために yaml で grep してみたけれど今のところ特にドキュメントにも、コードにも、設定ファイルにもなさそうだった。

設定ファイルには

Cadl has a configuration file cadl-project.yaml that right now is only used to configure the default emitter to use.

という記述があるし、出しわけるというのは今のところなさそう。まあ json を出してから yaml に変換するのでもいいか。

nikuniku

完全に横道に逸れてしまったが yq という yaml をいじるためのシングルバイナリになるツールがあるので、それを使って cat openapi.json| yq --prettyPrint すると、見慣れた形式になった。

openapi: 3.0.0
info:
  title: (title)
  version: 0000-00-00
tags: []
paths:
  /dog:
    get:
      operationId: getDog
      parameters: []
      responses:
        "200":
          description: Ok
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Dog'
      requestBody:
        content:
          application/json:
            schema:
              type: string
components:
  schemas:
    Dog:
      type: object
      properties:
        name:
          type: string
        favoriteToy:
          type: string
      required:
        - name
nikuniku

OpenAPIのpetstore.jsonと機能上ほぼ同一のcadlを書いた。

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

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

using Cadl.Http;

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

// TODO: Pets というスキーマを作る方法があるか調べる
alias Pets = Pet[];

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

@get
@route("/pets")
@operationId("listPets")
@tag("pets")
op listPets(@query @doc("How many items to return at one time max(100)") limit?: int32): {
  // TODO: responses - 200 - description の値を書き換える方法を調べる
  @statusCode statusCode: 200;
  @header @doc("A link to the next page of responses") "x-next": string;
  @body body: Pets
} | Error;

@post
@route("/pets")
@operationId("createPets")
@tag("pets")
op createPets(): {
  // TODO: responses - 201 - description の値を書き換える方法を調べる
  @statusCode statusCode: 201;
} | Error;

@get
@route("/pets")
@operationId("showPetById")
@tag("pets")
op showPetById(@path @doc("The id of the pet to retrieve") petId: string): OkResponse<Pet> | Error;
{
  "openapi": "3.0.0",
  "info": {
    "title": "Swagger Petstore",
    "version": "1.0.0"
  },
  "tags": [
    {
      "name": "pets"
    }
  ],
  "paths": {
    "/pets": {
      "get": {
        "operationId": "listPets",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "How many items to return at one time max(100)",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ok",
            "headers": {
              "x-next": {
                "description": "A link to the next page of responses",
                "schema": {
                  "type": "string",
                  "description": "A link to the next page of responses"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Pet"
                  },
                  "x-cadl-name": "PetStore.Pet[]"
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "pets"
        ]
      },
      "post": {
        "operationId": "createPets",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Created"
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "pets"
        ]
      }
    },
    "/pets/{petId}": {
      "get": {
        "operationId": "showPetById",
        "parameters": [
          {
            "name": "petId",
            "in": "path",
            "required": true,
            "description": "The id of the pet to retrieve",
            "schema": {
              "type": "string",
              "description": "The id of the pet to retrieve"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The request has succeeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pet"
                }
              }
            }
          },
          "default": {
            "description": "unexpected error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "tags": [
          "pets"
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "Pet": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64"
          },
          "name": {
            "type": "string"
          },
          "tag": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "code": {
            "type": "integer",
            "format": "int32"
          },
          "message": {
            "type": "string"
          }
        },
        "description": "unexpected error",
        "required": [
          "code",
          "message"
        ]
      }
    }
  },
  "servers": [
    {
      "url": "https://http://petstore.swagger.io/v1"
    }
  ]
}
nikuniku

もうちょっとまとめたり直したりした。

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;
}

元にした petstore.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"
       }
     }
   }