読者です 読者をやめる 読者になる 読者になる

Azure FuncsionsでChatworkへの書き込みをSlackに通知させてみた

azure JavaScript

今開発中の案件で、お客さんの使っているチャットツールがChatworkだった。前からお付き合いのある担当者さんは弊社のSlackに招待してそこでやり取りしているのですが、他のメンバーさんにSlackを強制するのが若干無理目な状況でした。Chatworkは常用していないので、どうしても書き込みを見落としてしまいがち…。ということで、前々から興味があったAzure FunctionsでSlackに通知するようにしてみました。

Azure FunctionsはMicrosoftのサーバーレスなコンピューティングサービスです。FaaSってやつですね。

関数を作成

今回は、タイマーで5分おきに起動して、Chatworkの指定したチャットに新着があればSlcakに通知することにするので、Function App作成後の初期画面で、タイマー、Javascriptを選んでこの関数を作成するを選んで関数を作成します。

f:id:kaz_29:20161207154832p:plain

この状態で、5分おきに自動的に関数が実行されるように設定されています。

環境変数の設定

今回の機能では、ChatworkのAPIトークンなどを扱います。これらをコードに埋め込みたくないので、以下の項目を環境変数として設定します。

変数名 説明
CW_CHATID 監視対象のChatID
CW_TOKEN ChatworkのAPIトークン
SLACK_WEBHOOK_URI SlackのIncomingWebHookのURI
SLACK_CHANNEL 投稿先のチャンネル名(#含む)

設定は、画面左下のFunction App の設定 -> アプリケーション設定の構成で表示されるアプリ設定で行います。以下のような感じ。

f:id:kaz_29:20161207154902p:plain

使用するパッケージのインストール

今回は以下の2つのnpmパッケージを使用するので、以下の手順でインストールします。

Function App の設定 -> Kudu に移動 を選択してKuduのコンソールで以下のように入力します。

D:\home>cd site/wwwroot/<作成した関数名>
D:\homesite/wwwroot/TimerTriggerJS1> npm install request slack-node 

package.jsonを用意してインストールする方法もあるようなのですが、今回は簡易的にこんな感じでやってしまいました。

コードを作成

これで前準備が整いましたので、関数を作っていきます。以下のコードを作成した関数の開発を選択しコードエディタに貼り付けて保存すればOKです。


Azure functions Chatwork to Slack


問題なく設定ができていれば、タイマーで実行されるのを待つか実行ボタンを押せば以下のような形でSlackに通知されます。

f:id:kaz_29:20161207154951p:plain

エラー処理とか超適当ですが、これでChatworkへの書き込みも見落とさないでしょう。

今回、Azure Functionを初めて使いましたが、なかなか面白いですねー。何かいいケースがあったら使いたいなと思います。

SQL Server on Linuxを入れてみた

ansible azure linux ubuntu SQL Server

Linuxに対応したSQL Serverの時期バージョンがパブリックプレビューになったので早速入れてみた。

SQL Server v.Next—SQL Server on Linux | Microsoft

インストール環境

今回は手元のmacOS Sierra + Vagrant 1.8.7 環境に構築してみました。今後何かで使うかもしれないので、Ansibleのplaybookを作ったので以下を見てもらえれば。。。

github.com

使い方

GitHub - kaz29/mssql-vagrant にも書きましたがこんな感じで。ansible-localを使っているのでVagrantが動く環境なら問題なく起動できると思います。

$ git clone https://github.com/kaz29/mssql-vagrant.git
$ cd mssql-vagrant
$ vagrant up
...
$ vagrant ssh


$ sudo /opt/mssql/bin/sqlservr-setup
Microsoft(R) SQL Server(R) Setup

You can abort setup at anytime by pressing Ctrl-C. Start this program
with the --help option for information about running it in unattended
mode.

The license terms for this product can be downloaded from
http://go.microsoft.com/fwlink/?LinkId=746388 and found
in /usr/share/doc/mssql-server/LICENSE.TXT.

Do you accept the license terms? If so, please type "YES": YES

Please enter a password for the system administrator (SA) account:
Please confirm the password for the system administrator (SA) account:

Setting system administrator (SA) account password...

Do you wish to start the SQL Server service now? [y/n]: y
Do you wish to enable SQL Server to start on boot? [y/n]: y
Created symlink from /etc/systemd/system/multi-user.target.wants/mssql-server.service to /lib/systemd/system/mssql-server.service.
Created symlink from /etc/systemd/system/multi-user.target.wants/mssql-server-telemetry.service to /lib/systemd/system/mssql-server-telemetry.service.

Setup completed successfully.

$ sqlcmd -S localhost -U SA -P '<YourPassword>'
1> SELECT Name from sys.Databases;
2> GO
Name
--------------------------------------------------------------------------------------------------------------------------------
master
tempdb
model
msdb

(4 rows affected)
1> quit

一点注意点としては、SQL Serverはメモリが3.25G以上必要ということで、Vagrantに4Gのメモリを割り当てる様に設定されていますので、MBAとかMacBookだと厳しいかもしれません。

とりあえず、サクッと動いたので今後色々試してみたいと思います。

AlamofireのRequest Paramater エンコードをカスタマイズする

iPhone Swift

最近、PHP(CakePHP2,CakePHP3,Lumen)、iPhoneアプリ(Objective-C,Swift)とAndroidアプリの運用をしていて頭ん中がカオスになっているわたなべです。

とある趣味アプリで、Alamofireを使って某APIを使おうとしてハマったのでメモっときます。

AlamofireのRequest Paramater エンコード

基本的には、RFC3986に従ってエンコードされるのですが、'/' と '?' がエンコードされません。実際のエンコード処理にはこんなコメントがあります。

// does not include "?" or "/" due to RFC 3986 - Section 3.4

RFC 3986 - Section 3.4ってなんだよ?ってことで、RFC3986 日本語訳を見るとこんな風に書いてあります。

スラッシュ ("/") と疑問符 ("?") の文字は、query 要素の中のデータを表すかもしれない。 いくつかの古い、エラーのある実装では、それが相対的参照 (Section 5.1) の基底 URI として使用される場合、階層的な区切りを探す時に query データと path データの区別に失敗する事が多いので、そのようなデータを正しく扱わないかもしれない事に注意せよ。 しかし、query 構成要素はしばしば "key=value" の対の形式で識別するための情報を運ぶために使用され、そこで頻繁に使用された値は別の URI の参照なので、時にはそれらの文字をパーセントエンコーディングする事を避けるほうがユーザビリティのためにはよい。

https://triple-underscore.github.io/RFC3986-ja.html#section-3.4

時にはそれらの文字をパーセントエンコーディングする事を避けるほうがユーザビリティのためにはよい。」これを根拠に'/' と '?'がエンコードされないようです。。。

普通に使用している分には気づかなかったのですが、APIを使うためにSignetureを生成する場合に、ハッシュ化する元の文字列が変わってしまうため問題が起こります。

ParameterEncodingを独自実装

AlamofireのParameterEncodingには、一般的に使用するURLエンコード以外にも以下のエンコードが定義されています。

  • URL
  • URLEncodedInURL
  • JSON
  • PropertyList
  • Custom

今回はCustomを使用して、'/' と '?'もエンコードするようにカスタマイズします。

Alamofireのencode処理を参考に作ったのがこんなの。

gist.github.com

この関数で作成したエンコーディング処理を以下のように指定することで独自にエンコードすることができます。

gist.github.com

わかってしまえば、たいしたことではないのですが、なかなか原因がわからずちょっと手こずりました(;_;

Ansible2.1がARMに対応したので"少しだけ"試してみた #2

ansible azure ops

先日、Ansible2.1でARMを試した記事を書きましたが、その後もう少し深くいじってみました。

kaz29.hatenablog.com

前回はリソースグループを作っただけでしたが、今回は、牛尾さん(id:simplearchitect)の以下の記事でTerraformを使ってやっていることをなぞってみました。

qiita.com

設定内容

設定した内容はこんな感じ。yamlでかけるのは読みやすくていいですね。

---

azure_resource_group:
  - name: Testing
    region: japanwest
    state: present
    tags:
      testing: testing
      delete: never

azure_virtual_network:
  - name: test
    state: present
    resource_group: Testing
    address_prefixes_cidr:
        - "10.1.0.0/16"
    dns_servers: []
    tags:
        testing: testing
        delete: on-exit

azure_subnet:
  - name: acctsub
    state: present
    virtual_network_name: test
    resource_group: Testing
    address_prefix_cidr: "10.1.2.0/24"

azure_publicipaddress:
  - name: ansibletestip
    state: present
    resource_group: Testing
    allocation_method: Static
    domain_name: ansibletestlinux
    tags:
        testing: testing
        environment: Production

azure_networkinterface:
  - name: testnic1
    state: present
    resource_group: Testing
    virtual_network_name: test
    subnet_name: acctsub
    security_group_name: 
    public_ip_address_name: ansibletestip
    private_ip_allocation_method: Static
    private_ip_address: 10.1.2.10
    tags:
        testing: testing

azure_storage_account:
  - name: accsa1971eey
    state: present
    resource_group: Testing
    type: Standard_LRS
    tags:
        testing: testing

azure_storage_container:
  - name: vhds
    state: present
    resource_group: Testing
    storage_account_name: accsa1971eey
    tags:
      testing: testing

azure_virtualmachine:
  - name: AnsibleVM02
    state: present
    resource_group: Testing
    vm_size: Standard_A0
    storage_account: accsa1971eey
    storage_container_name: vhds
    network_interfaces: testnic1
    admin_username: kaz
    remove_on_absent:
      - virtual_storage
    image:
      offer: UbuntuServer
      publisher: Canonical
      sku: '14.04.2-LTS'
      version: latest
    tags:
      testing: testing

Ansibleを使用して実行してる内容は以下。

  1. ストレージクループの作成
  2. 仮想ネットワークの作成
  3. サブネットの作成
  4. パブリックIPアドレスの作成
  5. ネットワークI/Fを作成
  6. ストレージアカウントの作成
  7. ストレージコンテナの作成
  8. VMの作成

今回作ったものはGithubにあげましたので参考になれば。

github.com

ポータルを使わないで、いつものAnsibleで構築して何回でもやり直せるのはかなり便利ですね。

まとめ

まだ理解しきてれていないので、VMを削除しようとしたらエラーが出てうまく削除できませんでした。VM削除時に自動で消されるものがあるようなので、その辺りが原因(`remove_on_absent`)かなと思うので、もう少し調べようと思います。

とはいえ、ポータルをポチポチしないで良いのはとても便利なので今後 Terraformとどっちを使うかも含めて色々検討しようと思います。

Ansible2.1がARMに対応したので"少しだけ"試してみた

ansible azure ops

Ansible2.1が発表された記事がFacebookに流れてきたので、何気に眺めていたらARM(Azure Resource Manager)のサポートが追加されたらしいので少し試してみた。

Broader support for Microsoft Azure, expanding Ansible’s support for hybrid cloud deployments, including the ability to take advantage of Azure’s Resource Manager functionality.

www.redhat.com

お試しの前に...

実務でAnsibleを使っている場合(特に1.x系)、影響が出てしまうと困るので以下などを参考にpyenv/pyenv-virtualenvを入れて環境を切り替えられるようにしてから試すほうがいいでしょう。

qiita.com

Azure Python SDKのインストール

pyenv環境にazure SDKを入れて試したところ以下のようなエラーが出て、SDKを認識できませんでした。

fatal: [ansible.decr.jp]: FAILED! => {"changed": false, "failed": true, "msg": "The Azure Python SDK is not installed (try 'pip install azure') - No module named enum"}

試しに global の方に入れてみたところ認識されました。*1

$ sudo pip install azure==2.0.0rc2

ちなみに、最新のSDKは2.0.0rc4だったのでこちらで試したところ以下のようなエラーで動作しませんでした。

fatal: [ansible.decr.jp]: FAILED! => {"changed": false, "failed": true, "msg": "Expecting azure.mgmt.compute.__version__ to be >= 2016-03-30. Found version Do you have Azure >= 2.0.0rc2 installed?"}

この辺りはまだ出たばかりなので、今後の改善を期待したいですね。

Ansibleのインストール

Ansible本体をインストールします

$ pip install ansible

サービスプリンシパル認証のための設定

以下の2のあたりを参考にclient_id/secret/tenant_idなどを取得する

qiita.com

Ansibleの設定を書く

  • hosts/hosts

Asure関連の処理はlocalで動作するのでhostsはこんな感じで適当に...

[ansibletest]
ansibletest.example.com
  • roles/azure/tasks/main.yml

<サブスクリプションID>などの設定は各自の環境に合わせて修正してください。*2

- name: Create a resource group
  azure_rm_resourcegroup:
    name: Testing
    location: japanwest
    state: present
    subscription_id: <サブスクリプションID>
    client_id: <クライアントID>
    secret: <secret>
    tenant: <テナントID>
    tags:
      testing: testing
      delete: never
  • azuretest.yml
---
- name: ansible test
  hosts: ansibletest
  connection: local

  roles:
    - azure

playbookを実行する

$ ansible-playbook -i hosts/hosts default.yml

PLAY [ansible test] ************************************************************

TASK [setup] *******************************************************************
ok: [ansibletest.example.com]

TASK [azure : include] *********************************************************
included: /Users/kaz/dev/ansible21/roles/azure/tasks/resource-group.yml for ansibletest.example.com

TASK [azure : Create a resource group] *****************************************
ok: [ansibletest.example.com]

PLAY RECAP *********************************************************************
ansibletest.example.com : ok=3 changed=0 unreachable=0 failed=0


問題なく実行されると、portal上でもリソースグループが追加されました。


f:id:kaz_29:20160531065723j:plain

まとめ

今回はリソースグループを作ってみただけですが、業務に適用できるか引き続き色々試してみようと思います。
TerraformもARM対応したりと色々面白くなってきましたね(^^。

*1:python力足らないんで原因わからないですが、引き続き調査。

*2:ドキュメントには、subscription_idなどの情報を環境変数や、~/.azure/credentialsに設定できると書いてあるのですがうまくいかなかったのでこの辺りも引き続き調査。

swaggerでAPIドキュメントを書いたらめっちゃはかどった話

CakePHP CI swagger

Swaggerは、REST APIの仕様とそれに関連するツール群の総称です。REST APIの仕様を定義したJSONファイル(Swagger Spec)を軸に以下のようなツールから構成されています。

  • Swagger UI - Swagger Spec から動的にAPIドキュメントを生成するツール
  • Swagger Editor - Swagger Specのエディタ
  • Swagger Codegen - Swagger Specからクライアントのコードを生成するツール

最近では、Open API InitiativeがAPIの記述のためにSwaggerを採用して話題になりました。

www.publickey1.jp

APIドキュメントのメンテは結構面倒

一般的にAPIの仕様書は、古くはExcel/Wordなどを使ったり、最近ではWikiMarkdown形式で記述したりなどプロジェクトによって色々な方法で記述するのが一般的かなと思います。

このには幾つか問題があって、APIの実装ができてもドキュメントがない状態があったり、せっかくドキュメントを作ってもAPIの修正・追加に追いていない状態になったりすることがよくあります。

そこで今回は、実際に開発が進んでいたCakePHP3で作成したiOSアプリのバックエンドのAPIドキュメントをSwaggerで記述したので、そこで得た知見をもとに実際の記述方法などを解説します。

SwaggerでAPIの仕様を書く

CakePHPのブログチュートリアル(http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html)で使用するスキーマを若干変更した、以下の様なデータをAPIで扱う場合のドキュメントを書いてみます。

  • User
    • id
    • username
    • password
    • created
    • modified
  • Article
    • id
    • user_id
    • title
    • body
    • created
    • modified
アプリケーションの定義

まずは、今回使用するアプリケーション自体の定義を記述します。以下の様に、@SWG\Info内にアプリケーションの名前、説明、APIバージョンなどの情報を記載します。

後ほど一覧取得APIで共通で使用するPageInfoデータに関してもここに記載します。詳細は後述します。

このファイル自体は、コメントだけなのでどこに置いておいても構わないのですが、今回は app/config/swagger.phpとしておきます。

app/config/swagger.php
<?php
/**
 * @SWG\Swagger(
 *     basePath="/api",
 *     host="api.example.com",
 *     schemes={"https"},
 *     produces={"application/json"},
 *     consumes={"application/json"},
 *     @SWG\Info(
 *         version="1.0.0",
 *         title="Swaggerサンプル",
 *         description="SwaggerサンプルAPI仕様",
 *         @SWG\Contact(name="foo@example.com"),
 *         @SWG\License(name="ライセンス表記")
 *     ),
 *
 *     @SWG\Definition(
 *         definition="PageInfo",
 *         required={"page_count", "current_page", "has_next_page", "has_prev_page", "count", "limit"},
 *         @SWG\Property(
 *             property="page_count",
 *             type="integer",
 *             format="int32",
 *             description="総ページ数"
 *         ),
 *         @SWG\Property(
 *             property="current_page",
 *             type="integer",
 *             format="int32",
 *             description="現在のページ番号"
 *         ),
 *         @SWG\Property(
 *             property="has_next_page",
 *             type="boolean",
 *             description="次ページ有/無"
 *         ),
 *         @SWG\Property(
 *             property="has_prev_page",
 *             type="boolean",
 *             description="前ページ有/無"
 *         ),
 *         @SWG\Property(
 *             property="count",
 *             type="integer",
 *             format="int32",
 *             description="ページ内アイテム数"
 *         ),
 *         @SWG\Property(
 *             property="limit",
 *             type="integer",
 *             format="int32",
 *             description="ページ内最大アイテム数"
 *         )
 *     )
 * )
 */
Modelの定義

CakePHP3では、エンティティの内容を明示的に宣言しないので今回は、各Entityクラスのヘッダーに定義を記述しました。実際の記述は以下のような感じになります。

Model/Entity/User.php
<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
 * User Entity.
 *
 * @SWG\Definition(
 *      definition="User",
 *      required={
 *          "id", "username", "password", "created", "modified"
 *      },
 *      @SWG\Property(
 *          property="id",
 *          type="integer",
 *          description="ユーザid"
 *      ),
 *      @SWG\Property(
 *          property="username",
 *          type="string",
 *          description="ユーザ名",
 *          maxLength=255,
 *      ),
 *      @SWG\Property(
 *          property="password",
 *          type="string",
 *          description="パスワード",
 *          maxLength=255,
 *      ),
 *      @SWG\Property(
 *          property="created",
 *          type="datetime",
 *          description="作成日時"
 *      ),
 *      @SWG\Property(
 *          property="modified",
 *          type="datetime",
 *          description="更新日時"
 *      )
 * )
 */
class User extends Entity
{
...
}
Model/Entity/Article.php
<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

/**
 * Article Entity.
 *
 * @SWG\Definition(
 *      definition="Article",
 *      required={
 *          "id", "user_id", "title", "body", "created", "modified"
 *      },
 *      @SWG\Property(
 *          property="id",
 *          type="integer",
 *          description="アーティクルid"
 *      ),
 *      @SWG\Property(
 *          property="user_id",
 *          type="integer",
 *          description="ユーザid"
 *      ),
 *      @SWG\Property(
 *          property="title",
 *          type="string",
 *          description="タイトル",
 *			maxLength=255
 *      ),
 *      @SWG\Property(
 *          property="body",
 *          type="string",
 *          description="記事本文"
 *      ),
 *      @SWG\Property(
 *          property="created",
 *          type="datetime",
 *          description="作成日時"
 *      ),
 *      @SWG\Property(
 *          property="modified",
 *          type="datetime",
 *          description="更新日時"
 *      )
 * )
 */
class Article extends Entity
{
...
}
APIの定義

APIで使うモデルの定義が完了したので、次に実際のAPIの定義を記述します。APIの定義は各コントローラのアクションのコメントに記述します。すべてのAPIの記述方法を書くのとなかなかの量になるので、特徴的な記述を使うことが多い一覧系のAPI仕様の書き方について説明します。

今回は、CakePHP3+Crud Pluginを使用しましたので、一覧取得時の実際のレスポンスは以下の様な形式になります。

CakePHP3+Crud Pluginに関しては id:skywaker さんの以下の記事が詳しいです。
slywalker.hateblo.jp

{
    "success": true,		// boolean: 処理結果
    "data": [			// [Entity]: エンティティの配列
        {
            "id": "1",
            "username": 'test',
            "pasword": "xxxx",
            "created": "2016-01-01 01:01:01"
        }
    ],					// PageInfo: ページング情報
    "pagination": {
        "page_count": 1,
        "current_page": 1,
        "has_next_page": false,
        "has_prev_page": false,
        "count": 1,
        "limit": 20
    }
}

*** Controller/UsersController.php

/api/users.json はユーザ一覧を取得するAPIで20件ごとにページングされ、検索文字列を指定することで、ユーザ名で検索できる仕様とします。

レスポンスコードごとに@SWG\Responseブロックを記述してレスポンス内容の解説と、どんなデータが帰るかを記述します。

@SWG\Propertytypearrayとして記述し、参照先のモデルを@SWG\Itemsで参照することができます。また、プロパティ自体が、定義済みのモデルを使用する場合は、ref="#/definitions/PageInfo"の様に記述することで参照できます。

<?php
namespace App\Controller\Api;

use Cake\Event\Event;
use Cake\ORM\TableRegistry;

class UsersController extends ApiController
{
	...
    /**
     * Index method
     *
     * @return void
     * @SWG\Get(
     *      path="/users",
     *      description="ユーザ一覧",
     *      produces={"application/json"},
     *      @SWG\Parameter(
     *          description="検索文字列",
     *          in="path",
     *          name="q",
     *          required=false,
     *          type="string"
     *      ),
     *      @SWG\Response(
     *          response=200,
     *          description="取得成功",
     *          @SWG\Schema(
     *              @SWG\Property(
     *                  property="status",
     *                  type="boolean",
     *                  description="処理結果",
     *              ),
     *              @SWG\Property(
     *                  property="data",
     *                  type="array",
     *                  @SWG\Items(ref="#/definitions/User"),
     *                  description="ユーザ情報",
     *              ),
     *              @SWG\Property(
     *                  property="pagination",
     *                  ref="#/definitions/PageInfo",
     *                  description="ページ情報",
     *              ),
     *          )
     *      ),
     *      @SWG\Response(
     *          response="401",
     *          description="Unauthorized"
     *      ),
     * )
     */
    public function index()
    {
    	...
    }

*** Controller/ArticlesController.php

api/articles.jsonは、記事の一覧を返すAPIを想定しています。api/users.jsonと違い、記事のデータだけでなく記事を投稿したユーザを含む形で以下の様なレスポンスを返すことにします。

{
    "success": true,		// boolean: 処理結果
    "data": [			// [Article]: 記事の配列
        {
            "id": "1",
            "user_id": 1,
            "title": 'test title',
            "body": "test body",
            "created": "2016-01-01 01:01:01"
            "user": {
	            "id": "1",
	            "username": 'test',
	            "pasword": "xxxx",
	            "created": "2016-01-01 01:01:01"
	        }
        }
    ],					// PageInfo: ページング情報
    "pagination": {
        "page_count": 1,
        "current_page": 1,
        "has_next_page": false,
        "has_prev_page": false,
        "count": 1,
        "limit": 20
    }
}

記事一覧のレスポンスのdata内で定義しているItemは先ほど定義したArticleではなく、ユーザ情報を含む形で定義した、IndexArticleを指定しています。

IndexArticleの定義は allOfを使うことで定義済みのArticleモデルの情報を再利用して、userプロパティを追加しています。

この様に、@ref,@SWG\ItemsallOfなどを使用して定義済みのモデルを参照することで、個別のAPIごとにレスポンス内容が微妙み異なる場合などに記述量をかなり減らすことができます。

<?php
namespace App\Controller\Api;

use Cake\Event\Event;
use Cake\ORM\TableRegistry;

class ArticleController extends ApiController
{
	...
    /**
     * Index method
     *
     * @return void
     * @SWG\Get(
     *      path="/articles",
     *      description="記事一覧",
     *      produces={"application/json"},
     *      @SWG\Parameter(
     *          description="検索文字列",
     *          in="path",
     *          name="q",
     *          required=false,
     *          type="string"
     *      ),
     *      @SWG\Response(
     *          response=200,
     *          description="取得成功",
     *          @SWG\Schema(
     *              @SWG\Property(
     *                  property="status",
     *                  type="boolean",
     *                  description="処理結果",
     *              ),
     *              @SWG\Property(
     *                  property="data",
     *                  type="array",
     *                  @SWG\Items(ref="#/definitions/IndexArticle"),
     *                  description="記事",
     *              ),
     *              @SWG\Property(
     *                  property="pagination",
     *                  ref="#/definitions/PageInfo",
     *                  description="ページ情報",
     *              ),
     *          )
     *      ),
     *      @SWG\Response(
     *          response="401",
     *          description="Unauthorized"
     *      ),
     * )
     *
     * @SWG\Definition(
     *      definition="IndexArticle",
     *      allOf={
     *          @SWG\Schema(ref="#/definitions/Article"),
     *      },
     *      @SWG\Property(
     *          property="user",
     *          description="ユーザ情報",
     *          ref="#/definitions/User"
     *      )
     * )
     */
    public function index()
    {
    	...
    }

swagger-phpのインストール

APIの定義の記述が終わったら、記述したアノテーションを解析するツール swagger-php をインストールしてSpecを生成します。composerでインストールできるので、いつものようにこんな感じですね。

$ composer require --dev zircote/swagger-php

Specファイルの生成

アプリケーションのルートで以下の様な感じでswagger-phpを実行します。-o で出力先のディレクトリを指定できるので適宜環境に合わせて指定してください。今回は、Specファイルは後述のswagger-uiで参照しますので、ブラウザからアクセスできる必要があります。

$ ./app/vendor/bin/swagger ./app/src/ -o ./app/webroot/api-documents/

swagger-uiのインストール

Specファイルを解析して、APIドキュメントとして表示する swagger-ui は以下で公開されています。

https://github.com/swagger-api/swagger-ui

このリポジトリの `dist` ディレクトリの内容をブラウザからアクセスできる場所に適宜コピーしてください。

swagger-uiの設定を修正

設置したswagger-ui内のindex.htmlの以下の箇所を、先ほど作成したSpecファイルを参照するように修正します。

var url = window.location.search.match(/url=([^&]+)/);
if (url && url.length > 1) {
  url = decodeURIComponent(url[1]);
} else {
  url = "http://petstore.swagger.io/v2/swagger.json"; // ここを先ほど作成したSpecファイルのURLに修正
}

問題なく生成されれば以下のように、APIドキュメントが参照できます。



f:id:kaz_29:20160218112002p:plain

f:id:kaz_29:20160218112015p:plain

今回作成したファイルは gistで公開しましたので swagger-uiで実際にAPIドキュメントをみられます

まとめ

APIドキュメント作成は、なかなかに気の重い作業という人も多いと思いますがswaggerを使うとかなり手間を減らせる上に、実際のコード内に仕様を記述するため、APIが更新されたのにドキュメントが更新されないといった問題も防ぎやすくなると思います。また、CIに組み込んでリポジトリの更新時に自動生成・公開するようにすることも簡単にできますので、APIの実装が完了したらAPIドキュメントも自動で公開するみたいなこともできますね。

チームで効率良く開発を進めるには、必要なドキュメントをいかに効率良く作成するかがとても重要だと思うので是非swaggerを試してみてください!

ErrorTypeをNSErrorにキャストするとuserInfoが消えてしまう問題の対策

iPhone Swift2.0 Swift iOS

Swiftを使い始めてそろそろ4w位経ちますが、まだまだ細かく引っかかることが多くて若干発狂気味なわたなべです。

今開発中のアプリで使う為に、Swift2+Alamofire+PromiseKit3でアプリの基盤的なものを作っているのですが、ErrorTypeをNSErrorにキャストするとuserInfoが消えてしまう問題が発生したので今回とった対策のメモ

以下のようにPromiseKitから受け取る error は ErrorType(実態はNSError)なので、userInfoを参照できるようにNSError型の変数に代入するとuserInfoが消えてしまいます。

do {
    try RestAPI.post(data)
        .then { (response: Foo) -> Void in
            debugPrint(response)
        }.error { (error) -> Void in
            let err: NSError = error as NSError // これでerr.userInfo が消える...
        }
} catch let error as NSError {
    // Error
}

色々調べた結果以下のようにすることでNSErrorにキャスト出来るようです。

    ((error as Any) as! NSError).localizedDescription

毎回キャストするのは面倒なのでこんな extension を作って対応しました。(作ったってほどのものじゃないけど...)

extension ErrorType {
    func nserror() -> NSError {
        return ((self as Any) as! NSError)
    }
}

このextensionを使えばこんな感じにかけてちょっとスッキリしました。

do {
    try RestAPI.post(data)
        .then { (response: Foo) -> Void in
            debugPrint(response)
        }.error { (error) -> Void in
            debugPrint(error.nserror().userInfo)
        }
} catch let error as NSError {
    // Error
}

Swift2はまだまだはまることも多いですが、Objective-Cと比べて格段にコード量は減るし便利なことも沢山あるのでいい感じです。早く使いこなせるようにならないとねー。

参考

Swift 2標準ガイドブック

Swift 2標準ガイドブック

stackoverflow.com