Swagger-nodeのSwagger定義ファイルを自動的にマージさせる

Swagger-nodeを使って、よくRESTfulサーバを立ち上げるのですが、エンドポイントの追加作業が煩雑になりがちです。

普段はこんな感じで、エンドポイントを追加しているのではないでしょうか。

・既存のapi/swagger/swagger.yamlにエンドポイントを追加
・各エンドポイントに以下を追加
　　x-swagger-router-controller: ソースファイル名
・各メソッドに以下を追加
　　operationId: オペレーションID
・ソースファイルに、以下のexportsを追記

```js
module.exports = {
  オペレーションID: 関数名
};
```

上記の作業において、課題がいくつかあります。

・swagger.yaml に、複数のエンドポイントの定義が混在しているためメンテナンスしづらい
・ソースファイルを置いているフォルダ(api/controllers/)に、複数のエンドポイントのソースファイルが混在し散らかる。

そこで、以下をやってくれるユーティリティを作ろうと思います。

＜準備＞
・api/contollers/フォルダ配下にフォルダを作成し、エンドポイントのソースファイルを配置する。複数のエンドポイントの実装を1つのソースファイルにまとめてもよいです。
・そのフォルダに「swagger.yaml」を作成し、それらエンドポイントを定義する。

＜操作＞
```> npm run automount```
を実行すると、api/contollers/フォルダ配下の各フォルダにswagger.yamlがある場合、その内容をapi/swagger/swagger.yamlファイルに自動マージする。
```> npm run autoclean```
を実行すると、自動マージしていた定義をapi/swagger/swagger.yamlファイルから削除する。

どうでしょうか、フォルダごとにエンドポイントの実装もSwagger定義ファイルもまとめられます。

例えばこんな感じです。まずは自動マージ前です。

```yaml:api/swagger/swagger.yaml
swagger: "2.0"
info:
  version: "0.0.1"
  title: Hello World App
host: localhost:10010
basePath: /
schemes:
  - http
  - https
consumes:
  - application/json
produces:
  - application/json
paths:
  /swagger:
    x-swagger-pipe: swagger_raw
  /hello:
    x-swagger-router-controller: hello_world
    get:
      operationId: hello
      parameters:
        - name: name
          in: query
          type: string
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/HelloWorldResponse"

definitions:
  HelloWorldResponse:
    required:
      - message
    properties:
      message:
        type: string
```

このままでも、api/controllers/hello_world.jsは呼び出せます。

api/controllers/フォルダに以下のフォルダおよびファイルを作成します。

api/controllers/folder1/
　index.js
　swagger.yaml

api/controllers/folder2/
　index.js
　swagger.yaml

各ファイルの内容です。

```yaml:api/controllers/folder1/swagger.yaml
paths:
  /test1:
    get:
      description: Returns 'Hello' to the caller
      # used as the method name of the controller
      operationId: test1_id
      parameters:
        - name: name
          in: query
          description: The name of the person to whom to say hello
          required: false
          type: string
      responses:
        "200":
          description: Success
          schema:
            # a pointer to a definition
            $ref: "#/definitions/HelloWorldResponse"
        # responses may fall through to errors
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"
```

```js:api/controllers/folder1/index.js
'use strict';

const util = require('util');

module.exports = {
  test1_id: test
};

function test(req, res) {
  // variables defined in the Swagger document can be referenced using req.swagger.params.{parameter_name}
  var name = req.swagger.params.name.value || 'stranger';
  var hello = util.format('Hello, %s!', name);

  // this sends back a JSON response which is a single string
  res.json(hello);
}
```

folder1にオペレーションID：test1_idが定義されているのがわかります。
同様に、folder2にオペレーションID：test2_idが定義しています。

この状態で、以下を実行します。

```> npm run automount```

そうすると、api/swagger/swagger.yamlが以下のように再構成されます。

```yaml:api/swagger/swagger.yaml
swagger: "2.0"
info:
  version: "0.0.1"
  title: Hello World App
host: localhost:10010
basePath: /
schemes:
  - http
  - https
consumes:
  - application/json
produces:
  - application/json
paths:
  /swagger:
    x-swagger-pipe: swagger_raw
  /hello:
    x-swagger-router-controller: hello_world
    get:
      operationId: hello
      parameters:
        - name: name
          in: query
          type: string
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/HelloWorldResponse"
  /test1: # automounted
    get:
      description: Returns 'Hello' to the caller
      # used as the method name of the controller
      operationId: test1_id
      parameters:
        - name: name
          in: query
          description: The name of the person to whom to say hello
          required: false
          type: string
      responses:
        "200":
          description: Success
          schema:
            # a pointer to a definition
            $ref: "#/definitions/HelloWorldResponse"
        # responses may fall through to errors
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"
      x-swagger-router-controller: routing
      x-automount: folder1
  /test2: # automounted
    get:
      description: Returns 'Hello' to the caller
      # used as the method name of the controller
      operationId: test2_id
      parameters:
        - name: name
          in: query
          description: The name of the person to whom to say hello
          required: false
          type: string
      responses:
        "200":
          description: Success
          schema:
            # a pointer to a definition
            $ref: "#/definitions/HelloWorldResponse"
        # responses may fall through to errors
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"
      x-swagger-router-controller: routing
      x-automount: folder2

definitions:
  HelloWorldResponse:
    required:
      - message
    properties:
      message:
        type: string
```

各フォルダfolder1とfolder2にあるswagger.yamlがマージされているがわかりますでしょうか。
その際に、以下の2つを自動的に付けています。

      x-swagger-router-controller: routing
      x-automount: folder1

これらは後でからくりを示しますが、いったんrouting.jsに振り向けた後、各フォルダfolder1またはfolder2のindex.jsに転送しています。

あと、わかりやすいように、エンドポイントのところにコメントで ```# automounted```と付けておきました。

# Swagger-nodeへの組み込み

それではさっそく上記機能をSwagger-nodeに組み込んでみます。

まずは普通に、Swaggerプロジェクトを作成します。

```> swagger project create プロジェクト名```
プロジェクト名は何でも良いです。
途中、フレームワークを聞かれますが、expressを選択します。

次に、api/controllers/routing.jsを作成しましょう。

```js:routing.js
'use strict';

const fs = require('fs');

var func_auto_table = [];

const files = fs.readdirSync("api/controllers");
for( var i = 0 ; i < files.length ; i++ ){
    var stats_dir = fs.statSync("api/controllers/" + files[i]);
    if( !stats_dir.isDirectory() )
      continue;
    try{
      fs.statSync("api/controllers/" + files[i] + "/swagger.yaml" );
    }catch(error){
      continue;
    }
    func_auto_table[files[i]] = require('./' + files[i]);
    console.log('auto mouted: ' + files[i]);
};

var exports_list = {};

for( var operationId in func_auto_table ){
  Object.keys(func_auto_table[operationId]).forEach(item =>{
    exports_list[item] = routing;
  });
}

module.exports = exports_list;

function routing(req, res) {
  var operationId = req.swagger.operation.operationId;
  return func_auto_table[req.swagger.operation["x-automount"]][operationId](req, res);
}
```

次に、ルートにautomount.jsを作成しましょう。

```js:automount.js
'use strict';

const yaml = require('yaml');
const yaml_types = require('yaml/types');
const fs = require('fs');

const SWAGGER_FILE = "api/swagger/swagger.yaml";
const TARGET_FNAME = "swagger.yaml";
const ROUTING_NAME = "routing";

const root_file = fs.readFileSync(SWAGGER_FILE, 'utf-8');
const root = yaml.parseDocument(root_file);

if( process.argv.length != 3){
  console.error('Invalid Params');
  return;
}

if( process.argv[2] == 'clean'){
  var num = 0;
  
  num += delete_paths(root);
  num += delete_definitions(root);

  if( num == 0 ){
    console.log(SWAGGER_FILE + ' no changed');
    return;
  }

  var swagger = String(root);
  fs.writeFileSync(SWAGGER_FILE, swagger, 'utf-8');
  console.log(SWAGGER_FILE + ' cleaned');
}else if( process.argv[2] == 'mount'){
  var num = 0;

  num += delete_paths(root);
  num += delete_definitions(root);

  const files = fs.readdirSync("api/controllers");
  for( var i = 0 ; i < files.length ; i++ ){
    var stats_dir = fs.statSync("api/controllers/" + files[i]);
    if( !stats_dir.isDirectory() )
      continue;
    try{
      fs.statSync("api/controllers/" + files[i] + '/' + TARGET_FNAME );
    }catch(error){
      continue;
    }

    const file = fs.readFileSync("api/controllers/" + files[i] + '/' + TARGET_FNAME, 'utf-8');
    const doc = yaml.parseDocument(file);
    num += append_paths(root, doc, files[i]);
    num += append_definitions(root, doc, files[i]);
  };

  if( num == 0 ){
    console.log(SWAGGER_FILE + ' no changed');
    return;
  }

  var swagger = String(root);
  fs.writeFileSync(SWAGGER_FILE, swagger, 'utf-8');
  console.log(SWAGGER_FILE + ' mounted');
}else{
  console.error('Invalid Params');
  return;
}

return;


function append_paths(root, target, name){
  var map = target.get('paths');
  if( !map )
    return 0;

  var num = 0;
  for( var i = 0 ; i < map.items.length ; i++ ){
    if( map.items[i].value.items[0].value instanceof yaml_types.Scalar )
      continue;

    var children = map.items[i].value.items;
    for( var j = 0 ; j < children.length ; j++ ){
      children[j].value.set("x-swagger-router-controller", ROUTING_NAME);
      children[j].value.set("x-automount", name);
    }

    map.items[i].comment = " automounted";
    root.addIn(['paths'], map.items[i]);
    console.log('mounted(paths): ' + map.items[i].key.value);
    num++;
  }

  return num;
}

function append_definitions(root, target, name){
  var map = target.get('definitions');
  if( !map )
    return 0;

  for( var i = 0 ; i < map.items.length ; i++ ){
    map.items[i].value.set("x-automount", name);

    map.items[i].comment = " automounted";
    root.addIn(['definitions'], map.items[i]);
    console.log('mounted(definition): ' + map.items[i].key.value);
  }

  return map.items.length;
}

function delete_paths(root){
  var map = root.get('paths');
  if( !map )
    return 0;

  var delete_target = [];
  for( var i = 0 ; i < map.items.length ; i++ ){
    if( map.items[i].value.items[0].value instanceof yaml_types.Scalar )
      continue;

    var children = map.items[i].value.items;
    for( var j = 0 ; j < children.length ; j++ ){
      if( children[j].value.get("x-automount") ){
        delete_target.push(map.items[i].key.value);
        break;
      }
    }
  }

  for( var i = 0 ; i < delete_target.length ; i++ )
    map.delete(delete_target[i]);
  
  return delete_target.length;
}

function delete_definitions(root){
  var map = root.get('definitions');
  if( !map )
    return 0;

  var delete_target = [];
  for( var i = 0 ; i < map.items.length ; i++ ){
    if( map.items[i].value.get("x-automount") )
      delete_target.push(map.items[i].key.value);
  }

  for( var i = 0 ; i < delete_target.length ; i++ )
    map.delete(delete_target[i]);

  return delete_target.length;
}
```

npmモジュール yamlを使ってSwaggerファイルの参照および編集を行っています。

yamlパッケージを追加しましょう。

```> npm install yaml```

package.jsonのscriptsに、以下を追加しましょう。

    "automount": "node automount.js mount",
    "autoclean": "node automount.js clean",

これで準備完了です。
とりあえず、以下で起動できます。

```> npm run start```
または
```> node app.js```

コンソールに表示されている通り、
```http://127.0.0.1:10010/hello?name=Scott```
にアクセスするとレスポンスが返ってきます。

それでは、次に先ほど示したfolder1フォルダとその配下のファイルindex.jsとswagger.yamlをapi/controllers/フォルダに配置します。
そして以下を実行します。

```> npm run automount```
```> npm run start```

以下のURLにアクセスして、ちゃんと応答が返ってきたら成功です。
```http://127.0.0.1:10010/test1?name=Scott```

# 完成品

以下のGitHubに置いておきました。

poruruba/swagger_automount
　https://github.com/poruruba/swagger_automount

こちらもご参考にどうぞ
　[SwaggerでRESTful環境を構築する](https://qiita.com/poruruba/items/72d8c85f8923bc78b918)

以上