おためしcadl
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の枠組みを記述できる。
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 に似ているかもしれないなという印象。
vscode で cadl を書いてみる。
はじめかたは https://github.com/microsoft/cadl#getting-started にある。Docker と Node & NPM の方法があるが、ここではより詳細な手順が記載されていそうな Node & NPM のほうを実行する。
VSCode の拡張もあるじゃないか。いいですね。
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"
]
}
}
}
}
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.
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 に変換するのでもいいか。
完全に横道に逸れてしまったが 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
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"
}
]
}
License は Not currently supported.
summary は Not currently supported.
tags が top レベルにも追加されちゃうのはたぶんバグなので issue で質問した
doc が parameter と parameter の schema 両方に追加されちゃうのはたぶんバグなので issue で質問した
もうちょっとまとめたり直したりした。
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"
}
}
}