前回は簡単な 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<User></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<User></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 のテンプレートはカスタマイズが必要だな。