Swagger definition からクライアントとサーバーのソースコードを生成してみた

前回は簡単な Web API の Swagger definition を書いただけで終わってしまったけど、 ここからが本題。

やりたいのは、Swagger definition からクライアントとサーバーのソースコード生成すること。 そのためのツールとして swagger-codegen が提供されている。

http://swagger.io/swagger-codegen/swagger.io

お試しなので、Homebrew や mvn を使わずに直接 jar をダウンロードしてしまおう。

http://repo1.maven.org/maven2/io/swagger/swagger-codegen-cli/2.1.5/swagger-codegen-cli-2.1.5.jar

前回の Swagger definition を sample_api.yml として保存し、 まずは API クライアントを生成してみる。

java -jar swagger-codegen-cli-2.1.5.jar generate \
  -i sample_api.yml \
  -l csharp \
  -o SampleApiClient

を実行すると、RestSharp を使った API クライアントのソースコードが生成された。 一部抜粋すると、こんな感じ。

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using RestSharp;
using IO.Swagger.Client;
using IO.Swagger.Model;

namespace IO.Swagger.Api
{
    // ...(IDefaultApi の定義は省略)...

    /// <summary>
    /// Represents a collection of functions to interact with the API endpoints
    /// </summary>
    public class DefaultApi : IDefaultApi
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultApi"/> class.
        /// </summary>
        /// <returns></returns>
        public DefaultApi(String basePath)
        {
            this.Configuration = new Configuration(new ApiClient(basePath));
        }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultApi"/> class
        /// using Configuration object
        /// </summary>
        /// <param name="configuration">An instance of Configuration</param>
        /// <returns></returns>
        public DefaultApi(Configuration configuration = null)
        {
            if (configuration == null) // use the default one in Configuration
                this.Configuration = Configuration.Default; 
            else
                this.Configuration = configuration;
        }

        /// <summary>
        /// Gets the base path of the API client.
        /// </summary>
        /// <value>The base path</value>
        public String GetBasePath()
        {
            return this.Configuration.ApiClient.RestClient.BaseUrl.ToString();
        }

        /// <summary>
        /// Sets the base path of the API client.
        /// </summary>
        /// <value>The base path</value>
        [Obsolete("SetBasePath is deprecated, please do 'Configuraiton.ApiClient = new ApiClient(\"http://new-path\")' instead.")]
        public void SetBasePath(String basePath)
        {
            // do nothing
        }
    
        /// <summary>
        /// Gets or sets the configuration object
        /// </summary>
        /// <value>An instance of the Configuration</value>
        public Configuration Configuration {get; set;}

        /// <summary>
        /// Gets the default header.
        /// </summary>
        /// <returns>Dictionary of HTTP header</returns>
        [Obsolete("DefaultHeader is deprecated, please use Configuration.DefaultHeader instead.")]
        public Dictionary<String, String> DefaultHeader()
        {
            return this.Configuration.DefaultHeader;
        }

        /// <summary>
        /// Add default header.
        /// </summary>
        /// <param name="key">Header field name.</param>
        /// <param name="value">Header field value.</param>
        /// <returns></returns>
        [Obsolete("AddDefaultHeader is deprecated, please use Configuration.AddDefaultHeader instead.")]
        public void AddDefaultHeader(string key, string value)
        {
            this.Configuration.AddDefaultHeader(key, value);
        }
   
        
        /// <summary>
        /// ユーザー一覧取得 ユーザー一覧を取得します。
        /// </summary>
        /// <param name="page">ページ番号</param> 
        /// <returns>List&lt;User&gt;</returns>
        public List<User> UsersGet (int? page = null)
        {
             ApiResponse<List<User>> response = UsersGetWithHttpInfo(page);
             return response.Data;
        }

        /// <summary>
        /// ユーザー一覧取得 ユーザー一覧を取得します。
        /// </summary>
        /// <param name="page">ページ番号</param> 
        /// <returns>ApiResponse of List&lt;User&gt;</returns>
        public ApiResponse< List<User> > UsersGetWithHttpInfo (int? page = null)
        {
            
    
            var path_ = "/users";
    
            var pathParams = new Dictionary<String, String>();
            var queryParams = new Dictionary<String, String>();
            var headerParams = new Dictionary<String, String>(Configuration.DefaultHeader);
            var formParams = new Dictionary<String, String>();
            var fileParams = new Dictionary<String, FileParameter>();
            String postBody = null;

            // to determine the Accept header
            String[] http_header_accepts = new String[] {
                "application/json"
            };
            String http_header_accept = Configuration.ApiClient.SelectHeaderAccept(http_header_accepts);
            if (http_header_accept != null)
                headerParams.Add("Accept", Configuration.ApiClient.SelectHeaderAccept(http_header_accepts));

            // set "format" to json by default
            // e.g. /pet/{petId}.{format} becomes /pet/{petId}.json
            pathParams.Add("format", "json");
            
            if (page != null) queryParams.Add("page", Configuration.ApiClient.ParameterToString(page)); // query parameter
            
            
            
            

            
    
            // make the HTTP request
            IRestResponse response = (IRestResponse) Configuration.ApiClient.CallApi(path_, Method.GET, queryParams, postBody, headerParams, formParams, fileParams, pathParams);

            int statusCode = (int) response.StatusCode;
    
            if (statusCode >= 400)
                throw new ApiException (statusCode, "Error calling UsersGet: " + response.Content, response.Content);
            else if (statusCode == 0)
                throw new ApiException (statusCode, "Error calling UsersGet: " + response.ErrorMessage, response.ErrorMessage);
    
            return new ApiResponse<List<User>>(statusCode,
                response.Headers.ToDictionary(x => x.Name, x => x.Value.ToString()),
                (List<User>) Configuration.ApiClient.Deserialize(response, typeof(List<User>)));
            
        }


        // ...(以下 Web API を呼び出すメソッドが続く)...
    }
}

次はサーバー側。 README によると、ASP.NET Core のプロジェクトを生成できるようだ(名前は ASP.NET 5 のままになっているけど)。

java -jar swagger-codegen-cli-2.1.5.jar generate \
  -i sample_api.yml \
  -l aspnet5 \
  -o SampleApiServer

を実行したら、aspnet5 には対応してないっていうエラーになった。 …ASP.NET Core 対応が入ったの 1 ヶ月前だけど、2.1.5 がリリースされたのは 1 月 7 日だった。 含まれてなかったのか。

mvn でビルドするしかないかと思っていたら、 Swagger Editor でも ASP.NET Core のソースコードを生成できるではないですか。

メニューをクリックするとソースコードのアーカイブをダウンロードできた。 一部抜粋すると、こんな感じ。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Newtonsoft.Json;
using Swashbuckle.SwaggerGen.Annotations;
using IO.Swagger.Models;

namespace IO.Swagger.Controllers
{ 
    /// <summary>
    /// 
    /// </summary>
    public class DefaultApiController : Controller
    { 

        /// <summary>
        /// ユーザー一覧取得
        /// </summary>
        /// <remarks>ユーザー一覧を取得します。</remarks>
        /// <param name="page">ページ番号</param>
        /// <response code="200">OK</response>
        [HttpGet]
        [Route("/users")]
        [SwaggerOperation("UsersGet")]
        [SwaggerResponse(200, type: typeof(List<User>))]
        public IActionResult UsersGet([FromQuery]int? page)
        { 
            string exampleJson = null;
            
            var example = exampleJson != null
            ? JsonConvert.DeserializeObject<List<User>>(exampleJson)
            : default(List<User>);
            
            return new ObjectResult(example);
        }


        /// <summary>
        /// ユーザー登録
        /// </summary>
        /// <remarks>ユーザーを 1 件登録します。</remarks>
        /// <param name="name">ユーザー名</param>
        /// <response code="200">OK</response>
        [HttpPost]
        [Route("/users")]
        [SwaggerOperation("UsersPost")]
        [SwaggerResponse(200, type: typeof(User))]
        public IActionResult UsersPost([FromForm]string name)
        { 
            string exampleJson = null;
            
            var example = exampleJson != null
            ? JsonConvert.DeserializeObject<User>(exampleJson)
            : default(User);
            
            return new ObjectResult(example);
        }


        /// <summary>
        /// ユーザー取得
        /// </summary>
        /// <remarks>ユーザーを 1 件取得します。</remarks>
        /// <param name="userId">ユーザー ID</param>
        /// <response code="200">OK</response>
        [HttpGet]
        [Route("/users/{userId}")]
        [SwaggerOperation("UsersUserIdGet")]
        [SwaggerResponse(200, type: typeof(User))]
        public IActionResult UsersUserIdGet([FromRoute]string userId)
        { 
            string exampleJson = null;
            
            var example = exampleJson != null
            ? JsonConvert.DeserializeObject<User>(exampleJson)
            : default(User);
            
            return new ObjectResult(example);
        }


        /// <summary>
        /// ユーザー更新
        /// </summary>
        /// <remarks>ユーザーを 1 件更新します。</remarks>
        /// <param name="userId">ユーザー ID</param>
        /// <param name="name">ユーザー名</param>
        /// <response code="200">OK</response>
        [HttpPut]
        [Route("/users/{userId}")]
        [SwaggerOperation("UsersUserIdPut")]
        public void UsersUserIdPut([FromRoute]string userId, [FromForm]string name)
        { 
            throw new NotImplementedException();
        }


        /// <summary>
        /// ユーザー削除
        /// </summary>
        /// <remarks>ユーザーを 1 件削除します。</remarks>
        /// <param name="userId">ユーザー ID</param>
        /// <response code="200">OK</response>
        [HttpDelete]
        [Route("/users/{userId}")]
        [SwaggerOperation("UsersUserIdDelete")]
        public void UsersUserIdDelete([FromRoute]string userId)
        { 
            throw new NotImplementedException();
        }
    }
}

サーバー側は Swashbuckle に依存していた。 アクションの定義だけはあるけど、中身は実装してないに等しい。 せめて partial メソッドとか使っていればいいのに。

デモ版 Swagger Editor で生成できるソースコードに不満が無い場合は、 わざわざ swagger-codegen をインストールしなくてもよさそうだ。

ソースコードを生成するテンプレートをいじりたいときや、生成ロジックをカスタマイズしたいときに、 swagger-codegen を使うことになりそう。 とりあえず ASP.NET Core のテンプレートはカスタマイズが必要だな。