2018年5月31日木曜日

Sinatra でコントローラを分離する方法

概要

app.rb など一つのファイルにいろいろなルーティングを実装するとファイルを分けたくなると思います
今回はその方法を紹介したいと思います
おそらく王道の方法かなと思います

環境

  • macOS 10.13.4
  • Ruby 2.5.1p57
  • Sinatra 2.0.1

各種ファイルの準備

  • bundle init
  • vim Gemfile
gem "sinatra"
  • bundle install --path vendor
  • touch config.ru
  • mkdir controllers
  • touch controllers/home.rb
  • touch controllers/admin.rb
  • mkdir views
  • touch views/home.erb
  • touch views/admin.erb

今回は分離するコントローラは controllers というディレクトリで管理することにします
全体の構成は以下のとおりです

.
├── .bundle
│   └── config
├── Gemfile
├── Gemfile.lock
├── config.ru
├── controllers
│   ├── admin.rb
│   └── home.rb
└── views
    ├── admin.erb
    └── home.erb

3 directories, 8 files

config.ru の作成

Sinatra でコントローラを分ける場合、パスごとに処理するコントローラを Rack::URLMap という機能を使って定義します
今回は 2 つのルーティングを違うコントローラを使って処理します

require './controllers/home.rb'
require './controllers/admin.rb'

run Rack::URLMap.new({
        '/' => Home,
        '/admin' => Admin
})

作成した Rack::URLMap を run することを忘れないようにしてください
home.rb に Home クラスを admin.rb に Admin クラスを後で定義します

home.rb の作成

先程定義した / ルーティングを処理するクラスを定義します

  • vim controllers/home.rb
require 'sinatra/base'

class Home < Sinatra::Base
        set :root, File.join(File.dirname(__FILE__), '..')
        set :views, Proc.new { File.join(root, "views") } 

        get '/' do
                erb :home
        end

        get '/users' do
                'users'
        end
end

今回は 2 つの処理を記載しました
/ に対する処理と /users に対する処理です
ついでにテンプレートも作成します

  • vim views/home.erb
home

とりあえず文字列を返すようにします
作成したクラスは必ず Sinatra::Base を継承します
また今回のようにコントローラをアプリケーションの直下ではなく controllers といったサブディレクトリに配置する場合には Sinatra の :root:views の書き換えが必要になります

set :root, File.join(File.dirname(__FILE__), '..')
set :views, Proc.new { File.join(root, "views") } 

何をしているかというと home.rb の一つ上位のディレクトリを :root に設定しかつ :root 配下にある views というディレクトリをテンプレートを定義するディレクトリとして設定しています
デフォルトだとこれがコントローラクラスの直下になっています

admin.rb の作成

今度は /admin 配下を処理する Admin クラスを定義します

  • vim controllers/admin.rb
require 'sinatra/base'

class Admin < Sinatra::Base
        set :root, File.join(File.dirname(__FILE__), '..')
        set :views, Proc.new { File.join(root, "views") } 

        get '/' do
                erb :admin
        end

        get '/users' do
                'admin users'
        end
end
  • vim views/admin.erb
admin

先程の home.rb とほぼ同じように定義されていることがわかります
が、admin.rb/admin 配下のルーティングを処理します
なので上記の場合、処理するパスとしては /admin/admin/users が対象になります
このように URLMap で定義したパスはコントローラ内の実装では省略することができます

動作確認

  • bundle exec rackup config.ru

で起動しましょう
localhost:9292 で起動します
確認するパスとしては以下の 4 つです

  • / -> home が返ってくる
  • /users -> users が返ってくる
  • /admin -> admin が返ってくる
  • /admin/users -> admin users が返ってくる

となれば OK です
上記以外のパスにアクセスした場合は Sinatra のデフォルトの 404 ページが返ってくると思います

ちょっとしたテクニック

「404 や 500 ページは共通のページを出したい」
「コントローラクラスに毎回 :root:views を定義するのは面倒」
という場合には Base クラスを作成しましょう

  • vim controllers/base.rb
require 'sinatra/base'

class Base < Sinatra::Base
        set :root, File.join(File.dirname(__FILE__), '..')
        set :views, Proc.new { File.join(root, "views") } 
end

そして home.rb で継承するクラスを Base クラスに変更すれば OK です

  • vim controllers/home.rb
require './controllers/base.rb'

class Home < Base
        get '/' do
                erb :home
        end

        get '/users' do
                'users'
        end
end

こうすることで毎回定義する必要がなくなります
また、先程少し出てきた 404 ページの処理などもこの base.rb に記載すれば継承した home.rb などで実装する必要がなくなります

また rspec と組み合わせたテストを行いたい場合は spec_helper.rb で app メソッドを定義する際に Rack::URLMap を返すようにすれば OK です
各種コントローラクラスの require も忘れないようにしてください

module RSpecMixin
  include Rack::Test::Methods

  def app
    Rack::URLMap.new({
      '/' => Home,
      '/admin' => Admin
    })
  end
end

最後に

Sinatra でコントローラを分離する方法を紹介しました
おそらくこれが王道の方法だと思います

このあたりの処理は Sinatra では自分で実装するしかありません
が、rails などを使えばフレームワーク側で全部やってくれるものもあります
ただ、rails の場合はルーティングを追加すると実装するべき関数も勝手にいろいろ定義されるのでやりすぎちゃう感もあります
この辺りは自分のやりたいことに合わせて選択すれば良いかなと思います

参考サイト

2018年5月30日水曜日

Mac で Android の動画撮影をしよう

概要

android-tool という定番ソフトを使えば簡単に撮影することできます

環境

  • macOS 10.13.4
  • android-tool 1.66
  • Huawei p9 lite

事前準備

今回使用する端末は SIM フリー版の Huawei p9 lite です
事前にデベロッパーモード+ USB デバッグを ON にしておきましょう

  • 設定アプリ -> 端末情報 -> ビルド番号

を 7 回タップすれば OK です
そして

  • 設定アプリ -> 開発向けオプション -> USB デバッグ

のトグルを ON にしましょう
何度か確認が出ますがすべて許可してください

android-tool のインストール

  • brew cask install androidtool

zip をダウンロードして .app を配置しても OK です

起動する

まずは android-tool を起動しましょう
ミッションコントロールとか Spotlight から起動すれば OK です
android_tool1.png

こんな感じになった Mac に端末を接続しましょう
Android 側に許可するかどうか確認するダイアログが出たら許可するを選択してください
問題なく認識されると以下のように表示が変わります
android_tool2.png

動画を撮影する

で撮影してみようと思ったのですがどうやら High Sierra だと現状動作しないようです、、、

開発も止まっているようで修正は厳しいと思います
adb コマンドを使えばできるようですが、p9 lite には screenrecord コマンドがないので動きません

仕方ないので p9 lite の動画撮影機能を使う

はじめからこれを使えば良かったのですが p9 lite には純正の録画機能があるようです

使い方は簡単で電源ボタン+ボリュームアップボタンを長押ししましょう
すると録画開始画面になるので「有効」を選択します
(ちなみに Android から Mac へのファイルの転送は Android File Transfer を使っています)

3, 2, 1 のカウントダウン後に録画が開始されます
終了したい場合は再度「電源ボタン+ボリュームアップボタン」を押せば OK です
撮影したデータは Pictures -> Scrrenshots 配下にあります
android_tool4.png

マイクも有効になるのでしゃべると声が入ってしまうので注意してください

最後に

androidtool をつかう予定でしたがどうやら High Sierra では動作しないということがわかったので急遽 p9 lite に標準で搭載されている録画アプリを使ってみました
他にも探せば同じようなアプリが出てくるのでマイクの録音をしたくない場合などには別のアプリを使ってみてください

2018年5月29日火曜日

macOS High Sierra に ngrok をインストールして使ってみる

概要

ngrok は簡単に言えば localhost で動作しているアプリをインターネットに公開することができるツールです
主にはテスト等で使用します

環境

  • macOS 10.13.4
  • ngrok 2.2.8

ngrok のインストール

  • brew cask install ngrok

使ってみる

とりあえず localhost で何かアプリを動作させてみましょう
何でも OK なので適当に docker で動かします

  • docker run -d -p 8080:80 nginx

この 8080 ポートを ngrok を使ってインターネットからアクセスできるようにしてみます

  • ngrok http 8080

するとターミナルが以下のような表示なります
ここで表示されている ngrok.io のアドレスにアクセスしてみましょう
ngrok1.png

すると以下のようにちゃんと nginx のデフォルト画面が表示されると思います
ngrok2.png

更にターミナルには nginx のログが表示されています
ngrok3.png

終了する場合は Ctrl+c で終了できます

最後に

macOS に ngrok をインストールして使ってみました
ngrok にはアカウントの登録機能があるようですが、この機能はアカウント登録なしで使えるようです

何に使うのかですが、例えば OGP タグの動作確認などあります
localhost だと動作しない場合にドメインを降ることで OGP タグの動作を確認することができます

かなり便利なので覚えておいて損はないと思います

参考サイト

2018年5月28日月曜日

標準ライブラリの net/http を使ったサンプル集

概要

net/http は Ruby に標準でインストールされている HTTP のクライアントライブラリです
Ruby にはいろいろな HTTP のクライアントライブラリがありますがこれは標準で使えます
gem をインストールするまでもない場合は net/http を使えば OK です
簡単な使い方のサンプルを紹介します

環境

  • macOS 10.13.4
  • Ruby ruby 2.5.1p57

とりあえず GET する

require 'net/https'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Get.new uri.request_uri
res = http.request req
pp res.body

ヘッダを設定する

require 'net/https'
require 'json'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Get.new uri.request_uri
req['X-Test-Header'] = 'test_header'
res = http.request req
pp JSON.parse(res.body)['headers']['HTTP_X_TEST_HEADER']

req にハッシュとして追加すれば OK です
Content-Type などは専用のメソッドがあります

  • req.content_type = 'text/xml'

POST する

require 'net/https'
require 'json'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new uri.request_uri
res = http.request req
pp JSON.parse(res.body)['method']

パラメータ付きで POST する

require 'net/https'
require 'json'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new uri.request_uri
req.content_type = 'application/json'
req.body = { 'test_key' => 'test_value' }.to_json
res = http.request req
pp JSON.parse(res.body)['body']

XML をボディに設定する

require 'net/https'
require 'json'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new uri.request_uri
req.content_type = 'text/xml'
req.body = '<Root><Name>bob</Name><Age>30</Age></Root>'
res = http.request req
pp JSON.parse(res.body)['body']

DELETE をコールする

require 'net/https'
require 'json'

uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Delete.new uri.request_uri
res = http.request req
pp JSON.parse(res.body)['method']

http にアクセスしたい場合

net/https の代わりに net/http を require し

  • http.use_ssl = true

の設定を削除すればそれ以外のコードをそのまま使えます

最後に

net/http の簡単な使い方を紹介しました
他にもいろいろな使い方があるので詳細は公式のドキュメントを見ることをおすすめします

2018年5月27日日曜日

xmlsimple で xml_out を使ってハッシュから XML を生成する場合のコツ

概要

Ruby の xmlsimple ライブラリを使ってハッシュから XML を生成してみました
基本的な使い方から使う際のポイントを紹介します

環境

  • macOS 10.13.4
  • Ruby 2.5.1p57
  • xmlsimple 1.1.5

xml_out のサンプル

とりあえずサンプルです
今回は AWS の route53 でゾーンを作成するために必要な XML CreateHostedZoneRequest を作成してみます
ハッシュを定義してそれを食わすことで XML を出力しています

require 'xmlsimple'

body = { 
  '@xmlns' => 'https://route53.amazonaws.com/doc/2013-04-01/',
  'Name' => ['content' => ''],
  'CallerReference' => ['content' => ''],
  'HostedZoneConfig' => {
    'Common' => ['content' => ''],
    'PrivateZone' => ['content' => ''],
  },
  'DelegationSetId' => ['content' => ''],
  'VPC' => {
    'VPCId' => ['content' => ''],
    'VPCRegion' => ['content' => ''],
  }
}

options = {
  'AttrPrefix' => true,
  'RootName' => 'CreateHostedZoneRequest',
  'ContentKey' => 'content'
}

puts XmlSimple.xml_out(body, options)

結果は以下のようになります
どうしてこうなるのか詳細を説明します

<CreateHostedZoneRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">                                                  
  <Name></Name>
  <CallerReference></CallerReference>
  <HostedZoneConfig>
    <Common></Common>
    <PrivateZone></PrivateZone>
  </HostedZoneConfig>
  <DelegationSetId></DelegationSetId>
  <VPC>
    <VPCId></VPCId>
    <VPCRegion></VPCRegion>
  </VPC>
</CreateHostedZoneRequest>

説明

XML に変換する対象のハッシュ body はとりあえずおいておきます

XmlSimple では hash -> XML の変換ルールを options で定義します
今回は AttrPrefix, RootName, ContentKey の 3 つの設定をしています
他にも様々な設定ルールがあります
(サイトは CPAN のドキュメントになります)

その中でも上記 3 つのオプションはよく使うと思います
それぞれ説明すると

  • AttrPrefix・・・@ で始まるキーがあった場合はそれを XML のタグとして使用せず属性として使用する
  • RootName・・・XML のルートドキュメントの名前を指定します、これを指定しない場合は <opt> になってしまいます
  • ContentKey・・・XML の値として使用するデータを明示的に指定することができます

となります
ここで body とそれぞれのオプションの効果を見ていきます
まず冒頭の '@xmlns' => 'https://route53.amazonaws.com/doc/2013-04-01/' ですがこれは AttrPrefix の効果でタグにはなりません
ルートの位置で定義しているのでルートの属性情報として使用されます

  • <CreateHostedZoneRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">

次にハッシュの各所に定義してある ['content' => ''] ですが、これは ContentKey が影響します
このオプションの値とハッシュ内で使われている値がどちらも content になっているのがわかると思います
つまりこの content の値で指定したデータが XML に出力されたときのタグないのデータになるわけです
ContentKey は必須ではないですが、タグに属性が入る場合はほぼ必須になると思います
またデータとなるキーがわかりやすくもなるので使うことをおすすめします

最後に RootName ですがこれは生成したい XML ルートを指定するだけです

Tips

XML declaration を定義したい

例えば <?xml version='1.0' standalone='yes'?> の情報です
XML の先頭に付与する情報になります
XmlDeclaration というオプションが使えるのでそれを使います
先頭に追加したい文字列を直接指定することで追加することができます

options = {
  'AttrPrefix' => true,
  'RootName' => 'CreateHostedZoneRequest',
  'ContentKey' => 'content',
  'XmlDeclaration' => '<?xml version=\'1.0\' standalone=\'yes\'?>'                                                                
}

xml_in は可逆ではない

例えば今回生成できた XML をそのまま xml_in メソッドに投げも同じハッシュを得ることはできません

out = XmlSimple.xml_out(body, options)
pp XmlSimple.xml_in(out)
{"xmlns"=>"https://route53.amazonaws.com/doc/2013-04-01/",
 "Name"=>[{}],
 "CallerReference"=>[{}],
 "HostedZoneConfig"=>[{"Common"=>[{}], "PrivateZone"=>[{}]}],
 "DelegationSetId"=>[{}],
 "VPC"=>[{"VPCId"=>[{}], "VPCRegion"=>[{}]}]}

当然と言えば当然ですが上記のようになります
ハッシュからデータを取得するときに気をつけるのはデータの部分が必ず配列に格納されている点ですデータが 1 つしかなくても配列に入ります

最後に

Ruby の xmlsimple を使ってハッシュから XML を生成してみました
結構クセのある使い方をするのとドキュメントが少ないのが辛い点です

ハッシュから XML に変換するのであればこの方法でも良いですが単純に XML を欲しいだけであれば nokogiri や rexml でも生成できます
もしくは erb を使って XML をテンプレート化すればそこからでも XML 情報を生成することができます

今回紹介した方法も XML を生成する手段の一つにしかすぎませんので各自の環境にあった最適な方法を選択するようにしてください

2018年5月26日土曜日

Ubuntu16.04 に kubernetes をインストールしてみた

概要

Ubuntu に kubernetes をインストールしてみました
今回はクラウドは使わず localhost の lxd 上に必要なコンポーネントをデプロイしています

環境

  • Ubuntu 16.04
  • conjure-up 2.5.6
  • kubernetes 1.10
  • lxd 3.0.0 (required)

snapd のインストール

  • apt -y install snapd

conjure-up のインストール

  • snap install conjure-up --classic

一旦ログアウトして再度ログインしましょう
conjure-up の設定を反映させます

lxd のインストール

今回は localhost に対して kubernetes 環境を構築するので lxd が必要になります
lxd 上に必要なコンポーネントをデプロイします

  • snap install lxd

lxd の初期化

  • lxd init

いろいろ聞かれます
3 箇所で設定を変更しています

  • Name of the storage backend to use -> dir
  • What IPv6 address should be used? -> none
  • Would you like LXD to be available over the network? -> yes

あとはすべてデフォルトを設定しています

Would you like to use LXD clustering? (yes/no) [default=no]:
Do you want to configure a new storage pool? (yes/no) [default=yes]:                                                             
Name of the new storage pool [default=default]:
Name of the storage backend to use (btrfs, ceph, dir, lvm, zfs) [default=zfs]: dir                                               
Would you like to connect to a MAAS server? (yes/no) [default=no]:                                                               
Would you like to create a new local network bridge? (yes/no) [default=yes]:                                                     
What should the new bridge be called? [default=lxdbr0]:
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:                                       
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]: none                                  
Would you like LXD to be available over the network? (yes/no) [default=no]: yes                                                  
Address to bind LXD to (not including port) [default=all]:
Port to bind LXD to [default=8443]:
Trust password for new clients:
Again:
No password set, client certificates will have to be manually trusted.Would you like stale cached images to be updated automatically? (yes/no) [default=yes]
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]:

Error: Failed to create network 'lxdbr0': Failed to run: dnsmasq ... みたいなエラーが出る場合はすでに 53 ポートをバインドしているプロセスがあります (参考)
例えば bind などを動かしている場合はエラーになるので停止してあげます

  • systemctl stop bind9

ユーザの作成

conjure-up コマンドは root ユーザでは実行できないのでコマンドを実行する専用のユーザを作成します

  • adduser conjure
Adding user `conjure' ...
Adding new group `conjure' (1000) ...
Adding new user `conjure' (1000) with group `conjure' ...
The home directory `/home/conjure' already exists.  Not copying from `/etc/skel'.
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
Changing the user information for conjure
Enter the new value, or press ENTER for the default
        Full Name []: 
        Room Number []: 
        Work Phone []: 
        Home Phone []: 
        Other []: 
Is the information correct? [Y/n] Y

こんな感じで作成すれば OK です
パスワードなど対話形式で聞かれますので適当に設定してください

あと lxd のグループに追加し sudo ができるようにします

  • usermod -a -G lxd conjure
  • gpasswd -a conjure sudo

移行の作業は作成した conjure ユーザで行います

kubernetes のインストール

  • su - conjure
  • conjure-up kubernetes

対話形式でインストールします
矢印の上下で「Kubernetes Core」を選択して Enter でインストールしましょう
ubuntu_kube1.png

どの環境を使用するか選択する画面になるので、今回は「localhost」を選択します
lxd が起動していれば localhost が選択できるようになっています
選択できない場合は lxd が起動しているか conjure ユーザが lxd グループに所属しているなど調べてください
ubuntu_kube2.png

ネットワークブリッジとストレージプールを選択します
とりあえずそのままで OK です
TAB でカーソルを Save に合わせて Enter で次に行きます
ubuntu_kube3.png

ネットワークプラグインは flannel を選択します
ubuntu_kube4.png

sudo のパスワードを入力します
ubuntu_kube5.png

各コンポーネントをデプロイする設定をします
ここもデフォルトで OK です
ubuntu_kube6.png

デプロイされるので待ちましょう
ubuntu_kube7.png

以下の画面にならないでエラーになってしまう場合は ufw をオフにしてみてください

  • ufw disable

lxd 上に各コンポーネントのデプロイが完了するまで待ちましょう
各コンポーネント間で連携チェックをしながらデプロイされるので、ここは割りと時間がかかります
maintenance や blocked のステータスを繰り返しますが active になるまで辛抱強く待ちます
ubuntu_kube8.png

すべて active になれば次に進みます
Quit でインストールを完了します
ubuntu_kube9.png

動作確認

kubectl コマンドが使えるので確認してみます

  • kubectl --kubeconfig=.kube/config cluster-info

これでダッシュボードの IP の情報や各コンポーネントのエンドポイントが表示されると思います

また lxc list コマンドで lxd 上に各コンポーネントがデプロイされていることも確認できると思います

+---------------+---------+-----------------------+------+------------+-----------+
|     NAME      |  STATE  |         IPV4          | IPV6 |    TYPE    | SNAPSHOTS |
+---------------+---------+-----------------------+------+------------+-----------+
| juju-6e5d3b-0 | RUNNING | 10.156.235.117 (eth0) |      | PERSISTENT | 0         |
+---------------+---------+-----------------------+------+------------+-----------+
| juju-de4886-0 | RUNNING | 10.156.235.71 (eth0)  |      | PERSISTENT | 0         |
|               |         | 10.1.82.0 (flannel.1) |      |            |           |
|               |         | 10.0.113.1 (lxdbr0)   |      |            |           |
+---------------+---------+-----------------------+------+------------+-----------+
| juju-de4886-1 | RUNNING | 172.17.0.1 (docker0)  |      | PERSISTENT | 0         |
|               |         | 10.156.235.35 (eth0)  |      |            |           |
|               |         | 10.1.5.1 (cni0)       |      |            |           |
|               |         | 10.1.5.0 (flannel.1)  |      |            |           |
+---------------+---------+-----------------------+------+------------+-----------+

おまけ kubernetes-master のエンドポイントにアクセスする

今回だと lxdbr0 の IP が振られてしまっているため Ubuntu からしかアクセスできません
が SSH のポートフォーワードを使えばクライアントマシンにアクセスすることでエンドポイントにアクセスすることができます

  • ssh -N -L localhost:6443:10.156.235.71:6443 root@kube -i /path/to/secret.pem

これで https://localhost:6443 で master の API 一覧を確認することができます
ちなみにパスワードは

  • kubectl --kubeconfig=.kube/config config view

で確認できます
ちなみにダッシュボード URL が長いのですが

  • https://10.156.235.71:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy

ubuntu_kube10.png
でアクセスできます (IP の部分は各自の lxdbr0 の IP に変更してください)
ログインするには

  • kubectl -n kube-system get secret
  • kubectl -n kube-system describe secret default-token-bwmbd

で表示されるトークンを入力してください

最後に

Ubuntu16.04 上に kubernetes をインストールしてみました
嵌りポイントは少しありましたが割りとスムーズにインストールできました
過去に minikube を使った構築方法を紹介しましたが mac がない場合には今回の方法を使うと良いと思います

次回はこの環境にコンテナを立てたりしたいと思います

参考サイト

2018年5月25日金曜日

Zapier で Webhook を使ってツイートする

概要

Zapier は IFTTT のようなマッシュアップサービスです
今回は Webhook 機能を使ってそれをトリガーにツイートしてみました

環境

  • macOS 10.13.4
  • Zapier 2018/05/21 時点

Twitter アカウント連携

事前に Zapier で Twitter アカウントを連携しておきましょう
Home で Twitter のアプリを選択しそのまま下にスクロールすると設定できる画面があるので連携しましょう (画面は認証済み)
zapier_webhook1.png

Connect すると Twitter の OAuth 画面になるのでログインしましょう
もしすでにログインしている場合、そのログインしているアカウントと連携されてしまうので、事前にログアウトして連携したいアカウントで再度ログインしてください

zap の作成

では Webhook を使った zap を作成していきます
ホーム画面から右上の「Make a Zap!」を選択します
zapier_webhook2.png

トリガーの作成

トリガーの設定画面で Webhook を選択します
zapier_webhook3.png

トリガーの種類を選択します
Webhook したらツイートするので「Catch Hook」を選択します
zapier_webhook4.png

Zapier の Webhook はリクエスト時に JSON ボディを設定できます
基本はフラットな JSON を自由に設定できるのですが、入れ子にしたい場合は指定しなければいけません
今回は特に入れ子にはしないのでそのまま次に進みます
zapier_webhook5.png

Webhook のテスト画面になります
Webhook 用の URL が表示されているのでコピーしましょう
そして「OK, I did this」をクリックすると Webhook の待ち状態がスタートします
zapier_webhook6.png

今回は curl を使ってテストしてみます
ターミナルから以下のコマンドを実行してみましょう

curl -v -H "Accept: application/json" \
        -H "Content-Type: application/json" \
        -X POST \
        -d '{"first_name":"Bryan","last_name":"Helmig","age":27}' \
        https://hooks.zapier.com/hooks/catch/12345/abcdeft/

Webhook 用の URL はサンプルになるので払い出された URL に変更してください
テストの場合 JSON ボディが何かしら設定されていないとテストが成功しないので適当に設定しリクエストします

これで画面に戻るとテストが成功していると思います
「Continue」でアクションの設定をしましょう
zapier_webhook7.png

アクションの作成

先程連携した Twitter アカウントでツイートするアクションを追加します
ちゃんと連携できていれば「YOUR APPS」に Twitter があるので選択しましょう
zapier_webhook8.png

「Create Tweet」を選択します
zapier_webhook9.png

ツイートしたいアカウントを選択します
おそらく先程連携したアカウントが 1 つ表示されると思います
Zapier は複数の Twitter アカウントを紐付けることができます
この機能は IFTTT にはないので嬉しい点です
一応「Test」を選択するとアカウントがちゃんと紐付いているか確認できます
zapier_webhook10.png

ツイートする内容を設定できます
「+」を選択すると先程 JSON ボディに載せた情報を使うことができます
今回は first_name を使ってみます
zapier_webhook11.png

テストを送信してみましょう
成功すると Twitter 側にツイートされているはずです
zapier_webhook12.png

Twitter 側
zapier_webhook13.png

これで Finish で zap の作成は完了です
zap 名を設定し有効にすれば使えます
zapier_webhook14.png

動作確認

念のため動作確認してみましょう
以下の curl を実行してみます

curl -v -H "Accept: application/json" \
        -H "Content-Type: application/json" \
        -X POST \
        -d '{"first_name":"Bob"}' \
        https://hooks.zapier.com/hooks/catch/12345/abcdeft/

すると以下のように違う名前でツイートすることができます
zapier_webhook15.png

最後に

Zapier を使って Webhook 経由でツイートしてみました
これを使うメリットとしては Twitter 側に API を使ったアプリを登録しなくて良いことです
最近だと Twitter アプリの登録には電話番号が必要になるのでその面倒な手間を省くことができます

また Zapier の場合 1 つの Zapier アカウント内で複数の Twitter アカウントを紐付けることができます
これは IFTTT にはない機能でやろうとしたらわざわざ専用の IFTTT アカウントを作成しなければいけません

使い勝手もそれほど IFTTT と変わらないので複数アカウントを管理したい場合にはおすすめです

参考サイト

2018年5月24日木曜日

High Sierra で Sound flower をインストールする方法

概要

High Sierra をクリーンインストールした際に Soundflower を再インストールしようと思ったのですが躓いたのでメモしておきます

環境

  • macOS 10.13.4
  • Sounfflower 2.0.0b2
  • Linux Photon 3.19.2

インストール方法

  1. Soundflower をダウンロードします
  2. dmg を展開してインストーラを起動してインストールを開始します
  3. セキュリティとプライバシーで「許可」ボタンを押しますが反応しないのでそのまま進めてとりあえずエラーにします
  4. 一旦 Mac をシャットダウンします
  5. セーフブートで起動します (起動時に Shift ボタン押しっぱなし)
  6. セキュリティとプライバシーから再度「許可」ボタンを押します、今度は押せます (ここで押せない場合はギブアップ)
  7. 再度シャットダウンして今度は通常通り Mac を起動します
  8. 再度、dmg からインストーラを実行すると無事インストールできます

長いですが要するに一度セーフブートして「許可」を押すのがポイントです

最後に

原因は全く不明ですが自分はこれでインストールできました
調べると同じように High Sierra + Soundflower のインストールに躓いている人がいるようです
さすがにこれは Apple さんも対応せざるを得ないと思うのでいづれ修正されると期待しています

2018年5月23日水曜日

Mac をクリーンインストールしたせいで Xcode の証明書がエラーになったので修正してみた

概要

Mac を High Sierra にクリーンインストールした際にキーチェーンのバックアップを忘れたせいで Xcode が iTunes Connect にある署名書と鍵をローカルで参照できなくなりました
Xcode に Repair 機能があったので実行して挙動を確認してみました

環境

  • macOS 10.13.4
  • Xcode 9.3.1 (9E501)

エラーの状況

クリーンインストール後 Xcode をインストールし既存のプロジェクトをリポジトリから取得して開いてみたところ以下のようなエラーになりました
new_dev_cert1.png

この状態では実機での動作や実機でのアーカイブができません
なので修正してみました
といっても Xcode さんがすべて解消してくれるのでやることは「Revoke」ボタンを押すだけです
ただ、いろいろと生成されるので何が生成されるか追ってみました

とりあえず押すだけで以下のようにエラーは解消されます
new_dev_cert4.png

iTunes Connect 側にできるもの

まず Certificates に iOS Development 用の証明書が追加されます
new_dev_cert2.png

次に App IDs に XC Wildcard の ID が追加されます
これが追加されることで今後別のプロジェクトを開いてもエラーが発生しなくなります
new_dev_cert3.png

キーチェインアクセスにできるもの

キーチェインアクセスにも鍵と証明書が自動的に登録されます

鍵は 1 つ登録されます
new_dev_cert5.png

証明書は 3 つ登録されます
new_dev_cert6.png

メール

もし既存の iOS Development がある場合それを削除して証明書を登録し直しているので以下のようなメールが来ると思います
new_dev_cert7.png

最後に

自分が確認している分では上記の変化やアクションが確認できました
あくまでも生成しているのは開発用の鍵や証明書なので作成し直したからといって本番に影響が出るということはありません

ただ、既存の開発証明書は削除されてしまうためもし Apple ID を共有して開発している場合は他の Xcode で今度はエラーが発生します
このあたりチームで開発するための証明書管理のベストプラクティスがあったと思うのでそれを参考にすると良いかなと思います

2018年5月22日火曜日

Service Worker 超入門

概要

先日の Google I/O 2018 などで更に勢いがました PWA ですがその基本的な知識として Service Worker があります
全く触ったことがなかったので今回はローカルキャッシュ機能を試してみました

環境

  • macOS 10.13.4
  • Google Chrome 66.0.3359.181

事前準備

今回は localhost を使います
実際に運用するときは Service Worker は https でないと動作しないようです

またページをホストするミドルウェアを用意しておいてください
特に指定はないので Apache Httpd なり nginx なりお好きなものを使ってください
ちなみに自分は Sinatra で動かしています
以下では http://localhost:9292/sw_test で動いているページを Service Worker 化します

HTML ファイルの準備

今回はオフラインキャッシュ機能を使います
簡単に言うと http://localhost:9292/sw_test にオフライン状態でもアクセスできるようにします
「localhost なんだからオフラインでもアクセスできるだろ」と思いますがローカルホストのプロセスを停止していてもアクセスできるようになります

<html>
<head>
<script>
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service_worker.js', { scope: '/sw_test' }).then(function(reg) {                             
    if(reg.installing) {
      console.log('Service worker installing');
    } else if(reg.waiting) {
      console.log('Service worker installed');
    } else if(reg.active) {
      console.log('Service worker active');
    }
  }).catch(function(error) {
    console.log('Registration failed with ' + error);
  });
}
</script>
</head>
<body>
test page
</body>

言わずもがなポイントは <script> タグ内の内容です
やっていることは /service_worker.js の登録になります
この専用の JavaScript を登録することでオフラインでもページが描画できるようにします
navigator.serviceWorker.register することで登録でき、それぞれのイベントに応じてログを出力しています

あと大事な概念としては「スコープ」があります
これも簡単に説明すると

Service Worker 化するページの範囲

を指定します
今回であれば /sw_test のページのみを Service Worker 化するのでそれを指定しています
サイト全体を対象にする場合は / と指定します

service_worker.js の作成

キモの部分となる Service Worker に処理させる内容をこの service_worker.js に記載します
service_worker.js は今回であれば http://localhost:9292/service_worker.js で見えるようにしておいてください

ちなみに JavaScript だから http://localhost:9292/js/service_worker.js という感じで配信したいという人もいると思いますがこれだと動きません (今回の場合は)
なぜかというと service_worker.js/sw_test の親もしくは子の範囲にいないからです
もう少し説明すると /sw_test/service_worker.js で配信するなら動作しますが、/sw_test2/service_worker.js だと動作しません
特にこだわりがないのであればルートに配置しておくのが無難だと思います

今回はオフライン時にキャッシュしたページを表示させるだけの処理を記載します
まず全体は以下のとおりです

let version = '0.1';

self.addEventListener('install', e => {
  let timeStamp = Date.now();
  e.waitUntil(
    caches.open('mismith').then(cache => {
      return cache.addAll([
        `/sw_test`
      ])
      .then(() => self.skipWaiting());
    })
  )
});

self.addEventListener('activate',  event => {
  event.waitUntil(self.clients.claim());
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request, {ignoreSearch:true}).then(response => {                                                          
      console.log('fetching');
      return response || fetch(event.request);
    })
  );
});

やっている処理を大きく 3 つです

  • インストール時に Service Worker がキャッシュするページを指定
  • Service Worker が activate したら直ちに起動
  • 対象のページにリクエスト (fetch) があったときにキャッシュしたページをレスポンスとして返却

になります
ポイントですがまずキャッシュするページは cache.addAll に配列してパスを指定します
今回であれば HTML だけなので /sw_test だけで OK です
感が良ければここで気づくと思いますが CSS や JS も必要なページの場合はそれらもキャッシュしなければなりません
例えば http://localhost:9292/js/custom.js という JavaScript が必要な場合は /js/custom.js を配列に追加してください

activatefetch のイベントに関してはとりあえずそのまま使えば OK です

動作確認

では動作確認してみましょう
ちなみに Chrome で確認する場合は開発ツールの「Application」->「Service Workers」で登録した Service Worker の一覧や現在の状態を確認することができます
chrome://serviceworker-internals/ に Chrome でアクセスしても登録済みの Service Worker の一覧を確認することができます

まず普通にページアクセスします
すると Service Worker の登録が行われます
かつ今回の場合であればページのキャッシュも同時に行われています
service_worker1.gif

次にオフラインにして確認してみましょう
先程の Chrome 開発ツールのところに「Offline」ボタンがあるのでこれを使って擬似的にオフライン状態を再現します
ちなみに本当にオフラインかどうか一度 Youtube にアクセスを試みています
service_worker2.gif

こんな感じでキャッシュしたページであればアクセスしてもエラーのページとはならないのがわかると思います

最後に

噂の Service Worker に入門してみました
今回はかなり基本的な機能しか試していません
Service Worker を使えばブラウザプッシュなどもできるようになります

使っていてポイントだなと感じたのはキャッシュするページの量が多いと大変だなと思いました
最近だと WebPack や browserify などを使って 1 つのファイルにまとめることができるのでそういった仕組みを使うと相性が良いかと思います
また、どうしても外部のサイトの JS を参照しなければいけない場合にはそれえらを外す必要があります
例えば Google Analytics ですが解析のための JavaScript コードが入っていると Service Worker がエラーを吐き続けてしまいます
Google Analytics に限って言えば解析のためのコードなのでページ自体は動作すると思いますがエラーが出続けてしまうというのが気になるところです

Service Worker 用の JavaScript を用意するだけで簡単に対応できるのでやっておいて損はないような気がします
というのもオフラインキャッシュがすごいというよりかは PWA 対応できるのでスマホアプリ化できると考えると損はないかなと思います
ただ、冒頭簡単に説明しましたが https が必須なので注意が必要です

参考サイト

2018年5月21日月曜日

Swift で UI のテストを書こう

概要

前回 Swift のユニットテストを書いてみました
今回は UI のテストを書いてみました

環境

  • macOS 10.13.4
  • Xcode 9.3.1 (9E501)

テスト対象の UI を作成する

StoryBoard を使って簡単な UI を作成します
とりあえず今回はテキストラベルを 1 つ配置しましょう
swift_ui_test1.png

そして作成したラベルに ID を設定しましょう
swift_ui_test2.png
テストではこの ID を使ってラベルのコンポーネントを取得します

テストを記載する

ではテストを書いていきます
テストを記載するのは testUITests になります
swift_ui_test3.png

mport XCTest

class testUITests: XCTestCase {

    override func setUp() {
        super.setUp()
        continueAfterFailure = false
        XCUIApplication().launch()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testExample() {
        let label1 = XCUIApplication().staticTexts["label1"]
        XCTAssertEqual(label1.label, "OK")
    }

}

最大のポイントは XCUIApplication になります
これを使って端末の起動や UI コンポーネントの取得、探索を行います
そして取得した各コンポーネントのプロパティにアクセスして文字列や設定されている色などを取得します
あとはユニットテストのときと同じで XCTAssert を使って値の比較をすれば OK です

各コンポーネントの取得方法はコンポーネントごとに異なるので詳しくはこちらを御覧ください

テストを実行する

Product -> Test で実行しましょう
Cmd + u でも実行できます
ユニットテストも実行されます
ユニットテストの後に UI のテストが実行されるので待ちましょう

各テストごとに結果が表示されます
またコンソールには各テストの実行時間とログのパスが表示されます
swift_ui_test4.png

最後に

Swift + Xcode で UI のテストを記載する方法を紹介してみました
ユニットテストと基本的には同じように書けます
UI コンポーネントを取得する方法だけ UI テスト独自の方法になるかなと思います

今回はかなり簡単な処理しか書いていないので考慮していませんが実際のアプリになるとネットワーク通信などが発生します
その場合はちゃんと通信を待ってから UI コンポーネント上の値をテストしなければなりません

その辺りも後々紹介できればなと思っています

参考サイト

2018年5月20日日曜日

Swift でユニットテストを書こう

概要

今までずっと逃げていたのでとりあえずテストする方法を勉強しました
まずは超かんたんなクラスのユニットテストから書いてみます

環境

  • macOS 10.13.4
  • Xcode 9.3.1 (9E501)

事前準備

プロジェクトを作成しましょう
今回は test という名前のプロジェクトとして進めていきます
プロジェクトは Single View Application として作成しました
今回は単純なクラスのテストなので、SpriteKit でも同じようにできると思います

テスト対象のクラスを作成

こんな感じで Swift ファイルを追加しましょう
UIViewController を継承した UI 専用のクラスではなくモデルとして管理するクラスを作成しました

class Person {
    var name = ""
    var height: Double = 0
    var weight: Double = 0

    init(_ name: String, _ height: Double, _ weight: Double) {
        self.name = name
        self.height = height
        self.weight = weight
    }

    func hi() -> String {
        return "Hi. I'm \(self.name)."
    }

    func bmi() -> Double {
        // 小数点 2 桁目で切り上げ
        let val = self.weight / (self.height / 100 * self.height / 100) * 100
        return val.rounded() / 100
    }
}

このコードをテストしてみます

テストを記載する

テストを記載します
プロジェクトを作成した際の雛形をベースにしています
記載するファイルは testTests.swift になります
swift_unit_test3.png

setUp, tearDown は特に何もしていませんが残しています
追加したのは testHItestBMI になります
命名規則としてテストメソッドは testXXX と先頭に test を付与する必要があります

import XCTest
@testable import test

class testTests: XCTestCase {

    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testHi() {
        let p = Person("taro", 160, 55)
        let ret = p.hi()
        XCTAssertEqual(ret, "Hi. I'm \(p.name).")
    }

    func testBMI() {
        let p = Person("hanako", 140, 40)
        let ret = p.bmi()
        XCTAssertEqual(ret, 20.41)
    }
}

XCT ほげほげでいろいろなアサートが用意されています
公式に Boolean 用のアサート、nil チェックのアサート、比較用のアサートなどアサートごとに使うメソッドが紹介されているのでテストに合わせて必要なアサートを使い分けると良いと思います

テストを実行する

テストが書けたら実行しましょう
Product -> Test でテストを実行できます
swift_unit_test1.png

今回の方法だと UI のテストも実行されてしまうのでシミュレータも起動してしまいます
少しだけ時間がかかりますが待ちましょう

テストが終了すると左メニューの項目からテスト結果を確認することができます
swift_unit_test2.png

コンソールにテスト結果が表示されます
デフォルトだと各テストにかかった時間が表示されます
またログはファイルにも保存してくれるようです

Test Suite 'All tests' started at 2018-05-14 15:02:42.270
Test Suite 'testTests.xctest' started at 2018-05-14 15:02:42.272
Test Suite 'testTests' started at 2018-05-14 15:02:42.273
Test Case '-[testTests.testTests testBMI]' started.
Test Case '-[testTests.testTests testBMI]' passed (0.010 seconds).
Test Case '-[testTests.testTests testHi]' started.
Test Case '-[testTests.testTests testHi]' passed (0.011 seconds).
Test Suite 'testTests' passed at 2018-05-14 15:02:42.298.
     Executed 2 tests, with 0 failures (0 unexpected) in 0.021 (0.025) seconds
Test Suite 'testTests.xctest' passed at 2018-05-14 15:02:42.299.
     Executed 2 tests, with 0 failures (0 unexpected) in 0.021 (0.027) seconds
Test Suite 'All tests' passed at 2018-05-14 15:02:42.302.
     Executed 2 tests, with 0 failures (0 unexpected) in 0.021 (0.032) seconds


Test session log:
    /var/folders/r1/x8tl35cx1mqfhjc2j_3xrmh40000gn/T/com.apple.dt.XCTest/IDETestRunSession-31D0CEE0-D0A5-4341-BCA6-6BD00BCA3D43/testTests-3E0BE5CC-A327-4694-A38A-F00135FE1DEB/Session-testTests-2018-05-14_150213-m3n24r.log

こんな感じでユニットテストを書くことができます

最後に

Swift で Xcode を使ってユニットテストを作成してみました
さすがにユニットテストは他の言語などと同じように書けました
次回は UI のテストを行う方法を紹介したいと思います

参考サイト

2018年5月19日土曜日

Google Vision API で OCR 入門

概要

Google Vision API は OCR が API 経由で使えるサービスです
1000 ユニットまでは無料で使えます
ユニットは画像に含まれる解析対象の情報の単位です (例えば 1 枚の画像から 1 つのテキストと 1 つの顔認識をする場合は 2 ユニット)
今回は API を使うためのセッティングから Ruby で呼び出すところまでやってみました

環境

  • macOS 10.13.4
  • Google Vision API 2018/05/11 時点
  • Ruby 2.4.1p111

API を有効にする

その前に Google Cloud Platform でプロジェクトを作成しプロジェクトの課金を有効にしましょう
詳しくはこちらの手順に従ってください
無料で使うことはできますが無料分があるだけなのでプロジェクトの課金を有効にする必要があります
もしかするとこの手順が一番大変かもしれません (クレカの登録必須)

このページに Cloud Vision API を有効にする ボタンがあるので選択します
まずプロジェクトを選択する画面になるので選択します
google_vision_api1.png

そのまま認証情報を作成する画面になるので進みます
google_vision_api2.png

認証情報を設定する

サービスアカウントを作成します
App Engine or Compute Engine は使わないので「いいえ」を選択する
google_vision_api3.png

アカウント名は好きな名前を設定してください
役割は今回「オーナー」を選択しましたが必要に応じて変更してください
google_vision_api6.png

これで「次へ」を選択すると認証用の JSON がダウンロードされます

ライブラリインストール

  • bundle init
  • vim Gemfile
gem "google-cloud"
  • bundle install --path vendor

テキスト検出するサンプルスクリプト

今回使用する画像はこれを使用します

  • vim sample.rb
project_id = "1234567890"
image_path = "./sample.gif"
key_file = "./project-name-xxxxxxxxxxxx.json"

require "google/cloud/vision"

vision = Google::Cloud::Vision.new(project: project_id, credentials: key_file)
image  = vision.image image_path

puts image.text
  • bundle exec ruby sample.rb

リターンした image オブジェクトは bounds, locale, pages, text, to_h, words が参照できます
words を使えば更に座標などの情報も取得できます

image.text.words.each { |word|
  p word.text
  p word.bounds
}

最後に

Google Vision API を使ってテキスト検出をしてみました
Vision API には他にもラベル検出やロゴ検出、顔検出なども可能です

割りと精度良く取得出来ると思います
ただテキスト解析の場合は文字単位で取得できるのでそれを座標を元に適当な単語につなげるなどの処理は必要です
画像内に必要な文字列が含まれているかどうかくらいの判断であればかなり使えると思います

参考サイト