Swagger-nodeを使って、よくRESTfulサーバを立ち上げるのですが、エンドポイントの追加作業が煩雑になりがちです。
普段はこんな感じで、エンドポイントを追加しているのではないでしょうか。
・既存のapi/swagger/swagger.yamlにエンドポイントを追加
・各エンドポイントに以下を追加
x-swagger-router-controller: ソースファイル名
・各メソッドに以下を追加
operationId: オペレーションID
・ソースファイルに、以下のexportsを追記
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定義ファイルもまとめられます。
例えばこんな感じです。まずは自動マージ前です。
swagger:"2.0"info:version:"0.0.1"title:Hello World Apphost:localhost:10010basePath:/schemes:-http-httpsconsumes:-application/jsonproduces:-application/jsonpaths:/swagger:x-swagger-pipe:swagger_raw/hello:x-swagger-router-controller:hello_worldget:operationId:helloparameters:-name:namein:querytype:stringresponses:"200":description:Successschema:$ref:"#/definitions/HelloWorldResponse"definitions:HelloWorldResponse:required:-messageproperties:message:type:string
このままでも、api/controllers/hello_world.jsを呼び出せます。
api/controllers/フォルダに以下のフォルダおよびファイルを作成します。
api/controllers/folder1/
index.js
swagger.yaml
api/controllers/folder2/index.js
index.js
swagger.yaml
各ファイルの内容です。
paths:/test1:get:description:Returns 'Hello' to the caller# used as the method name of the controlleroperationId:test1_idparameters:-name:namein:querydescription:The name of the person to whom to say hellorequired:falsetype:stringresponses:"200":description:Successschema:# a pointer to a definition$ref:"#/definitions/HelloWorldResponse"# responses may fall through to errorsdefault:description:Errorschema:$ref:"#/definitions/ErrorResponse"
'use strict';constutil=require('util');module.exports={test1_id:test};functiontest(req,res){// variables defined in the Swagger document can be referenced using req.swagger.params.{parameter_name}varname=req.swagger.params.name.value||'stranger';varhello=util.format('Hello, %s!',name);// this sends back a JSON response which is a single stringres.json(hello);}
folder1にオペレーションID:test1_idが定義されているのがわかります。
同様に、folder2にオペレーションID:test2_idが定義しています。
この状態で、以下を実行します。
> npm run automount
そうすると、api/swagger/swagger.yamlが以下のように再構成されます。
swagger:"2.0"info:version:"0.0.1"title:Hello World Apphost:localhost:10010basePath:/schemes:-http-httpsconsumes:-application/jsonproduces:-application/jsonpaths:/swagger:x-swagger-pipe:swagger_raw/hello:x-swagger-router-controller:hello_worldget:operationId:helloparameters:-name:namein:querytype:stringresponses:"200":description:Successschema:$ref:"#/definitions/HelloWorldResponse"/test1:# automountedget:description:Returns 'Hello' to the caller# used as the method name of the controlleroperationId:test1_idparameters:-name:namein:querydescription:The name of the person to whom to say hellorequired:falsetype:stringresponses:"200":description:Successschema:# a pointer to a definition$ref:"#/definitions/HelloWorldResponse"# responses may fall through to errorsdefault:description:Errorschema:$ref:"#/definitions/ErrorResponse"x-swagger-router-controller:routingx-automount:folder1/test2:# automountedget:description:Returns 'Hello' to the caller# used as the method name of the controlleroperationId:test2_idparameters:-name:namein:querydescription:The name of the person to whom to say hellorequired:falsetype:stringresponses:"200":description:Successschema:# a pointer to a definition$ref:"#/definitions/HelloWorldResponse"# responses may fall through to errorsdefault:description:Errorschema:$ref:"#/definitions/ErrorResponse"x-swagger-router-controller:routingx-automount:folder2definitions:HelloWorldResponse:required:-messageproperties: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を作成しましょう。
'use strict';constfs=require('fs');varfunc_auto_table=[];constfiles=fs.readdirSync("api/controllers");for(vari=0;i<files.length;i++){varstats_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]);};varexports_list={};for(varoperationIdinfunc_auto_table){Object.keys(func_auto_table[operationId]).forEach(item=>{exports_list[item]=routing;});}module.exports=exports_list;functionrouting(req,res){varoperationId=req.swagger.operation.operationId;returnfunc_auto_table[req.swagger.operation["x-automount"]][operationId](req,res);}
次に、ルートにautomount.jsを作成しましょう。
'use strict';constyaml=require('yaml');constyaml_types=require('yaml/types');constfs=require('fs');constSWAGGER_FILE="api/swagger/swagger.yaml";constTARGET_FNAME="swagger.yaml";constROUTING_NAME="routing";constroot_file=fs.readFileSync(SWAGGER_FILE,'utf-8');constroot=yaml.parseDocument(root_file);if(process.argv.length!=3){console.error('Invalid Params');return;}if(process.argv[2]=='clean'){varnum=0;num+=delete_paths(root);num+=delete_definitions(root);if(num==0){console.log(SWAGGER_FILE+' no changed');return;}varswagger=String(root);fs.writeFileSync(SWAGGER_FILE,swagger,'utf-8');console.log(SWAGGER_FILE+' cleaned');}elseif(process.argv[2]=='mount'){varnum=0;num+=delete_paths(root);num+=delete_definitions(root);constfiles=fs.readdirSync("api/controllers");for(vari=0;i<files.length;i++){varstats_dir=fs.statSync("api/controllers/"+files[i]);if(!stats_dir.isDirectory())continue;try{fs.statSync("api/controllers/"+files[i]+'/'+TARGET_FNAME);}catch(error){continue;}constfile=fs.readFileSync("api/controllers/"+files[i]+'/'+TARGET_FNAME,'utf-8');constdoc=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;}varswagger=String(root);fs.writeFileSync(SWAGGER_FILE,swagger,'utf-8');console.log(SWAGGER_FILE+' mounted');}else{console.error('Invalid Params');return;}return;functionappend_paths(root,target,name){varmap=target.get('paths');if(!map)return0;varnum=0;for(vari=0;i<map.items.length;i++){if(map.items[i].value.items[0].valueinstanceofyaml_types.Scalar)continue;varchildren=map.items[i].value.items;for(varj=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++;}returnnum;}functionappend_definitions(root,target,name){varmap=target.get('definitions');if(!map)return0;for(vari=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);}returnmap.items.length;}functiondelete_paths(root){varmap=root.get('paths');if(!map)return0;vardelete_target=[];for(vari=0;i<map.items.length;i++){if(map.items[i].value.items[0].valueinstanceofyaml_types.Scalar)continue;varchildren=map.items[i].value.items;for(varj=0;j<children.length;j++){if(children[j].value.get("x-automount")){delete_target.push(map.items[i].key.value);break;}}}for(vari=0;i<delete_target.length;i++)map.delete(delete_target[i]);returndelete_target.length;}functiondelete_definitions(root){varmap=root.get('definitions');if(!map)return0;vardelete_target=[];for(vari=0;i<map.items.length;i++){if(map.items[i].value.get("x-automount"))delete_target.push(map.items[i].key.value);}for(vari=0;i<delete_target.length;i++)map.delete(delete_target[i]);returndelete_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環境を構築する
以上