この記事はRecruit Engineers Advent Calendar 2020の19日目の記事です。
CREATE 文から OpenAPI を自動生成するsql-swagger-generatorというCLIを開発しました。
作ったきっかけ
アドベントカレンダーのネタに困っていたところ、0-1のフェーズで OpenAPI を書く必要があったため、ちょうどいいかなと思い実装してみました。
OpenAPIの説明をした後に、開発したCLIの紹介をしていきたいと思います!
OpenAPI とは
OpenAPI(Swagger)は RESTful API を構築するためのオープンソースのフレームワークのことです。
Swagger Spec を書いておけば自動的にドキュメント生成までしてくれ、それだけではなく、ドキュメントから実際のリクエストを投げられる優れものです。
ツール | 説明 |
---|---|
Swagger Editer | Swagger Spec の設計書を記載するためのエディタ |
Swagger UI | Swagger Spec で記載された設計からドキュメントを HTML 形式で自動生成するツール |
Swagger Codegen | Swagger Spec で記載された設計から API のスタブを自動生成 |
OpenAPI でドキュメントを書くメリット
OpenAPI でドキュメントを書くメリットは、大きく分けて二つの観点があると思います
短期的な開発リードタイム短縮
FE 開発
- OpenAPI から API モックを作成して、API の実装を待つことなく実装ができる
- 例: Prism
- OpenAPI から API Client と I/F・Entity の型定義を自動生成することで、その実装の工数を省略+型安全な開発をすることができる
BE開発
- OpenAPIからController(一部)、バリデーションロジック、Request・Responseの型定義を生成できる
- 個人的にNest.js用のgeneratorが開発されていて楽しみです。
自動生成に対応している言語/フレームワーク一覧
- ada-server
- aspnetcore
- cpp-pistache-server
- cpp-qt5-qhttpengine-server
- cpp-restbed-server
- csharp-nancyfx
- erlang-server
- fsharp-functions (beta)
- fsharp-giraffe-server (beta)
- go-gin-server
- go-server
- graphql-nodejs-express-server
- haskell
- java-inflector
- java-msf4j
- java-pkmst
- java-play-framework
- java-undertow-server
- java-vertx
- java-vertx-web (beta)
- jaxrs-cxf
- jaxrs-cxf-cdi
- jaxrs-cxf-extended
- jaxrs-jersey
- jaxrs-resteasy
- jaxrs-resteasy-eap
- jaxrs-spec
- kotlin-server
- kotlin-spring
- kotlin-vertx (beta)
- nodejs-express-server (beta)
- php-laravel
- php-lumen
- php-silex
- php-slim4
- php-symfony
- php-ze-ph
- python-aiohttp
- python-blueplanet
- python-flask
- ruby-on-rails
- ruby-sinatra
- rust-server
- scala-akka-http-server (beta)
- scala-finch
- scala-lagom-server
- scala-play-server
- scalatra
- spring
中〜長期的な保守観点
- いい感じのドキュメントを生成してくれる(OpenAPIからI/Fを生成する開発手法させ続けられれば
- リクエストとレスポンス用のモデルは定義ファイルにしたがって自動生成されるため、APIの仕様変更に追従しやすいく、型安全
- ある程度は書き方に統一性を持たせられるため、レビュー時に確認すべきポイントが明確になりやすい
sql-swagger-generatorについて
今回実装してみた、SQLのCreate分から各テーブルのCRUDを生成するsql-swagger-generatorについてです。
使い方
インストール方法
go get -v -u github.com/toshi1127/sql-swagger-generator
コードの自動生成に必要なもの(2つ)
1.プロダクトのDBを作成するSQL文
CREATE TABLE products (
id INT NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
price INT NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS users(
user_id varchar(255) PRIMARY KEY,
nick_name varchar(255) NOT NULL,
profile_image_uri varchar(255),
email varchar(255) NOT NULL,
description varchar(255),
social_link varchar(255),
gender enum('male', 'female', 'other'),
identify_status varchar(255),
customer_id varchar(255),
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp on update current_timestamp,
deleted_at timestamp
);
2.sql-swagger-generatorの設定ファイル
service:
name: TestService # サービス名
host: 1.1.1.1:1234 # ホスト名
prefix: /v1 # エンドポイントのprefix
resources: # SQLの中、どのテーブルを対象に自動生成するか
users:
title: user
definition:
name: User
products:
title: product
definition:
name: Product
実行方法
設定ファイル・生成対象のSQL文のパス、アウトプットの出力先を指定します。
sql-swagger-generator conf=./example/conf.yml -sql=./example/queries.sql -outputDir=./example/swagger/swagger.yml
実行結果
CLIを実行すると、モデルの定義とAPIドキュメントが生成されます。
自動生成されたモデルの定義
models
は以下に、ymlファイルで指定したテーブルの定義が生成されます。
title: User
type: object
properties:
user_id:
type: string
nick_name:
type: string
profile_image_uri:
type: string
email:
type: string
description:
type: string
social_link:
type: string
gender:
type: string
enum:
- male
- female
- other
identify_status:
type: string
customer_id:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
deleted_at:
type: string
format: date-time
required:
- nick_name
- email
- created_at
- updated_at
title: Product
type: object
properties:
id:
type: integer
format: int64
name:
type: string
price:
type: integer
format: int64
required:
- name
- price
各モデルのAPIドキュメント
各モデルのCRED+Bulk Updateに関するドキュメントが生成されます。
show:
swagger: '2.0'
info:
title: TestService
version: 1.0.0
host: 1.1.1.1:1234
basePath: /v1
schemes:
- http
consumes:
- application/json
produces:
- application/json
paths:
/health-check:
get:
operationId: HealthCheck
description: Returns 200 if the service is healthy.
responses:
200:
description: Healthy
500:
description: Not healthy
/users:
get:
operationId: GetUsers
summary: Get users
description: Returns all user resources.
parameters:
- in: query
name: limit
type: integer
- in: query
name: offset
type: integer
responses:
200:
description: List of user resources.
schema:
type: array
items:
$ref: './models/User.yml'
500:
description: Internal server error
post:
operationId: CreateUser
summary: Create user
description: Creates a user.
parameters:
- name: resource
in: body
required: true
schema:
$ref: './models/User.yml'
responses:
201:
description: Created
schema:
$ref: './models/User.yml'
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
/users/batch:
get:
operationId: GetUsersByID
summary: Get users by ID
description: Returns the user resources with the given IDs.
parameters:
- in: query
name: ids
type: array
items:
type: integer
responses:
200:
description: List of user resources
schema:
type: array
items:
$ref: './models/User.yml'
500:
description: Internal server error
/users/{id}:
get:
operationId: GetUser
summary: Get user by ID
description: Returns the user with the given ID.
parameters:
- in: path
name: id
type: integer
required: true
responses:
200:
description: Single user
schema:
$ref: './models/User.yml'
404:
description: Not found
500:
description: Internal server error
patch:
operationId: PatchUser
summary: Patch user
description: Patches the user with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
- name: patch
in: body
required: true
schema:
$ref: '#/definitions/Patch'
responses:
200:
description: Success
schema:
$ref: './models/User.yml'
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
404:
description: Not found
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
put:
operationId: PutUser
summary: Put user
description: Replaces the user with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
- name: resource
in: body
required: true
schema:
$ref: './models/User.yml'
responses:
200:
description: Success
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
404:
description: Not found
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
delete:
operationId: DeleteUser
summary: Delete user
description: Deletes the user with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
responses:
200:
description: Success
404:
description: Not found
500:
description: Internal server error
/products:
get:
operationId: GetProducts
summary: Get products
description: Returns all product resources.
parameters:
- in: query
name: limit
type: integer
- in: query
name: offset
type: integer
responses:
200:
description: List of product resources.
schema:
type: array
items:
$ref: './models/Product.yml'
500:
description: Internal server error
post:
operationId: CreateProduct
summary: Create product
description: Creates a product.
parameters:
- name: resource
in: body
required: true
schema:
$ref: './models/Product.yml'
responses:
201:
description: Created
schema:
$ref: './models/Product.yml'
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
/products/batch:
get:
operationId: GetProductsByID
summary: Get products by ID
description: Returns the product resources with the given IDs.
parameters:
- in: query
name: ids
type: array
items:
type: integer
responses:
200:
description: List of product resources
schema:
type: array
items:
$ref: './models/Product.yml'
500:
description: Internal server error
/products/{id}:
get:
operationId: GetProduct
summary: Get product by ID
description: Returns the product with the given ID.
parameters:
- in: path
name: id
type: integer
required: true
responses:
200:
description: Single product
schema:
$ref: './models/Product.yml'
404:
description: Not found
500:
description: Internal server error
patch:
operationId: PatchProduct
summary: Patch product
description: Patches the product with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
- name: patch
in: body
required: true
schema:
$ref: '#/definitions/Patch'
responses:
200:
description: Success
schema:
$ref: './models/Product.yml'
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
404:
description: Not found
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
put:
operationId: PutProduct
summary: Put product
description: Replaces the product with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
- name: resource
in: body
required: true
schema:
$ref: './models/Product.yml'
responses:
200:
description: Success
400:
description: Bad request
schema:
$ref: '#/definitions/Error'
404:
description: Not found
422:
description: Unprocessable entity
schema:
$ref: '#/definitions/Error'
500:
description: Internal server error
delete:
operationId: DeleteProduct
summary: Delete product
description: Deletes the product with the given ID.
parameters:
- name: id
in: path
type: integer
required: true
responses:
200:
description: Success
404:
description: Not found
500:
description: Internal server error
definitions:
Patch:
type: array
description: Patch instructions
items:
type: object
required:
- op
- path
properties:
op:
type: string
description: Operation
path:
type: string
description: Path to field to operate on
value:
$ref: '#/definitions/AnyValue'
AnyValue:
description: Any type of value
Error:
type: object
properties:
code:
type: integer
format: int64
x-nullable: true
message:
type: string
Principal:
type: object
description: Security principal for validating that a user is authorized to execute certain actions
properties:
userId:
type: string
permissions:
type: array
items:
type: string
生成されたOpen APIからコードを自動生成する
sql-swagger-generatorのexampleに、簡単なデモを用意しておいたので、それを使ってみましょう。
ソースコードをクローン
git clone https://github.com/toshi1127/sql-swagger-generator
exampleディレクトリは以下で、Dockerを使ってopenapi-generatorを使用する
cd example
make openapigen
httpclient
配下にFE向けのAPIクライアントが生成され、example/internal/restapi/openapi
配下にBE向けのコードが生成されたかと思います!
実際にAPI Clientを使い始めると、いまいちなところもある(query paramsがオブジェクトで渡せないなど)のですが、モデルの型定義を使えるだけでも、型安全に開発速度を上げることができると思います。
生成された各コードは下記の通りです(これは一部分ですが
/**
*
* @export
* @interface User
*/
export interface User {
/**
*
* @type {string}
* @memberof User
*/
userId?: string;
/**
*
* @type {string}
* @memberof User
*/
nickName: string;
/**
*
* @type {string}
* @memberof User
*/
profileImageUri?: string;
/**
*
* @type {string}
* @memberof User
*/
email: string;
/**
*
* @type {string}
* @memberof User
*/
description?: string;
/**
*
* @type {string}
* @memberof User
*/
socialLink?: string;
/**
*
* @type {string}
* @memberof User
*/
gender?: UserGenderEnum;
/**
*
* @type {string}
* @memberof User
*/
identifyStatus?: string;
/**
*
* @type {string}
* @memberof User
*/
customerId?: string;
/**
*
* @type {string}
* @memberof User
*/
createdAt: string;
/**
*
* @type {string}
* @memberof User
*/
updatedAt: string;
/**
*
* @type {string}
* @memberof User
*/
deletedAt?: string;
}
/**
* @export
* @enum {string}
*/
export enum UserGenderEnum {
Male = 'male',
Female = 'female',
Other = 'other'
}
............
/**
* Creates a user.
* @summary Create user
* @param {User} resource
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createUser(resource: User, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<User>> {
const localVarAxiosArgs = await DefaultApiAxiosParamCreator(configuration).createUser(resource, options);
return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
const axiosRequestArgs = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
return axios.request(axiosRequestArgs);
};
},
// User struct for User
type User struct {
UserId *string `json:"user_id,omitempty"`
NickName string `json:"nick_name"`
ProfileImageUri *string `json:"profile_image_uri,omitempty"`
Email string `json:"email"`
Description *string `json:"description,omitempty"`
SocialLink *string `json:"social_link,omitempty"`
Gender *string `json:"gender,omitempty"`
IdentifyStatus *string `json:"identify_status,omitempty"`
CustomerId *string `json:"customer_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at,omitempty"`
}
// NewUser instantiates a new User object
// This constructor will assign default values to properties that have it defined,
// and makes sure properties required by API are set, but the set of arguments
// will change when the set of required properties is changed
func NewUser(nickName string, email string, createdAt time.Time, updatedAt time.Time, ) *User {
this := User{}
this.NickName = nickName
this.Email = email
this.CreatedAt = createdAt
this.UpdatedAt = updatedAt
return &this
}
// NewUserWithDefaults instantiates a new User object
// This constructor will only assign default values to properties that have it defined,
// but it doesn't guarantee that properties required by API are set
func NewUserWithDefaults() *User {
this := User{}
return &this
}
// GetUserId returns the UserId field value if set, zero value otherwise.
func (o *User) GetUserId() string {
if o == nil || o.UserId == nil {
var ret string
return ret
}
return *o.UserId
}
// GetUserIdOk returns a tuple with the UserId field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *User) GetUserIdOk() (*string, bool) {
if o == nil || o.UserId == nil {
return nil, false
}
return o.UserId, true
}
.......
最後に
便利に感じだら、是非使ってみてください!
多分バグもあるので、PR歓迎です。笑
PS. 久しぶりのGo言語書いたら楽しかった...