[Ruby] Backlogの作業時間の記録を半自動化する

前提

以下の環境で開発、動作確認

要素 バージョン
debian 8.6
ruby 2.2.2
gem 2.4.5
bundle 1.13.4
backlog_kit 0.15.0

リポジトリ
https://github.com/Sa2Knight/BacklogTimeKeeper

以下の続きのようなもの
[Ruby] BacklogをCUIで操作してみる

概要

BacklogAPIを用いて、コマンド実行のみで課題の作業時間を記録するCUIツールを作ったので紹介。

以下のように、コマンドで課題キーを指定して課題を開始。課題が完了したら再度コマンドを実行すると、作業時間を計算して、課題の作業時間を更新してくれる。それだけ。

目新しい技術要素もないが、ブログネタが無くなってきたので

10:08:39$ bruby Main.rb -s DEVS-1
プログラムを書く (DEV-1)を開始しました
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13:14:08$ bruby Main.rb -e
プログラムを書く (DEV-1)を終了し、作業時間を3.09時間追加しました

BacklogAPIの準備

BacklogAPIは、特別な申請なしにすぐに利用を開始することができる。BacklogAPIを利用したいBacklogにログインし、個人設定→API設定に移動すると、以下の画面になるので、適当に名前を付けてAPIを発行する。APIキーは後ほど必要になるので控えておく。

ツールの導入

リポジトリからソースコードを取得

GithubからCloneする。

$ git clone git@github.com:Sa2Knight/BacklogTimeKeeper.git
Cloning into 'BacklogTimeKeeper'...
remote: Counting objects: 48, done.
remote: Compressing objects: 100% (33/33), done.
remote: Total 48 (delta 23), reused 37 (delta 12), pack-reused 0
Receiving objects: 100% (48/48), 7.10 KiB | 0 bytes/s, done.
Resolving deltas: 100% (23/23), done.
$ cd BacklogTimeKeeper/

必要なライブラリをインストール

サードパーティ製のbacklog_kitを利用するので、bundleでインストールする。

$ bundle install --path vendor/bundle
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...
Installing concurrent-ruby 1.0.5
Installing i18n 0.8.4
Installing minitest 5.10.2
Installing thread_safe 0.3.6
Installing multipart-post 2.0.0
Using bundler 1.13.4
Installing tzinfo 1.2.3
Installing faraday 0.10.1
Installing activesupport 5.1.1
Installing faraday_middleware 0.10.1
Installing backlog_kit 0.15.0
Bundle complete! 1 Gemfile dependency, 11 gems now installed.
Bundled gems are installed into ./vendor/bundle.

環境変数を設定

本ツールでは、BacklogのスペースID及びAPIキーを環境変数から参照する仕様になっているので、以下のようにエクスポートしておく。エクスポートする値は各々の環境にあわせたものにする。

$ export BACKLOG_SPACE="hoge"
$ export BACKLOGAPI="ghaiogapigipeghpghapghpheapiwthpiat"

使い方

課題の作業を開始する

Main.rbを-sオプションを付与して、対象の課題キーを指定して実行する。bundle execは必須だが、面倒なのでaliasつけたりするとマシになる。

$ bundle exec ruby Main.rb -s DEV-1
プログラムを書く(DEV-1)を開始しました

課題を開始すると、同ディレクトリにworkというファイルが作成されるが、これは対象の課題キーとその開始時間を記録しているだけのJSONファイルなので特に気にしなくて良い

$ cat work
{"key":"DEV-1","datetime":"2017-06-06 20:44:05 +0900"}

課題の作業を終了する

開始から数時間たったことにして、課題を終了する。終了時はMain.rbを-eオプションを付与して実行するだけで良い。前述のworkファイルで対象の課題キーが管理されているので、ここでは課題キーを指定する必要がない。

$ bundle exec ruby Main.rb -e
プログラムを書く(DEV-1)を終了し、作業時間を1.71時間追加しました

課題を確認すると1.71時間設定されているのがわかる

また、既に作業時間が記録されている課題に対して同様に作業開始→作業終了のコマンドを実行すると、以下のように作業時間を追加する形で更新される

使いみち

まだたったコレだけしかできないが、個人的には既に以下のような実用性がある

  • 課題の作業時間を自分で時刻見て計算する必要がなくなる
  • 億劫だった作業時間記録作業がむしろ楽しみになった
  • 本ツールで課題を開始するとその課題にだけしっかりと集中できるようになった気がする(複数課題並行できないのはむしろメリット)

今後

多分以下のように発展されられる気がする

  • 本日の作業時間の集計機能(どの課題にどのぐらい時間を使ったか)
  • 作業中の課題をチャットワークで通知(名前欄に動的に記入したり?)
  • 課題のステータス変更との組み合わせ(課題終了時に、ステータスを「完了」にするとか)

とにかくBacklogAPIとチャットワークAPIのマッシュアップが夢広がる。

チャットワークAPIをRubyで利用する

前提

以下の環境で実装及び動作確認を行った

要素 バージョン
debian 8.6
ruby 2.2.2

概要

チャットワークのAPIを使える状態にし、Rubyからチャットワークの各種操作を行う。今回の成果物についてはGithubから閲覧、ダウンロード可能。

チャットワークとは

ビジネス向けのチャットツールで以下の特徴がある

  • 基本無料
  • ルームを個別に作成し、複数人でチャット可能
  • ルームごとにタスクの管理が可能
  • ファイルの共有が可能
  • 音声通話/ビデオ通話も可能

チャットワークAPIとは

チャットワークの基本的な機能をプログラムから利用するためのRestFullなAPI。
現在(2017/05/20)は、プレビュー版のみの限定公開となっており、APIを利用するためには申請する必要がある

チャットワークAPIの利用準備

チャットワークAPIの利用申請

チャットワークにログインしているブラウザで、利用申請ページにアクセスし、APIの利用申請を行う。利用が可能な状態になると、チャットワークで登録しているメールアドレスに、以下のような通知メールが届くのでそれまで待機(1営業日程度?)

いつもお世話になっております。
チャットワーク サポートデスクです。

チャットワークAPI(プレビュー版)のご利用が可能となりましたので
お知らせいたします。

APIトークンの確認

メールが届いたら、ブラウザでチャットワークを開き、「動作設定」を開くと、以下のようにAPI発行メニューが追加されていることが確認できる。

パスワードを入力することで、以下のようにAPIトークンを確認することができる(画像のトークンはダミー)
トークンはこの画面で更新可能なので、必要に応じて更新する。

APIトークンはプログラムでAPIを利用するのに必須のため、環境変数に保存しておく。以下のコマンドをbashrcに追記するのがベスト

$ export CHATWORKAPI=hogehogehogehogehogehogehogehogehoge

APIの仕様確認

APIドキュメントを確認すれば、APIのシンプルな仕様を確認できるが、ここでは以下さえ抑えておけば問題ない

  • APIのエンドポイントは https://api.chatwork.com/v2
  • リクエストは必ずHTTPSを用いる
  • リクエストヘッダに”X-ChatWorkToken”というキーでAPIトークンを指定する必要がある
  • APIの制限は5分で100回程度
  • レスポンスは必ずJSONで返却される
  • ただし、APIの成否判定の多くはレスポンスヘッダに含まれている

APIの動作確認

さっそくプログラムを作っても良いが、API及びAPIトークンが正しく機能しているかを確認するため、curlコマンドで試しにAPIを呼び出してみる。

$ curl https://api.chatwork.com/v2/me GET -H "X-ChatWorkToken: hogehoge"

上記curlコマンドは、以下のHTTPリクエストを送信する

  • リクエスト先は https://api.chatwork.com/v2/me (自身のステータスを取得するAPI)
  • メソッドにGETを用いる
  • リクエストヘッダに”X-ChatWorkToken: hogehoge”を含める

すると、以下のレスポンスが返却される

{"account_id":2091543,"room_id":59255776,"name":"笹木信吾","chatwork_id":"","organization_id":1118988,"organization_name":"","department":"","title":"","url":"https://github.com/Sa2Knight","introduction":"","mail":"sasaki@91932.com","tel_organization":"","tel_extension":"","tel_mobile":"090-7067-7376","skype":"","facebook":"","twitter":"","avatar_image_url":"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}

レスポンスを見る限り、無事にチャットワークAPIを用いて、自身のユーザ情報を取得できていることがわかる。

RubyからチャットワークAPIを利用する

Chatworkクラスの作成

とりあえずチャットワークAPIをRubyで叩くためのエンドポイントとなるChatworkクラスを作成する。

require 'net/http'
require 'uri'
require 'json'
require 'date'

class Chatwork
  @@API_BASE = 'https://api.chatwork.com/v2'

  # tokenを指定してオブジェクトを生成
  # tokenを省略した場合、環境変数を参照する
  def initialize(token = nil)
    @token = token || ENV['CHATWORKAPI']
  end

end

例によって、APIトークンを指定してオブジェクトを生成する。トークンの指定を省略した場合に、環境変数”CHATWORKAPI”を参照するようにしてあるので、今回はそれを前提にする。APIのエンドポイントは全てのAPIで共通なので、クラス定数@@API_BASEで定義しておく。

HTTP通信するメソッドの実装

どのAPIでも共通となる、HTTPリクエストを投げてレスポンスを戻すメソッドを作成する。

createRequestObjectは、そのヘルパーメソッドで、指定したHTTPメソッド(:get,:post,:put,:delete)に応じた、空のHTTPRequestオブジェクトを返却する。

createHttpObject、対象のURL,HTTPメソッド,パラメータを指定すると、以下のことをしてくれる。
1. APIのエンドポイントと各APIのURLを結合し、URIオブジェクトを生成する
2. URIを元にHTTPオブジェクトを生成し、SSLを有効にする
3. (メソッドがGETの場合) URIにパラメータを付与する
4. メソッドに応じたRequestオブエクトを取得
5. リクエストヘッダにAPIトークンを付与
6. (メソッドがGET以外の場合) リクエストボディにパラメータを付与する
7. リクエストを送信し、レスポンスを戻す

## HTTPリクエストを送信する
def createHttpObject(url, method, params = {})
  api_uri = URI.parse(@@API_BASE + url)
  https = Net::HTTP.new(api_uri.host, api_uri.port)
  https.use_ssl = true
  api_uri.query = URI.encode_www_form(params) if method == :get
  req = createRequestObject(method, api_uri)
  req["X-ChatWorkToken"] = @token
  req.set_form_data(params) unless method == :get
  https.request(req)
end
## リクエストオブジェクトを生成する
def createRequestObject(method, uri)
  case method
    when :get
      return Net::HTTP::Get.new(uri.request_uri)
    when :post
      return Net::HTTP::Post.new(uri.request_uri)
    when :put
      return Net::HTTP::Put.new(uri.request_uri)
    when :delete
      return Net::HTTP::Delete.new(uri.request_uri)
  end
end

上記のメソッドを用いて、各APIに対応したメソッドを実装する。

各APIごとのメソッドを実装する

あとはAPIドキュメントを参考に、各種APIを呼び出すためのメソッドを順に実装する。全て実装するのは疲れるので、とりあえず以下を実装した。残りもおいおい実装する予定

メソッド名 内容
me 自身のユーザ情報を取得
myStatus 自身の未読数、未読To数、未完了タスク数を取得する
myTasks 自身のタスク一覧を取得する
myContact 自身のコンタクト一覧を取得する
myRooms 自信が参加しているチャット一覧を取得する
getRoom チャットの情報を取得する
createRoom 新しいチャットを作成する
updateRoom チャットの情報を更新する
getRoomMembers チャットに参加しているユーザ一覧を取得する
getRoomMessages チャットのメッセージ一覧を取得する
sendMessage チャットに新規メッセージを投稿する
getRoomTask チャット内のタスク一覧を取得する
getTask タスク情報を取得する
createTask タスクを新規登録する
getRoomFiles アップロードされたファイル一覧を取得する
getFile アップロードされたファイルの情報を取得する

※ 何故か既存のタスクを操作するためのAPIが無い。タスクを作ることは出来るのに完了したり編集したりすることがでない謎。

実装例

どのメソッドも基本的に実装内容は同じ(URL、メソッド、パラメータの有無などが異なるだけ)なので、全てを記載しないが、ここでは以下の2種類について記載する。

myStatus(自身の未読数、未読To数、未完了タスク数を取得する)

myStatusは特定のURLに対して、パラメータなしでシンプルにGETするだけなので、以下のように実装した

def myStatus
  url = '/my/status'
  res = createHttpObject(url, :get)
  return JSON.parse(res.body)
end

createTask(タスクを新規登録する)

createTaskは少しだけ複雑で、タスクを新規登録するルーム(チャット)IDとタスクの内容、担当者のユーザID一覧を指定して登録する。また、オプションでタスクの期限を指定することができる。

APIが要求するパラメータでは、担当者のユーザID一覧はカンマ区切りで、期限はUNIXTIMEで指定する必要がある。これはそのままだと使いづらいので、メソッド側では担当者のユーザID一覧を配列で、期限をDateTimeオブジェクトで受け取るようにしている。メソッド内でそれをAPI用に変換して実行している。

def createTask(room_id, body, to_ids = [], params = {})
  url = '/rooms/' + room_id + '/tasks'
  params[:body] = body
  params[:to_ids] = to_ids.join(',')
  params[:limit] = params[:limit].to_i if params[:limit].class == Time
  res = createHttpObject(url, :post, params)
  return res.body ? JSON.parse(res.body) : []
end

動作確認

本項では、以下の手順で動作確認する

  1. 動作確認用の新しいチャットルームを作成する(自分のみ参加)
  2. チャット内で適当に発言する
  3. チャット内にタスクを追加する
  4. チャットルームの情報を取得する
  5. チャットルームの発言一覧を取得する
  6. チャットルームのタスク一覧を取得する

なお、動作確認中に使用するユーザのIDを’2091543′,及び作成するチャットルームのIDを’76097933’とし、以下のようにChatworkオブジェクトを生成済みとする。

cw = Chatwork.new

チャットルームを作成

動作確認用に、「API動作確認ルーム」というチャットルームを作成する。参加者は、自身のみを管理者ユーザとして設定する。

cw.createRoom('API動作確認ルーム', ['2091543'], :description => '動作確認用のルームです')

実行すると、以下のように新規チャットルームが作成されたことがわかる。

チャット内で適当に発言する

「こんにちは,APIからの投稿です」という発言を、先ほど作成したルームに対して送信する

cw.sendMessage('76097933', 'こんにちは、APIからの投稿です')

実行すると、発言が送信されていることがわかる

チャット内にタスクを追加する

「WordPressを更新する」というタスクを、本日(5/21)を締め切りにして登録する

cw.createTask('76097933', 'WordPressを更新する', ['2091543'], :limit => Time.now)

実行すると、タスクが登録されたことが確認できる

チャットの情報を取得する

先程作成したチャットルームの情報を取得する。

cw.getRoom('76097933')

実行すると、チャットルームの基本的な情報を確認することができる

{"room_id"=>76097933, "name"=>"API動作確認ルーム", "type"=>"group", "role"=>"admin", "sticky"=>false, "unread_num"=>0, "mention_num"=>0, "mytask_num"=>1, "message_num"=>3, "file_num"=>0, "task_num"=>1, "icon_path"=>"https://appdata.chatwork.com/icon/ico_group.png", "description"=>"動作確認用のルームです", "last_update_time"=>1495307674}

チャットルーム内のメッセージ一覧を取得する

メッセージ一覧(と言っても先程の1件のみだが)を取得する

cw.getRoomMessages('76097933')

メッセージ一覧及び個々のメッセージの発言者の情報が取得できることがわかる

[{"message_id"=>"2020162066", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"[info][title][dtext:chatroom_groupchat_created][/title][dtext:chatroom_chatname_is]API動作確認ルーム[dtext:chatroom_set]\n\n[dtext:chatroom_description_is]動作確認用のルームです[dtext:chatroom_set]\n\n[dtext:chatroom_member_is][piconname:2091543][dtext:chatroom_added][/info]", "send_time"=>1495307261, "update_time"=>0}, {"message_id"=>"2020162308", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"こんにちは、APIからの投稿です", "send_time"=>1495307439, "update_time"=>0}, {"message_id"=>"2020162647", "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "body"=>"[info][title][dtext:task_added][/title][task aid=2091543 st=open lt=1495378799]WordPressを更新する[/task][/info]", "send_time"=>1495307674, "update_time"=>0}]

チャットルーム内のタスク一覧を取得

こちらも先程登録した1件のみだが、タスクの情報を取得する

p cw.getRoomTasks('76097933')

実行すると、タスク及びそのメタ情報が確認できる

[{"task_id"=>68592616, "account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "assigned_by_account"=>{"account_id"=>2091543, "name"=>"笹木 信吾", "avatar_image_url"=>"https://appdata.chatwork.com/avatar/1623/1623307.rsz.png"}, "message_id"=>"2020162647", "body"=>"WordPressを更新する", "limit_time"=>1495378799, "status"=>"open"}]

所感

  • チャットワークのAPIはそれは見事なRestFull設計で感動を覚えるほど使いやすかった
  • だがまだプレビュー版で、既存タスクの更新ができないなど、痒いところに手が届かないので正式リリースが待たれる
  • APIを叩くライブラリを作ったはいいが、問題はこれをどう活用するか。そこは全然考えてない。おそらくサーバを監視して報告をチャットワークに流すとか、プロジェクト管理ツールの更新をチャットワークでも通知するとか、読み込みより書き込みがメインになるとは思う

参考

[Ruby] codicAPIを使って英語での命名で楽する

前提

以下のシステム構成で開発、動作確認済み

要素 バージョン
debian 8.6
ruby 2.2.2

成果物

今回開発したツールは以下のリポジトリから取得可能。導入方法はReadme参照
https://github.com/Sa2Knight/codic-ruby

概要

コーディングに特化した翻訳サービスであるcodicのAPIを用いて、Rubyスクリプトから翻訳を行う。翻訳作業をコマンド化し、手軽に利用できるようにする。

codicには各種エディタから利用できるプラグインなどが既にいくつか存在するが、個人的にどれもしっくり来なかったり、機能過剰だったりしたので、自分でAPIを利用して作ってみることにする。

codicについて

codicとは

codicは、プログラマー・システムエンジニアのためのネーミングツール。私含め英語弱者なプログラマは、クラス名、関数名、変数名などの識別子の命名に苦労することが多々ある。

codicはそれをサポートするツールで、日本語で識別子の概要を入力すると、それなりに適した英語に翻訳してくれる。

一見すると、Google翻訳などの機械翻訳と変わらないが、codicはコーディングに特化した翻訳を行い、コード内で頻出する単語に特に力を入れていたり、キャメルケースやスネークケースなどのフォーマットを指定することもできるなど、まさにプログラマ向けの翻訳ツールである

codic APIとは

名の通り、codicのサービスをプログラムから実行するためのAPIである。ユーザ登録こそ必要なものの、OAuth認証などの手続きは必要なく、ユーザ登録時に取得できるアクセストークンをリクエストヘッダに付与するだけで手軽に利用できる。本記事では、日本語を投げると英語が返ってくるEngineAPIのみ利用する。

Engine APIとは

Engine APIは、codicのネーミング生成エンジンにアクセスし、ネーミング変換(和英変換)を行う。

シンプルなリクエストとレスポンスは以下の通り

request(日本語はURLエンコードするものとする)

https://api.codic.jp/v1/engine/translate.json?text=得点を取得する&casing=camel

response

[{"successful"=>true,
  "text"=>"得点を取得する",
  "translated_text"=>"getScore",
  "words"=>
   [{"successful"=>true,
     "text"=>"取得する",
     "translated_text"=>"get",
     "candidates"=>
      [{"text"=>"get"},
       {"text"=>"retrieve"},
       {"text"=>"fetch"},
       {"text"=>"obtain"},
       {"text"=>"acquire"},
       {"text"=>"getting"}]},
    {"successful"=>true,
     "text"=>"得点",
     "translated_text"=>"score",
     "candidates"=>[{"text"=>"score"}, {"text"=>"point"}]},
    {"successful"=>true,
     "text"=>"を",
     "translated_text"=>nil,
     "candidates"=>
      [{"text"=>nil},
       {"text"=>"that"},
       {"text"=>"to"},
       {"text"=>"for"},
       {"text"=>"from"},
       {"text"=>"is"},
       {"text"=>"of"}]}]}]

以上のように、エンジンAPIはGETパラメータで翻訳対象の日本語文字列(text)と、出力フォーマット(casing)を指定するだけで利用できる。

レスポンスは、全体の変換結果及び単語ごとの変換結果、さらに他の変換候補なども返却してくれるが、本記事では単純な変換結果のみ欲しいので、

“translated_text”=>”getScore”

のみ抜き出して利用する。

codic APIの利用準備

codic APIを利用するためには、codic APIのアクセストークンを取得する必要がある。以下の手順で簡単に取得できるので詳細は割愛
1. codicにユーザ登録するする
2. codicにログインし、アカウント設定ページに移動
3. APIをクリックし、以下の画面よりアクセストークンを確認(画像はダミー)

Rubyスクリプトからアクセストークンを利用する必要があるので、実行環境の環境変数にアクセストークンをエクスポートしておく

$ export CODICAPI hogehogehogehgoehgehgheogheohgehog

エクスポートを毎回するのが面倒なので、上記コードをbashrcに追加しておくことをオススメする

Rubyスクリプトの実装

今回は実装内容もシンプルで、OAuthする必要もないため、gemライブラリを利用せず、Rubyの標準モジュールのみで実装を行う。

Codicクラスを作成

スクリプトのベースとなるCodicクラスを以下のように作成する

require 'net/http'
require 'uri'
require 'json'

class Codic
  @@API_URL = "https://api.codic.jp/v1/engine/translate.json"

  def initialize(api_key = nil)
    @api_key = api_key || ENV['CODICAPI']
  end
end
  • APIURLをクラス定数に持たせる
  • アクセストークンを指定してインスタンスを生成する。指定がない場合環境変数を参照する

HTTPリクエストを送信するメソッドを実装

# HTTPリクエストを送信し、レスポンスJSONを戻す
def request(url, params = {})
  uri = URI.parse(appendQueryString(url, params))
  https = Net::HTTP.new(uri.host, uri.port)
  https.use_ssl = true
  req = Net::HTTP::Get.new(uri.request_uri)
  req["Authorization"] = "Bearer #{@api_key}"
  res = https.request(req)
  JSON.parse(res.body)
end
# URLにクエリストリングを付与する
def appendQueryString(url, params = {})
  query_string = params.map {|key, val| "#{key}=#{URI.escape(val)}"}.join("&")
  url += "?#{query_string}" if query_string
  return url
end

requestメソッド

  • Ruby標準モジュールのnet/httpを用いて、HTTPリクエストの作成、送信を行う
  • codicAPIでは、Authorizationヘッダにアクセスキーを付与する必要があるので、インスタンス変数の@api_keyを設定
  • text及びcasingをURLに付与する必要があるので、それはappendQueryStringメソッドに移譲

appendQueryStringメソッド

  • ハッシュ形式のパラメータを、文字列に置き換えてURLに付与する
  • textは日本語が飛んで来るので、URLエンコードを行う

※ どちらもツールの目的から独立したメソッドなのでこのような実装は望ましくない気がするが、軽量ツールなので妥協する

casingをAPI用に変換するメソッドを実装

casingに指定する文字列と出力フォーマットの対応は以下の通り

casing フォーマット 例(データを取得する)
camel キャメルケース getData
pascal パスカルケース GetData
lower underscore 小文字のスネークケース get_data
upper underscore 大文字のスネークケース GET_DATA
hyphen ハイフン刻み get-data
(指定なし) スペース刻み get data

といっても文字列を直接指定するのは面倒なので、アルファベット1文字でそれぞれに変換できる以下のメソッドを実装した

# casingをAPI用に変換する
def convertCasing(casing)
  casingList = {
    'c' => 'camel',
    'p' => 'pascal',
    'l' => 'lower underscore',
    'u' => 'upper underscore',
    'h' => 'hyphen',
  }
  new_casing = casingList[casing]
  new_casing ? new_casing : ''
end
  • c/p/l/u/hをそれぞれの文字列に変換する
  • 該当が無い場合、空文字(指定なし)を返却

翻訳を実行するメソッドを実装

ここまで実装したメソッドを利用して、APIを通して翻訳結果を出力する以下のメソッドを実装した

def translate(text, casing = '')
  casing = convertCasing(casing)
  result = request(@@API_URL, {:text => text, :casing => casing})
  result[0]["translated_text"] if result.class == Array && result[0].include?("translated_text")
end
  • responseは色々返ってくるが、その一部のみしか利用しないのでそこだけ抜き出して返却

エントリポイントの実装

コマンドライン引数を用いてCodicクラスのtranslateメソッドを実行するエントリポイントを実装

codic = Codic.new
word = ARGV[0] || exit
casing = ARGV[1] || ''
puts codic.translate(word, casing)

動作確認

$ ruby codic.rb データを取得
acquisition data
$ ruby codic.rb データを設定 c
settingData
$ ruby codic.rb データを削除 h
deleting-data

シェルスクリプトの実装

これでRubyコマンドを用いた簡易的な翻訳ツールを作成することができた。蛇足かもしれないが、より汎用的に使えるように、このツール自体をコマンド化したい。そこで、シェルスクリプトでcodicコマンドを作成し、それ経由でcodic.rbを呼び出すようにする。

シェルスクリプトの仕様

  • 第一引数に変換する日本語文字列を指定する
  • 第二引数で以下のcasingを指定する(省略可)
    • -c(デフォルト)
    • -p
    • -l
    • -u
    • -h
    • -n
  • Rubyスクリプトを実行し、結果を標準出力

スクリプトパスの定義

先程作成したスクリプトのパスを定義しておく

#!/bin/bash
SCRIPTPATH="/home/vagrant/codicRuby/codic.rb"

ヘルプ関数を作成

よくあるヘルプ関数を以下のように実装する。本関数はコマンド引数が存在しなかった場合に表示する

usage() {
  echo "Codic APIに基いて日本語の翻訳を行う"
  echo "  Usage: codic text [-c] [-p] [-l] [-u] [-h] [-n]"
  echo "オプション:"
  echo "  -c キャメルケースで取得(getData)"
  echo "  -p パスカルケースで取得(GetData)"
  echo "  -l 小文字のスネークケースで取得(get_data)"
  echo "  -u 大文字のスネークケースで取得(GET_DATA)"
  echo "  -h ハイフン刻みで取得(get-data)"
  echo "  -n スペース刻みで取得(get data)"
  exit 1
}

第一引数をチェック

第一引数(翻訳文字列)の存在を確認。存在しない場合前項のヘルプ関数を実行してスクリプトを終了

if [ -n "$1" ]; then
  text=$1
  shift 1
else
  usage
  exit 1
fi

第二引数をチェック

第二引数をチェックし、引数に応じてcasingを設定する

casing="c" #デフォルト設定をここに記述
while getopts cpluhn: OPT
do
  case $OPT in
  c)  casing="c"
      ;;
  p)  casing="p"
      ;;
  l)  casing="l"
      ;;
  u)  casing="u"
      ;;
  h)  casing="h"
      ;;
  n)  casing="n"
      ;;
  esac
done

Rubyスクリプトを実行する

text及びcasingを用いて、Rubyスクリプトを実行する

command="$SCRIPTPATH $text $casing"
ruby $command

コマンドをパスに含める

パスを確認

$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games

今回は/usr/local/binにハードリンクを作成する

sudo ln codic /usr/local/bin

パスが通っていることを確認

$ which codic
/usr/local/bin/codic

動作確認

text指定なし

$ codic
Codic APIに基いて日本語の翻訳を行う
  Usage: codic text [-c] [-p] [-l] [-u] [-h] [-n]
オプション:
  -c キャメルケースで取得(getData)
  -p パスカルケースで取得(GetData)
  -l 小文字のスネークケースで取得(get_data)
  -u 大文字のスネークケースで取得(GET_DATA)
  -h ハイフン刻みで取得(get-data)
  -n スペース刻みで取得(get data)
$

casing指定なし

$ codic データを取得する
getData

casing指定あり

$ codic データを設定する -u
SET_DATA

所感

  • 今までで一番手軽に利用できるAPIだったので使っていて楽しかった
  • 英語弱者なので命名が苦手だったので使えそう(あくまで参考程度に)
  • Vim上で効率的に利用したいので、これを元にvimプラグインを作ってみたい

Railsでサクッと電子掲示板を作成する

前提

以下の環境で開発、動作確認を行う

要素 バージョン
Ubuntu 16.04.2
Ruby 2.4.1
Ruby on Rails 5.1.0
mySQL 14.14

また、上記環境はDockerで構築したコンテナで、環境構築についてはコチラを参照

概要

本記事では、1.前提 の環境にて、簡易的な電子掲示板サービスの開発を行う。電子掲示板サービスの概要は以下の通り。
なお、本記事ではRuby on Railsに関する基本的な説明は割愛している。railsの経験がない人が雰囲気で何か掴めれば程度の内容。

電子掲示板の概念

  • 電子掲示板には0個以上のトピックが存在する
  • トピックにはタイトルが存在する
  • トピックには0個以上の書き込みが存在し、全ての書き込みは一つのトピックに属する
  • 書き込みには投稿者名と本文が存在する

画面イメージ

  • ユーザはトップページにアクセスすると、トピック一覧を確認することができる
  • トピック一覧画面の末尾には、新規トピック投稿フォームが存在し、新規投稿を行うことができる
  • 各トピックごとに、「削除」リンクが付与されており、それをクリックするとトピックを削除することができる
  • トピック削除時に、トピックに紐付いた書き込みもあわせて削除する
  • ユーザはトピック名をクリックすることで、そのトピックの書き込み一覧を確認することができる
  • トピックの書き込み一覧画面の末尾には、新規投稿のフォームが存在し、新規投稿を行うことができる

データベーススキーマ

電子掲示板サービスのスキーマは以下の通り

実装

実装(準備編)

rails newコマンドで、railsアプリケーションの雛形を作成することができる。ここでは、使用するデータベースをmysqlに指定して雛形を作成している(デフォルトではsqlite)

$ rails new mybbs -d mysql
(省略)
$ cd mybbs

作成したrailsアプリケーションの雛形を元に、データベースを作成する。アプリケーション名(mybbs)を元に、development用とtest用の2つのデータベースが作成される。本記事ではテストを対象としないので、mybbs_developmentデータベースを基本的に使用する。

$ rake db:create
Created database 'mybbs_development'
Created database 'mybbs_test'

実装(トピック編)

本項では、複数の書き込みを束ねる「トピック」に関する実装を行う

トピックモデルを作成

ここでは、rails generateコマンドを用いて、topicモデルを作成する。
topicモデルに対応づいたtopicsテーブルに必要な属性は以下の通り。
* id
* title
* created_at
* updated_at
id,created_at,updated_atは、自動生成してくれるため、ここで明示的に指定する必要があるのはtitleだけなので、以下のようなコマンドでtopicモデルを作成する。

$ rails generate model topic title:string
Running via Spring preloader in process 1142
      invoke  active_record
      create    db/migrate/20170505141519_create_topics.rb
      create    app/models/topic.rb
      invoke    test_unit
      create      test/models/topic_test.rb
      create      test/fixtures/topics.yml

すると、以下のマイグレーションファイル(テーブルを作成するスクリプト)が生成されるので

class CreateTopics < ActiveRecord::Migration[5.1]
  def change
    create_table :topics do |t|
      t.string :title
      t.timestamps
    end
  end
end

以下のコマンドでマイグレーション(テーブル作成)実行

$ rake db:migrate
== 20170505141519 CreateTopics: migrating =====================================
-- create_table(:topics)
   -> 0.1048s
== 20170505141519 CreateTopics: migrated (0.1050s) ============================

データベース内にtopicsテーブルが作成されていることを確認

mysql> show columns from topics;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| title      | varchar(255) | YES  |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

動作確認用Topicsレコードの作成

Railsでは、console機能を用いてCLI上でRailsアプリケーションに対するスクリプトを行うことができる。ここでは作成したTopicsモデルを用いて、console上でテストデータを登録する

$ rails console
Running via Spring preloader in process 1312
Loading development environment (Rails 5.1.0)
irb(main):001:0> Topic.create(:title => 'プロ野球について')
=> #<Topic id: 2, title: "プロ野球について", created_at: "2017-05-05 14:27:31", updated_at: "2017-05-05 14:27:31">
irb(main):002:0> Topic.create(:title => 'ラーメンについて')
=> #<Topic id: 3, title: "ラーメンについて", created_at: "2017-05-05 14:27:49", updated_at: "2017-05-05 14:27:49">
irb(main):003:0> Topic.create(:title => 'プログラミングについて')
=> #<Topic id: 4, title: "プログラミングについて", created_at: "2017-05-05 14:27:54", updated_at: "2017-05-05 14:27:54">

テーブルにレコードが追加されていることを確認

mysql> select * from topics;
+----+-----------------------------------+---------------------+---------------------+
| id | title                             | created_at          | updated_at          |
+----+-----------------------------------+---------------------+---------------------+
|  2 | プロ野球について                  | 2017-05-05 14:27:31 | 2017-05-05 14:27:31 |
|  3 | ラーメンについて                  | 2017-05-05 14:27:49 | 2017-05-05 14:27:49 |
|  4 | プログラミングについて            | 2017-05-05 14:27:54 | 2017-05-05 14:27:54 |
+----+-----------------------------------+---------------------+---------------------+
3 rows in set (0.00 sec)

トピックのコントローラとビューを作成

同様にトピック用のビューとコントローラを作成する。トピックを一覧するページ(index)と、トピックの詳細を表示するページ(show)の2つが欲しいので、以下のコマンドを実行する。

$ rails generate controller topics index show
(省略)

上記コマンドを実行することで、トピック用のコントローラ/ビュー及びルーティングの設定が作成される。

自動で生成されたルーティングだけだと少し足りないので、db/routes.rbに以下のように記述する。

Rails.application.routes.draw do
  get 'topics/index'
  get 'topics/show/:id' => 'topics#show', as: :topics_show
end

上記の例だと、’topics/show/:id’へのアクセスは、tpicsコントローラのshowメソッドが受け取る。このルーティングに対して、tpics_showという識別子を付与することで、プログラムからルーティングを特定することができる。

トピック一覧を画面に表示する機能を実装

実装の第一段階として、”/topics/index”アクセス時に、データベースに登録されているトピックの一覧を画面上に表示する。

コントローラの設定

/topics/indexアクセスは、topics_controllerのindexメソッドで受け取るので、そこでトピックの一覧をDBから取得し、ビューに受け渡す。
Tpic.allは、topicsテーブルの全ての行を取得する。

class TopicsController < ApplicationController
  def index
    @topics = Topic.all
  end
end

ビューの設定

railsでは、標準でテンプレートエンジンとしてERBを採用している。そのため、以下のようにHTML内でRubyスクリプトを挿入することができるので、コントローラから受け取った@topicsの数だけ、トピックのタイトルを表示している。

link_toは、railsのヘルパーメソッドで、aタグを生成してくれる。

<h1>トピック一覧</h1>
<ul>
  <% @topics.each do |topic| %>
    <li><%= link_to topic.title, topics_show_path(topic.id) %></li>
  <% end %>
</ul>

動作確認

以上より、/topics/indexにアクセスした際の画面が以下の通り。動作確認用に登録したトピックの一覧が画面に表示されていることが確認できる。

トピックを新規登録する機能を実装

本項では、トピックの新規登録機能を実装する。新規登録用のフォームはトピック一覧画面の一番下に設置し、そこからタイトルを入力して登録できるようにする。

ルーティングの追加

新規登録用フォームから、’tpics/create’に対してPOSTすることで新規登録を実行できるようにしたいので、ルーティングに以下を追加する

post 'topics/create' => 'topics#create'

コントローラの設定

新規登録用フォームは、トピック一覧ページに表示するため、topicコントローラのindexメソッドにて、@newTopicを生成する。これは、後述するform_forメソッドで必要になるので、ここでは割愛する。

def index
  @topics = Topic.all
  @newTopic = Topic.new
end

また、createメソッドは、新規登録用フォームからPOSTされたデータを受け取るので、ここでPOSTデータを元に新たにtopicsレコードを生成する。生成後は、トピック一覧画面にリダイレクトする。

def create
  @topic = Topic.new(params[:topic].permit(:title))
  @topic.save
  redirect_to topics_index_path
end

ビューの設定

ビューでは、railsのヘルパーメソッドである、form_forメソッドを用いてフォームを自動生成する。
form_forでは、formが登録する対象のモデル(ここでは@newTopic)を指定することで、良い感じのフォームを作成してくれる。

<h1>トピック新規登録</h1>
<%= form_for @newTopic, :url => {:action => 'create'} do |f| %>
  <%= f.text_field :title %>
  <%= f.submit %>
<% end %>

動作確認

以上より、トピックの新規登録ができるようになった

トピックを削除する機能を実装

トピックの削除は、トピック一覧のトピック名の横に「削除」というリンクを作成し、それをクリックすると”topics/delete/:id”にDELETEリクエストを送信するようにする。

ルーティングの追加

まず、topics/delete/:idにdeleteリクエスト送信時に、topicsコントローラのdeleteメソッドが受け取るようにルーティングを追加する

delete 'topics/delete/:id' => 'topics#delete', as: :topic_delete

コントローラの設定

topicsコントローラでは、削除リクエストを受け取った際に、該当するトピックを削除する。削除後、トピック一覧画面にリダイレクトする
def delete
  @topic = Topic.find(params[:id])
  @topic.destroy
  redirect_to topics_index_path
end

ビューの設定

ビューには、トピックごとに以下の削除リンクが表示されるように追記する。confirmは、リンククリック時に通知されるconfirmで、「OK」をクリックした場合にのみリンクが実行される。

<%= link_to '[削除]', topic_delete_path(topic.id), method: :delete, data: {confirm: "削除してもよろしいですか?"} %>

動作確認

以下のように、「削除」リンクをクリックすると、確認ダイアログが表示され、「OK」をクリックするとトピックが削除されたことが確認できる

実装(書き込み編)

本項では、トピックごとに0件以上存在する「書き込み(post)」について実装を行う。トピック(topic)の実装と重複する内容が多いので、サクッと流していく

書き込みモデルの作成

まずはトピック同様に、postのモデルを作成する。postは、投稿者名(name)と、本文(body)と、トピック番号(topic_id)が必要になる。以下のtopic:referencesのように指定すると、topicテーブルに外部キー制約を持つtpic_id列が生成される。

$ rails generate model Post name body:text topic:references
Running via Spring preloader in process 2470
      invoke  active_record
      create    db/migrate/20170505154158_create_posts.rb
      create    app/models/post.rb
      invoke    test_unit
      create      test/models/post_test.rb
      create      test/fixtures/posts.yml

マイグレーション実行

$ rake db:migrate
== 20170505154158 CreatePosts: migrating ======================================
-- create_table(:posts)
   -> 0.1274s
== 20170505154158 CreatePosts: migrated (0.1277s) =============================

postsテーブルが作成されたことが確認できる

mysql> show columns from posts;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name       | varchar(255) | YES  |     | NULL    |                |
| body       | text         | YES  |     | NULL    |                |
| topic_id   | bigint(20)   | YES  | MUL | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

動作確認用のPostsレコードを作成

トピックと同様に動作確認用のPostsレコードを、console上で作成する。内容が重複しているので結果のみ以下に示す

mysql> select * from posts;
+----+-----------+--------------------------------------------------------------+----------+---------------------+---------------------+
| id | name      | body                                                         | topic_id | created_at          | updated_at          |
+----+-----------+--------------------------------------------------------------+----------+---------------------+---------------------+
|  1 | ササキ    | プロ野球について一緒に語りましょう!                          |        2 | 2017-05-05 15:49:29 | 2017-05-05 15:49:29 |
|  2 | サトウ    | オススメのラーメン屋など教え合いましょう                     |        3 | 2017-05-05 15:49:52 | 2017-05-05 15:49:52 |
|  3 | ヤマダ    | 皆さんの得意な言語はなんですか?                             |        4 | 2017-05-05 15:50:11 | 2017-05-05 15:50:11 |
+----+-----------+--------------------------------------------------------------+----------+---------------------+---------------------+
3 rows in set (0.00 sec)

書き込みコントローラ/ビューの作成

$ rails generate controller Post
Running via Spring preloader in process 2508
      create  app/controllers/post_controller.rb
      invoke  erb
      create    app/views/post
      invoke  test_unit
      create    test/controllers/post_controller_test.rb
      invoke  helper
      create    app/helpers/post_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/post.coffee
      invoke    scss
      create      app/assets/stylesheets/post.scss

書き込み一覧を表示する機能を実装

“/topics/show/:id” にアクセスすることで、該当のトピックに関する書き込み一覧を表示させる

コントローラの設定

“/topics/show/:id”は、topicコントローラのshowメソッドにルーティンぎしているので、showメソッドにて、以下のようにTopicモデルと対応する書き込み一覧を取得してビューに引き渡す

def show
  @topic = Topic.find(params[:id])
  @posts = Post.where(topic_id: params[:id])
end

ビューの設定

ビューでは、トピックのタイトルと合わせて、コントローラから受け取った書き込み一覧を表示する

<h1><%= @topic.title %></h1>
<% @posts.each_with_index do |post , idx| %>
  <p>
    <%= idx + 1 %>. <%= post.name %> : <%= post.body %>
  </p>
<% end %>

動作確認

以下のように、書き込み一覧が表示されることが確認できる

新規書き込みを登録する機能を実装

新規書き込みの登録は、書き込み一覧ページの一番下に設置する新規投稿フォームを用いて登録する

ルーティングの追加

新規書き込み登録のリクエストは、’posts/create’にPOSTし、postコントローラのcreateメソッドが受け取るようにしたいので、以下のようにルーティングを追加する

post 'posts/create' => 'post#create', as: :post_create

コントローラの設定

まず、トピックの新規登録と同様に、書き込み一覧画面でform_forメソッドを使うために、topicコントローラのshowメソッドにて、新しいPostモデルを生成しておく

def show
  @topic = Topic.find(params[:id])
  @newpost = Post.new(:topic_id => params[:id]) #ここを追加
  @posts = Post.where(topic_id: params[:id])
end

postコントローラのcreateメソッドには、新規書き込みの内容がPOSTされるので、postsレコードを生成して書き込み一覧画面にリダイレクトする

class PostController < ApplicationController
  def create
    @post = Post.new(params[:post].permit(:topic_id, :name, :body))
    @post.save
    redirect_to topics_show_path(params[:post]['topic_id'])
  end
end

ビューの設定

書き込み一覧画面のビューに、以下を追加する。トピックの追加と同様にform_forメソッドを用いてformを自動生成する。

<h3>新規書き込み</h3>
<%= form_for @newpost, :url => post_create_path do |f| %>
  <p>お名前</p>
  <p><%= f.text_field :name %></p>
  <p>本文</p>
  <p><%= f.text_area :body %></p>
  <%= f.hidden_field :topic_id %>
  <%= f.submit %>
<% end %>

動作確認

以下のように、新規書き込み登録フォームを用いて書き込みを追加できるようになったことが確認できる

まとめ

以下のような簡易的な電子掲示板ができあがった

所感

  • 初めてRailsで1からアプリケーションを作ったが、想像以上に何でもフレームワーク側でやってくれて驚き
  • 元々触れたことがあったLaravelと似ている部分が多かったので、適応しやすかった
  • テーブル間の関連付けはもっとフレームワークの機能で出来た気がする(トピック削除時に自動で対応する書き込みを削除したり、トピック取得時に自動で書き込み一覧を取得したり)
  • 命名などの制約が非常に厳しいが、それに従うことで色々自動化出来る上、定まったルールの元で開発できるのが良い
  • ネット上に情報が膨大にあるので、他のフレームワークよりも遥かにググりやすい

その他

本記事で作成したRailsアプリケーションは、以下のリポジトリで公開している。記事内で作成したコードから微妙に改良したりしているので、記事と内容が合致しない部分もある。
https://github.com/Sa2Knight/rails-bbs

参考

DockerでRailsの開発環境を構築する

前提

以下の環境でDockerを使用する。またDockerHubのアカウントについては既に準備しているものとする。

要素 バージョン
debian 8.6
docker 1.13.0

概要

Mac上に立ち上げたDebian内でDockerを用いてRuby on railsの開発環境を構築する。railsについても学習用の環境にすぎないので、細かいバージョンなどはあまり気にしない方針。

構築にあたっては、rails環境用のDockerイメージをDockerHubから探すだけでも済むが、今回はDocker及び環境構築の学習を含めて以下の手順で構築する。

  1. DockerHubからUbuntuのベースイメージをpull
  2. コンテナを作成し、コンテナ内で環境構築(=Dockerfileは使わない)
  3. コンテナをコミット
  4. 作成したイメージをDockerHubにpush

元となるDockerイメージを取得

今回は使い慣れているUbuntuを用いてrails環境を構築するので、DockerHubからubuntuのイメージをpullする。

$ docker pull ubuntu

イメージが手に入ったら、コンテナを生成してアクセスする。以降の項ではコンテナ内で作業を行う。後々動作確認でコンテナにWebブラウザでアクセスするので、ポートフォワーディングの設定(8081→3000)も指定しておく

$ docker run -it -p 8081:3000 -name my_ubuntu ubuntu

Railsの環境構築

開発環境の前準備

まずはapt-getを更新

$ apt-get update
$ apt-get upgrade

日本語が使えない状態だったので日本語化作業

$ apt-get install language-pack-ja
$ update-locale LANG=ja_JP.UTF-8
$ echo "export LANG=ja_JP.UTF-8" >> ~/.bashrc

開発に必要になる基本的なツールをインストール

$ apt-get install vim git git-core curl zlib1g-dev build-essential libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 libpq-dev libxml2-dev libxslt1-dev libcurl4-openssl-dev python-software-properties libffi-dev aptitude

後々必要になりそうなのでnodejsも適当に入れておく

$ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
$ apt-get install -y nodejs

Rubyのインストール

Rubyについては、最新版をサクッと入れたいのでapt-getでなくrbenvを用いてインストールする

$ git clone https://github.com/rbenv/rbenv.git ~/.rbenv

rbenv用の初期化コマンドをbashrcに追加。これでrbenvディレクトリ経由でrubyを実行できるようにする。

$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ exec $SHELL

rbenvのプラグインで、rubyをインストールするためのruby-buildをインストール

$ git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
$ echo 'export PATH="$HOME/.rbenv/plugins/ruby-build/bin:$PATH"' >> ~/.bashrc
$ exec $SHELL

rbenvを用いて、現在の最新の安定版であるRUby2.4.1をインストール

$ rbenv install 2.4.1

rbenvで利用するRubyのバージョンを、先程インストールした2.4.1にする

$ rbenv global 2.4.1

Rubyのパッケージ管理ツールであるgemをインストール

$ apt-get install gem

gemがドキュメントもインストールするのを抑止するため、.gemrcに以下を記述

install: --no-document
update: --no-document

gemをインストールする度に自動でrbenbをrehashしてくれるrbenv-rehashをインストール

$ gem i rbenv-rehash

gemの依存管理をしてくれるbundleをインストール

$ gem i bundle

Ruby on railsの導入

railsをインストール

$ gem install rails

5.1.0がインストールできたことを確認

$ rails -v
Rails 5.1.0

mySQLをインストール

$ apt install mysql-server mysql-client

なんか以下も必要らしいのでインストール

$ sudo apt-get install libmysqld-dev

mysqlserverを起動

$ /etc/init.d/mysql start

railsアプリケーションの雛形を作成

$ rails new myapp -d mysql

雛形がデキていることを確認

$ ls
Gemfile  Gemfile.lock  README.md  Rakefile  app  bin  config  config.ru  db  lib  log  package.json  public  test  tmp  vendor

rails用のDBを作成

$ rake:db

DBがデキていることを確認

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| myapp_development  |
| myapp_test         |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
6 rows in set (0.01 sec)

railsアプリケーションを起動

rails s -b 0.0.0.0 -p 3000

が、何故かエラーが出る。gemライブラリが足りない的なエラーだがどうみても足りている。ググってもどうしても解決できなかったので、該当するコードをコメントアウト。調べてみるとタイムゾーン関係に必要になるそうなので、学習段階では大きな影響を受けないと判断。

myapp/vendor/bundle/ruby/2.4.0/gems/activesupport-5.1.0/lib/active_support/railtie.rb

 19       #begin
 20       #  TZInfo::DataSource.get
 21       #rescue TZInfo::DataSourceNotFound => e
 22       #  raise e.exception "tzinfo-data is not present. Please add gem 'tzinfo-data' to your Gemfile and run bundle install"
 23       #end
 24       #require "active_support/core_ext/time/zones"
 25       #zone_default = Time.find_zone!(app.config.time_zone)
 26
 27       #unless zone_default
 28       #  raise "Value assigned to config.time_zone not recognized. " \
 29       #    'Run "rake time:zones:all" for a time zone names list.'
 30       #end
 31
 32       #Time.zone_default = zone_default

改めてrailsアプリケーションを起動。無事に起動。

=> Booting Puma
=> Rails 5.1.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.8.2 (ruby 2.4.1-p111), codename: Sassy Salamander
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000

Railsアプリケーションの起動をブラウザで確認する

  • rails用のWebサーバはコンテナ内の3000番ポートで起動している
  • コンテナ起動時に、Dockerホストの8081番ポートをコンテナの3000番ポートにポートフォワーディングするように指定している
  • Macの8081番ポートをDebian(Dockerホスト)の8081番ポートにポートフォワーディングするように設定している(Vagrantfileにて)
    以上より、Macのブラウザでlocalhost:8081にアクセスすることで、コンテナ内で起動しているWebサーバにリクエストを送ることができる。

実際にブラウザを用いてアクセスしてみると、以下のようにrailsのウェルカムページが表示され、Dockerコンテナ側でもアクセスログが表示されているので、環境構築が成功したことがわかる。

Started GET "/" for 10.0.2.2 at 2017-05-03 14:19:10 +0000
Cannot render console from 10.0.2.2! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by Rails::WelcomeController#index as HTML
  Rendering vendor/bundle/ruby/2.4.0/gems/railties-5.1.0/lib/rails/templates/rails/welcome/index.html.erb
  Rendered vendor/bundle/ruby/2.4.0/gems/railties-5.1.0/lib/rails/templates/rails/welcome/index.html.erb (2.7ms)
Completed 200 OK in 115ms (Views: 5.4ms)

Dockerイメージのコミット

※ 以降ではコンテナから切断し、Dockerホスト側で作業を行う

4の項で、rails用の環境が構築されたコンテナが出来上がったので、このコンテナを元に新たなイメージをコミットする。

DockerHubにpushするためには、イメージ名が”DockerHubのユーザ名/イメージ名”である必要があるので、ここではそれに従ってコミットする

$ docker commit my_ubuntu sa2knight/rails

イメージ一覧に新しいイメージが追加されていることを確認。元のUbuntuとくらべて、様々なツールをインストールしたので容量が増大していることがわかる

$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
sa2knight/rails     latest              519638e57a05        22 hours ago        1.55 GB
ubuntu              latest              f7b3f317ec73        8 days ago          117 MB

DockerHubへのpush

ここでは、DockerHubに作成したイメージをpushするが、既にDockerHubアカウント及びリポジトリは作成済みで、ログインも完了しているものとする。

DockerHubへのpushは以下のコマンドで行う

$ docker push sa2knight/rails
The push refers to a repository [docker.io/sa2knight/rails]
d675a105e8a6: Pushing [========================================>          ] 1.153 GB/1.431 GB
d675a105e8a6: Pushed
08f405d988e4: Pushed
511ddc11cf68: Pushed
a1a54d352248: Pushed
9d3227c1793b: Pushed

latest: digest: sha256:2c8b2bb969347c41be37d8bb788e9c5a531b6478c8f4089039692d28bd74ec5d size: 1570

1.5HBをアップロードするのでかなり時間がかかるが、気長に待ってたらいつのまにか終わった。

DockerHubにアクセスすると、イメージがアップロードされていることが確認できる。

参考

DebianにDockerをインストールして基本的なコマンドを使ってみる

前提

以下の環境にDockerを導入する

要素 バージョン
Debian 8.6
Docker 1.13.0

概要

Debian8.6にDocker1.13.0をインストールし、そこでCentOSのコンテナを作成する。コンテナで適当に作業を行い、コミットして再利用するところまでをチュートリアルする。Dockerそのものに関する説明はそんなに無い。Dockerfileによる構築も扱わない。Dockerの雰囲気が味わえれば良いかなぐらい。

Dockerとは

Dockerはサーバ仮想化ソフトウェアの一つ。ただし、VMWareなどの、ハイパーバイザ型における完全な仮想化を提供するのではなく、ホストOSのカーネルを共有し、その上に「コンテナ」と呼ばれるOSのシステムレベルで仮想化されたゲストOSを取り扱う。

つまり、VMWareなどの仮想化と比べると不完全な仮想化で、ゲストOSの独立レベルは低い。Dockerではそのような構成にすることで以下のメリットが得られる。

  • 個別にOSを立ち上げる必要がないので作成や起動、破棄が高速
  • 使用する資源(CPU/メモリ/ストレージ)が少ない
  • オーバーヘッドが少なく性能劣化が殆ど無い

よって、Dockerの使い方としては、永続的に稼働している環境というよりは、頻繁に作成、破棄が行われる使い捨ての環境に用いることが向いていると言える。

個人的には開発環境を手軽に構築、共有するのに使っていけたら良いなと思う

Dockerのインストール

DockerはホストOSのカーネルを共有するので、当然Linux上に構築するのが望ましい。一応WindowsやMacでもDockerを導入することができるが、本記事ではMac上に構築したDebianの上にDocker環境を構築する。

例によってapt-getでインストールしたいところだが、Debianに標準で含まれてるaptリポジトリにdockerが含まれていないので、手作業でリポジトリを追加する必要がある。

/etc/apt/sources.list

が、aptのリポジトリリストなので、これに以下を追記する。ドメイン的に日本のDebian公式リポジトリなんだと思う。

deb http://ftp.jp.debian.org/debian/ sid main
deb-src http://ftp.jp.debian.org/debian/ sid main

で、アップデート

$ sudo apt-get update

dockerをインストール

$ sudo apt-get install docker.io

インストールできたことを確認

docker -v
Docker version 1.13.0, build 49bf474

Dockerイメージのインストール

Dockerでコンテナを作成するためには、元となるイメージが必要になる。

Dockerには、DockerイメージのリポジトリであるDockerHubを通じて、Dockerイメージを共有する機能があるので、今回はそちらからCentOSのDockerイメージをインストールする。

権限周りがやかましいのでここからはsudo権限を持たせる

$ sudo -s
# whoami
root

DockerHubからDockerイメージをダウンロードする場合は、docker pullコマンドを使用する。今回はCentOSをダウンロードする。

# docker pull centos
Using default tag: latest
latest: Pulling from library/centos
93857f76ae30: Already exists
Digest: sha256:4eda692c08e0a065ae91d74e82fff4af3da307b4341ad61fa61771cc4659af60
Status: Downloaded newer image for centos:latest

ダウンロード済みのDockerイメージの一覧の確認は、docker imagesコマンドで行う。centosのDockerイメージがダウンロードされていることが確認できる。

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
centos              latest              a8493f5f50ff        3 weeks ago         192 MB

Dockerイメージが仮想マシンのイメージと同じようなものと考えると、192MBというのは実に小さい。仮想マシンイメージと異なり、カーネルが含まれていないからである。

DockerコンテナによるHello World

DockerコンテナはDockerイメージから生成される仮想環境の実体である。一つのイメージから複数のコンテナを生成できるので、オブジェクト指向におけるクラスとインスタンスの関係のようなものだ。

コンテナ起動は、docker run コマンドを用いる。といっても、コンテナはホストOSのカーネルを用いてプロセスを実行するものなので、どんなプロセスを実行するかを指定する必要がある。

ここでは、”Hello Docker World”という文字列をechoするプロセスを実行するだけのコンテナを立ち上げてみる。

# docker run centos echo "Hello Docker World!"
Hello Docker World!
#

“Hello Docker World!” の文字列が出力され、コマンドの実行は終了する。ではこの時コンテナはどのような状態なのか、docker psコマンドで確認する。psコマンドは基本的にプロセスが動いているコンテナの一覧を表示するが、-aオプションを付与することで終了しているコンテナも表示する。

# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                          PORTS               NAMES
627496810cd6        centos              "echo 'Hello Docke..."   About a minute ago   Exited (0) About a minute ago                       angry_blackwell

627496810cd6がコンテナのID(ユニーク)で、angry_blackwellがコンテナの名称である。名称はユニークでない可能性もあるが、コンテナ生成時に任意の名称を付けることができる。

このコンテナはコマンド”echo ‘Hello Docker…”を実行完了し、Exited状態に移行している。

コンテナの削除

前項で作成したコンテナは、Hello Docker World!を出力してお役御免なので、docker rmコマンドを用いて削除する。削除の際はコンテナIDがコンテナ名を指定する。特に重複の可能性がないのであれば、可読性のあるコンテナ名を使ったほうが良さそう。

# docker rm angry_blackwell
angry_blackwell

削除されたことを確認

docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

コンテナを作成し、アクセスする

“アクセスする”という言い方には語弊がある気がするが、つまりSSHでアクセスしたときみたいにコンテナ内でシェルを使いたいということである。前項の用に、コンテナを起動する際にプロセスを指定することで、コンテナはプロセスを実行し、実行完了するまで稼働し続ける。

つまり、コンテナで実行するプロセスにシェルを指定してあげればよい。ここではbashを用いてシェルを利用することにする。また、-itオプションを付与することで、現在の標準入出力を引き継ぐことができるので、シェルを利用するためには必須となる。

# docker run -it centos /bin/bash
[root@a3936fcb8f31 /]# cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core)

コンテナ内でシェルを利用できるようになり、catコマンドでコンテナ内のOS情報ファイルを出力できていることがわかる。CentOSと書かれているが、実際にはホストOSであるDebianのカーネルを利用している。ややこしい。

コンテナ内でRubyをインストールする

コンテナ内に開発環境を作る、というほど大層なことは本記事では行わないが、その前進としてRubyをインストールして、適当なスクリプトを作成してみる。

カーネルはDebianのものを使ってるが、コンテナはCentOSなので、apt-getではなくyumを用いてRubyをインストールする。ここではRubyのバージョンとかは気にせず、簡単にインストールできる方法を用いる。

[root@a3936fcb8f31 /]# yum install ruby

インストールできたことを確認。どうやらこのコンテナ内のyumで標準でインストールできるRubyは2.0.0らしい。だいぶ古い。

[root@a3936fcb8f31 /]# ruby -v
ruby 2.0.0p648 (2015-12-16) [x86_64-linux]

適当にスクリプトを作成する。と思ったらvimも入っていないので先にvimをインストールする

[root@a3936fcb8f31 /]# yum install vim

インストールしたvimで、/fizzbazz.rb を作成する。フィズバズについてはWikipediaを参照。ここでは、コマンドライン引数Nを取り、1〜Nに対してのフィズバズを行うプログラムを作成する。カッコいいアルゴリズムでも使おうと思ったが、コードの内容は本記事とは関係ないので普通に書く。

N = ARGV[0].to_i
N.times do |m|
  if m % 3 == 0 && m % 5 == 0
    puts "Fizz Buzz"
  elsif m % 3 == 0
    puts "Fizz"
  elsif m % 5 == 0
    puts "Buzz"
  else
    puts m
  end
end

実行してみるとこんな感じ

root@a3936fcb8f31 /]# ruby /fizzbazz.rb 10
Fizz Buzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz

フィズバズを持つDockerイメージを作成する

前項では、コンテナ内でRubyとvimのインストール及びフィズバズプログラムの作成を行った。しかし、これからはあくまでコンテナ内に存在するだけで、元となったDockerイメージに存在するものではない。したがって、本コンテナを削除してしまえば前項の作業は水の泡となってしまい、再利用することができない。

そこで、本コンテナを元にしたDockerイメージを新たに作成する。つまり、「CentOSのイメージ」を元に、「CentOSの中にRubyとVimとフィズパズスクリプトが入ったイメージ」を作成するということだ。

コンテナからイメージを作成するには、docker commitコマンドを用いる。以下では、”unruffled_blackwell”(本コンテナの名前)を元に、”fizzbazz”というイメージを作成する。

# docker commit unruffled_blackwell fizzbazz
sha256:7bf2cab2f57325c76c67affd54f7545ecc7ecde41925e7cd2404ba6c602e05a7

イメージが作成されたことを確認。RubyとVimを入れたせいで容量がかなり増えている。それでも348MBだが。

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
fizzbazz            latest              7bf2cab2f573        26 seconds ago      348 MB
centos              latest              a8493f5f50ff        3 weeks ago         192 MB

作成したイメージから新たにコンテナを生成する

とりあえず先程Rubyやvimをインストールしたコンテナはもう不要なので削除する。以下は全てのコンテナを削除するおまじない的なコマンド。といっても、「全てのコンテナ一覧のコンテナIDのみ標準出力し、それらをdocker rmの引数にする」という一連の処理を行っているだけ。

# docker ps -aq | xargs docker rm
a9f5293d88a0
a3936fcb8f31
0357fffe82b2

これでRubyやVimがインストールされたコンテナが削除された。インストール作業は水の泡となってしまった。

# docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

ということはもちろん無く、以下のように先程作成したコンテナを用いてフィズバズプログラムを実行できることが確認できる。

# docker run fizzbazz ruby /fizzbazz.rb 10
Fizz Buzz
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz

まとめ

以上より、Dockerを用いることで非常に手軽にCentOSの環境を導入し、そこでRubyの開発環境(の卵)を作成することができた。さらに、Ruby開発環境を持ったCentOSのイメージを作成し、手軽に再利用できるようになった。

また、本記事では割愛しているが、DockerHubを用いて、本記事で作成したイメージをpushすることで、他のユーザがそれを再利用することもできる。逆に他のユーザが作成したイメージからコンテナを作成することもでき、例えばRubyの開発環境に留まらず、Ruby on railsの開発環境一式が揃ったイメージなども数多く出回っている。

つまり、今後何らかの技術を使って開発を行いたくなったら、その開発環境に適合するイメージを探して、docker pullするだけでスグに環境構築を行えるということだ。
完全に適合したイメージが無ければ、近いものをpullし、自分でカスタマイズしてcommit,pushすればさらに特定の目的に特化したイメージを流通させることができる。

今回、本記事を書くために初めてDockerを利用したが、まだまだ使いみちは模索中で、基礎的なコマンドしか抑えられていないので、今後はさらに実用的な使い方ができるように勉強し、実務にも取り入れられたらなと画策している。

GUIでgitを使っている人がCUIに乗り換えるための基礎知識

前提

本記事は以下の環境で動作確認しています

要素 バージョン
debian 8.6
git 2.1.4

また、本記事中ではリモートリポジトリとして、私用のGitHubを用いています。リモートリポジトリは以下のものを用いて、空の状態からスタートするものとします。

git@github.com:Sa2Knight/git_test.git

CUIによるメリット

SourceTreeなどのGUIツールを使う場合と、CUIでgitコマンドを叩いて利用する場合では、どちらもメリット・デメリットが存在すると思います。ここでは、CUIを使う場合のメリットにのみ触れますが、デメリットについてはCUIの利用を躊躇ってる方々の考えるとおりだと思います。(コマンド覚える必要があるなど)

※ 以下のメリット・デメリットは私の個人的な考えが多く、実際は異なる可能性もあります。

1. 作業内容を共有しやすい

これはgitに限らずCUI全般に言えることですが、CUI上での作業は全て正確に言語化することができます。ある作業内容を相手に伝えたい場合、実行したコマンドリストを送ることで全て解決します(もちろん付加情報を自然言語で与えることも重要ですが)

対してGUIでは、操作を言語化する際に曖昧な情報になりやすく、誤解を与えやすい問題があります。

2. 定型的な作業を自動化出来る

こちらもCUI全般の話ですが、GUIでの操作と比べたとき、CUIの操作は自動化を行いやすいです。例えば、pullしてaddしてcommitしてといった一連の作業は、GUIだとマウスカーソルをそれなりに動かして、何度かクリックしたりといった定形作業が発生します。(ショートカットキーを使った場合でも)

対してCUIでは、一連の処理を一つのコマンドで実行できるようにすることで、定形作業をまとめて実行することができます。

3. 使い方を検索しやすい

言うまでもなく、バージョン管理システム”git”は、CUIツールです。SourceTreeなどのGUIツールは、GUIでgitを利用できるようにしてくれる後発のソフトウェアです。

そのため、当然ながらgitの使い方について検索した場合に出てくる情報の大半はCUIのコマンドです。もちろんSourceTreeなど、GUIソフトウェア名を検索ワードに含むことでカバーは効きますが、それでもCUIに関する情報量と比べれば雀の涙程度です。

以上より、gitを利用していて何らかの問題が発生した時に、それを検索して解決できる可能性は確実にCUIのほうが早く見つかると言えます。

4. カッコいい

これに尽きます

gitのインストール

ここでは、debianにapt-getを用いてgitをインストールします。他の環境でのインストール方法については適宜ググってください。gitは特別インストールが難しいツールではないので大丈夫だと思います。

$ sudo apt-get install git

gitがインストールされていることを確認する

$ which git
/usr/bin/git
$ git --version
git version 2.11.0

gitの利用

以降では、実際に以下の手順でgitを利用してみます。
1. ローカルリポジトリの作成
2. 空のファイルを作成してコミット
3. リモートリポジトリを登録し、pushする
4. 既存のファイルを編集する
5. 既存のファイルの削除
6. リモートリポジトリの更新をローカルリポジトリに反映

また、本項では以下のコマンドを利用します

コマンド名 内容
git init ローカルリポジトリを作成
git status リポジトリの状態を確認
git remote add リモートリポジトリを追加
git add ファイルをステージング・エリアに移動
git rm ファイルをリポジトリから削除
git diff 直前のコミットと現在のファイルの差分を確認
git commit リポジトリをコミット
git push ローカルリポジトリの更新をリモートリポジトリに反映
git pull リモートリポジトリの更新をローカルリポジトリに反映

1. ローカルリポジトリの作成

以下のコマンドは、作業ディレクトリ直下で実行してください

$ git init
Initialized empty Git repository in /home/vagrant/gittest/.git/

2. 空のファイルを作成してコミット

適当にファイルを作ります。以下は、”test”と書かれたテキストファイルを作成しています。

$ echo "test" > file1
$ cat file1
test

git statusコマンドを実行すると、リポジトリ内の現在の状態を確認できます

$ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	file1

nothing added to commit but untracked files present (use "git add" to track)

Untracked filesは、まだリポジトリに追加されていないファイルのことです。コレに対して、git addコマンドを用いて、file1をリポジトリに追加します。

$ git add file1

もう一度git statusを実行すると、file1がリポジトリに追加され、Changes to be comittedの下にfile1が表示されていることが確認できます。この場所をステージング・エリアと呼びます。

 git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   file1

$

コミットすることができるのは、ステージング・エリアにあるファイルのみです。つまり、現状の場合file1のみがコミット対象のファイルです。コミットは、git commitコマンドで行うことができます。

$ git commit -m "初めてのコミット"
[master (root-commit) 32cd62b] 初めてのコミット
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file1

これで初めてのコミットが完了です。-mオプションを指定することで、”初めてのコミット”など、コミットコメントを記述することができます。-mを省略した場合、vimなどのエディタが立ち上がり、コミットメッセージの記述を求められます。

コミットが完了したことを、コミットログを閲覧することで確認することができます。コミットログの閲覧には、git logコマンドを用います。

$ git log
commit 32cd62bb013c5f8fe7021789ebb68d6a618fab9e
Author: Sa2Knight <shingo.sasaki.0529@gmail.com>
Date:   Sun Apr 23 20:15:53 2017 +0900

    初めてのコミット

“初めてのコミット”がコミットログに書き込まれていることが確認できます。

commit 32cd62bb013c5f8fe7021789ebb68d6a618fab9e

は、コミットハッシュと呼び、全てのコミットを一意に識別するための文字列です。

3.リモートリポジトリを登録し、pushする

ここでは、空のリモートリポジトリ

git@github.com:Sa2Knight/git_test.git

を登録し、ローカルリポジトリの内容をpushします。

リモートリポジトリの登録にはgit remote addコマンドを用います。

git remote add origin git@github.com:Sa2Knight/git_test.git

originはリモートリポジトリに付けた名前です。特にこだわりがなく、一つしかリモートリポジトリを使わない場合はoriginにする慣習があるっぽいです。

登録されているリモートリポジトリの一覧は、git remote -vコマンドで確認できます。

$ git remote -v
origin	git@github.com:Sa2Knight/git_test.git (fetch)
origin	git@github.com:Sa2Knight/git_test.git (push)

前項でコミットした内容をリモートリポジトリにpushします。git pushコマンドを用いてpushします。

$ git push origin master
Counting objects: 3, done.
Writing objects: 100% (3/3), 239 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:Sa2Knight/git_test.git
 * [new branch]      master -> master

masterはリモートリポジトリのブランチ名です。ブランチについて詳しくない方はとりあえずmasterと入れるものと思ってください。

4.既存のファイルを編集する

既存のfile1の内容を、”test”から”test2″に書き換えます。

$ echo "test2" > file1
$ cat file1
test2

リポジトリの状態を確認すると、file1がChanges not staged for commitの欄に表示されていることが確認できます。

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   file1

no changes added to commit (use "git add" and/or "git commit -a")

modifiedと書かれているとおり、ファイルの内容が前回のコミット時から変更されているが、ステージング・エリアに移動していないファイルを表します。

ここで、前回のコミット時と現在でファイルがどう変わったのか、git diffコマンドで確認することができます。

$ git diff file1
diff --git a/file1 b/file1
index 9daeafb..180cf83 100644
--- a/file1
+++ b/file1
@@ -1 +1 @@
-test
+test2

“- test”となっている部分は”test”という行が削除されたことを表し、逆に”+ test2″となっている部分は、”test2″という行が追加されたことを表します。

前項の手順で、ステージングエリアへの移動、コミット、プッシュを行いますが、やることは同じなので割愛します。

5.既存のファイルの削除

先程から利用していたfile1を削除します。

$ rm file1

リポジトリの状態を確認すると、今度はdeleted: file1と表示されています。

$ git status
On branch master
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	deleted:    file1

no changes added to commit (use "git add" and/or "git commit -a")

これは、リポジトリに存在するファイルがディレクトリに存在しない状態を表します。リポジトリからも削除する場合は、git rmコマンドを用いて、ステージング・エリアに移動します。

$ git rm file1
rm 'file1'
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	deleted:    file1

あとはコミット、プッシュを行うだけです。

6. リモートリポジトリの更新をローカルリポジトリに反映

今度は、リモートリポジトリ側が誰かによって更新されたとします。ここでは、リモートリポジトリにfile2というファイルが追加されたことを想定して、git pullコマンドを用いてpullします。

$ git pull origin master
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:Sa2Knight/git_test
 * branch            master     -> FETCH_HEAD
   9cc3229..0d3c678  master     -> origin/master
Updating 9cc3229..0d3c678
Fast-forward
 file2 | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 file2

pull後、追加されたfile2が存在することが確認できます。

$ ls file2
file2

その他のコマンド

以下のコマンドは本記事では使用しませんでしたが、覚えると非常に役立つコマンドです。興味があればどんどん調べて使ってみてください。コマンドは私が使うもののみで、実際は他にもう少しあります。

コマンド名 内容
git checkout ファイルを直前のコミット時の状態に戻す
git stash ファイルの変更状態を一時的に保存する
git reset リポジトリの状態を特定のコミット時に戻す
git branch リポジトリのブランチに関する操作を行う
git merge 複数のブランチを統合する
git rebase 複数のコミットを統合する

Bowerを使ってフロントエンドの依存関係を管理する

前提

要素 バージョン
debian 8.7
node 7.4.0
npm 4.1.2
bower 1.8.0

概要

Debianにnpmを用いてBowerをインストールし、Bowerを簡単に使ってみる

Bowerとは

Bower(バウアー)はフロントエンド開発の依存関係(JavaScript/CSS/HTMLなど)を管理してくれるフロントエンド用パッケージ管理システム。

PHPのComposer、RubyのGem、iOSのcocoaPodsなどの仲間だと思えば良い。

ライブラリなどのインストールをコマンドで行えるようになり、インストールした内容及びその依存関係が、bowser.jsonに書き込まれる。

同一のbowser.jsonさえあれば、他環境にライブラリをインストールする際にコマンド一発で解決する。

Bowerのインストール

npmを用いてインストールを行う。パッケージ管理システム(npm)でパッケージ管理システム(bower)をインストールするのがちょっと面白い。ここではver1.8.0がインストールされる。

$ sudo npm install bower -g
/usr/local/bin/bower -> /usr/local/lib/node_modules/bower/bin/bower
/usr/local/lib
└── bower@1.8.0

bower.jsonの作成

初めにパッケージの依存関係などを記述するbower.jsonを作成する。といっても、JSONファイルを直接作成するのでなく、以下のコマンドを実行すると、対話式に入力を促されるのでそれに従っていけば自動的にbower.jsonが作成される

$ bower init

以下のように、名称、説明文、メインファイル(省略可)、キーワード(省略可)、作者、ライセンス、Webサイト(省略可)などを入力することで、bower.jsonが生成される。ここで入力される情報のうちのいくつかは、自分でbowerにライブラリを公開する場合に必要になるものなので、単に開発で試用するライブラリを管理するという場合は省略して良い。

$ bower init
? name qs-test
? description bower test directory
? main file
? keywords
? authors Sa2Knight <shingo.sasaki.0529@gmail.com>
? license MIT
? homepage
? set currently installed components as dependencies? No
? add commonly ignored files to ignore list? No
? would you like to mark this package as private which prevents it from being accidentally published to the registry? No

{
  name: 'qs-test',
  authors: [
    'Sa2Knight <shingo.sasaki.0529@gmail.com>'
  ],
  description: 'bower test directory',
  main: '',
  license: 'MIT',
  homepage: ''
}

? Looks good? Yes

Bowerを用いてjQueryUIパッケージをインストールする

Bowerを用いたパッケージのインストールは、下記のコマンドで行える。 –save を付与することで、インストール結果をbower.jsonに記録してくれるので実質必須。

$ bower install [パッケージ名] –save

jQueryUIのパッケージ名はjquery-uiなので、以下のように、jQueryUIをインストールする

$ bower install jquery-ui --save
bower jquery-ui#*               cached https://github.com/components/jqueryui.git#1.12.1
bower jquery-ui#*             validate 1.12.1 against https://github.com/components/jqueryui.git#*
bower jquery#>=1.6              cached https://github.com/jquery/jquery-dist.git#3.2.1
bower jquery#>=1.6            validate 3.2.1 against https://github.com/jquery/jquery-dist.git#>=1.6
bower jquery-ui#^1.12.1        install jquery-ui#1.12.1
bower jquery#>=1.6             install jquery#3.2.1

bower.jsonの内容を確認すると、jQueryUI1.12.1に依存している状態にあることが追記されている

$ cat bower.json
{
  "name": "qs-test",
  "authors": [
    "Sa2Knight <shingo.sasaki.0529@gmail.com>"
  ],
  "description": "bower test directory",
  "main": "",
  "license": "MIT",
  "homepage": "",
  "dependencies": {
    "jquery-ui": "^1.12.1"
  }
}

インストールされたパッケージは、bower_componentsディレクトリ以下に配置されるのだが、ここを見てみると、jQueryUIが依存しているjQuery本体もインストールされていることがわかる。

$ ls bower_components/
jquery  jquery-ui

このように、Bowerでは、インストール対象のパッケージが依存している他のパッケージについても自動的にインストールしてくれる。(composerやnpm、gemなどに慣れている人にとっては当然だが)

所感

  • npmの影響を大きく受けているらしく、全体的な仕様やコマンドの使い方がnpmに限りなく近い
  • bowerでは、対象パッケージをgitからcloneしてくるだけなので、実際に必要なるファイルはその中の極一部である。それをどのように扱うかはまた別の手段を用いる必要があるようなので、即戦力という感じでは無さそうだ
  • scriptタグでロードする時に依存関係に基づいた順番でロードしなきゃならないって問題は解決できてない
  • と言うかJavaScriptの外部ライブラリってそんな依存管理が必要なほど沢山使うかな
  • bowerの触りをやった程度なのでメリットがあまり掴めてない。詳しい方がいたらご教授願う。
  • bowerのサブコマンドが大量にあるので、余裕があれば色々見てみたい

参考

[Ruby] BacklogをCUIで操作してみる

前提

要素 バージョン
Debian 8.6
Ruby 2.2.2
gem 2.4.5
bundle 1.13.4
Backlog API 2.9.0
BacklogKit 0.15.0

また、Backlogの個人設定より、APIキーを生成していることを前提とする

概要

国産プロジェクト管理ツールであるBacklogを、ブラウザではなく、BacklogAPIを用いて、スクリプト経由で操作する。スクリプトの記述には、APIを間接的に利用するライブラリであるBacklogKitを用いるためにRubyを採用する。

本記事で作成するツールでは、主に以下のことをCUIで実現する

  • ユーザ情報の取得
  • 課題の作成(タイトル/本文のみ指定可能)
  • 課題の操作(ステータスの変更/完了理由の変更/実作業時間の変更)

実用的にするにはさらにカスタマイズする必要があるが、本記事では基本的なAPI利用が出来るようになる部分までにする。また、本課題での動作確認は、個人利用のために開設しているBacklogスペースを用いる。

BacklogAPI利用の準備

今回は、BacklogAPIをRubyで簡易的に利用するためのライブラリである、BacklogKitを利用する。BacklogKitはgemで管理されているので、bundleを用いてインストールする。

※ gem: Rubyのパッケージングシステム
※ Bundle: gemパッケージを管理するツール

Gemfileに以下のように記述

source "https://rubygems.org"
gem 'backlog_kit'

Bundleでインストール

bundle install --path bundle/vendor

スペースIDとAPIキーを記述したファイルを作成

BacklogAPIを利用するためには、対象のスペースIDと、APIキーが必要になる。スクリプト中にハードコーディングしても良いが、本記事で実装するツールは、Githubにてコードを公開しているため、APIキーが漏洩することを避けるために別途JSONファイルに記述することにする(もちろん該当のJSONファイルはリポジトリで共有しない)

ということで、以下のようなsecret.jsonを作成する。もちろんAPI_KEYはダミー

{
  "space_id": "saknight",
  "api_key": "hogehogefugafugafoofoobarbar",
  "project_key": "DEV"
}

とりあえず汎用的なメソッドを持たせることを目的に、Util.rbを作成し、そこにJSONファイルをロードするメソッドを実装する

require 'json'
class Util
  def self.load_secret_file
    File.open('secret.json') do |file|
      JSON.load(file)
    end
  end
end

これでBacklogAPIを利用する下準備は完了

BacklogAPIを使ってみる

Backloger.rbを作成し、ここにBacklogAPIを利用するコードを書いていく。とりあえずBacklogAPIを試しに使ってみるということで、ユーザ情報を取得してみる。

require 'backlog_kit'
require_relative 'Util'

secret = Util.load_secret_file
client = BacklogKit::Client.new(
  space_id: secret['space_id'],
  api_key: secret['api_key']
)

p client.get_space.body

bundleを使っているため、上記ファイルを以下のように実行する

$ bundle exec ruby Backloger.rb

以下のような実行結果が得られた

$ bundle exec ruby Backloger.rb
#<BacklogKit::Resource:0x007f9534806850 @attributes={:spaceKey=>"saknight", :name=>"saknight", :ownerId=>85748, :lang=>"ja", :timezone=>"Asia/Tokyo", :reportSendTime=>"18:00:00", :textFormattingRule=>"backlog", :created=>"2017-03-04T15:47:40Z", :updated=>"2017-03-04T15:47:40Z"}>

どうやらBacklogKitではAPIの実行結果はBacklogKit::Resourceクラスのオブジェクトとして返却されるらしい。詳しい使い方はドキュメントやソースコードを追う必要があるが、以下のようにコードを変更すればスペースIDが取得できそうだ。

p client.get_space.body.spaceKey

実行結果

$ bundle exec ruby Backloger.rb
"saknight"

とりあえずBacklogkitを用いたBacklogAPIの利用が正常にできていることがわかった。あとはドキュメントやソースコードを読んで本ツールの目的を達成するためのコードを作成していく

課題の作成に必要なパラメータの取得

リファレンスによると、課題の追加には以下のパラメータが必須となる

  • 課題の名前
  • プロジェクトID
  • 課題の種別ID
  • 課題の優先度ID

ここで面倒なのが、プロジェクト/種別/優先度が全てIDを要求されていることだ。
例えば優先度の場合「高」「中」「低」といった表示名で指定するのではなく、それに対応するIDが必要になる。面倒なことにIDはBacklog環境のカスタマイズ次第で変わってしまうので、今回はスクリプトから動的に取得できるようにすることにする。

プロジェクトID/種別ID/優先度IDを動的に取得するために、以下のIdentifierクラスを作成した

require 'backlog_kit'

class Identifier

  def initialize(client)
    @client = client
  end

  def project(projectKey)
    project = @client.get_projects.body.find do |pb|
      pb.projectKey == projectKey
    end
    project.id
  end

  def priority(label)
    priority = @client.get_priorities.body.find do |pr|
      pr.name == label
    end
    priority.id
  end

  def issueType(project_key, label)
    type = @client.get_issue_types(project_key).body.find do |it|
      it.name == label
    end
    type.id
  end
end

上記クラスのメソッドは、それぞれプロジェクトキー、優先度、種別を指定すると、それに対応するIDを返却してくれる。
例えば以下のようなコードを実行すると

identifier = Identifier.new(client)
p identifier.projectId("DEV")
p identifier.priorities('中')
p identifier.issueTypeId("DEV", 'タスク')

以下のようにそれぞれIDに変換して出力してくれる

$ bundle exec ruby Backloger.rb
38382
3
172585

もちろんIDは基本的に不変であるため、毎回動的に取得する必要はなく、一度取得したら適当にキャッシュするなどすれば簡単に高速化することができるが、ここでは割愛する。

課題の作成

ようやく本題。前項で、課題の作成に必要なパラメータを動的に取得できるようになったので、create_issueメソッドを用いて、課題を作成する。以下のコードでは、「課題テスト」という、優先度が「中」で、種別が「タスク」の課題を作成する(他の設定項目は未設定状態)

client.create_issue('課題テスト', {
  :projectId   => identifier.project(secret['project_key']),
  :priorityId  => identifier.priority('中'),
  :issueTypeId => identifier.issueType(secret['project_key'], 'タスク'),
})

スクリプト実行後、課題が生成されていることが確認できる

課題の編集に必要なパラメータの取得

今回は、前項で作成した課題に対して、以下の編集を行う。

  • ステータスを「完了」にする
  • 完了理由を「対応済み」にする
  • 実績時間を「1時間」にする
  • 適当なコメントを投稿する

“課題の作成に必要なパラメータの取得”同様、ステータス/完了理由は、対応するIDが必要になるので、同様にIdentifierクラスに以下のメソッドを追加する。

  def status(label)
    status = @client.get_statuses.body.find do |st|
      st.name == label
    end
    status.id
  end

  def resolution(label)
    resolution = @client.get_resolutions.body.find do |res|
      res.name == label
    end
    resolution.id
  end

以下のコードを実行すると

identifier = Identifier.new(client)

p identifier.status('完了')
p identifier.resolution('対応済み')

以下のように完了と対応済みに対応するIDが取得できる

$ bundle exec ruby Backloger.rb
4
0

課題の編集

前項で課題編集のためのIDが取得できたので、update_issueメソッドを用いて、以下のように課題の修正を行う

client.update_issue('DEV-47', {
  :statusId => identifier.status('完了'),
  :resolutionId => identifier.resolution('対応済み'),
})

実行すると、以下のように課題が修正されたことが確認できる

所感

  • Backlogを普段から利用してるが、どうにも定型的な操作が億劫だと思っていたので、色々自動化してみたいと思いBacklogAPIの利用を初めてみた
  • リファレンスを見る限りほぼ全ての操作をAPIで利用できるので、極端に言えばブラウザを一切使わずにBacklogを利用することもできそう
  • 本記事ではBacklogAPI及びBacklogKitについて軽く触れた程度だが、ここから実用的なツールの開発を進め、一段落したところでまた記事を書ければなと思う
  • ここ1ヶ月資格試験に追われてなかなかプログラミング出来てなかったので、リハビリがてら楽しくコーディングできた
  • Rubyを使うのも久しぶりだが、リスト操作の柔軟さはやはりRubyならでは。気持ちよく使える

[Swift3] UIImageから、アスペクト比を維持してサムネイルを生成する

前提

要素 バージョン
xcode 8.2.1
Swift 3.0.2
iPhone 6s
iOS 10.2.1

概要

既存のUIImageに対して、アスペクト比を維持したままサムネイル(縮小画像)を作成する関数を実装する。
本関数ではWidthを指定し、そのWidthとアスペクト比に合致したHeightを計算してリサイズする。
リサイズは非破壊的に行い、オリジナルのUIImageを維持したまま、リサイズ後のUIImageを新たに生成して返却する。

もっと良い方法がある気がするが、ググってもあまり見つからなかったので自前で実装

コード

/**
 * 横幅を指定してUIImageをリサイズする
 * @params image: 対象の画像
 * @params width: 基準となる横幅
 * @return 横幅をwidthに、縦幅はアスペクト比を保持したサイズにリサイズしたUIImage
*/
func resizeUIImageByWidth(image: UIImage, width: Double) -> UIImage {
  // オリジナル画像のサイズから、アスペクト比を計算
  let aspectRate = image.size.height / image.size.width
  // リサイズ後のWidthをアスペクト比を元に、リサイズ後のサイズを取得
  let resizedSize = CGSize(width: width, height: width * Double(aspectRate))
  // リサイズ後のUIImageを生成して返却
  UIGraphicsBeginImageContext(resizedSize)
  image.draw(in: CGRect(x: 0, y: 0, width: resizedSize.width, height: resizedSize.height))
  let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()
  return resizedImage!
}

備考

  • パラメータと計算式を微修正すれば、Heightをベースにリサイズする関数も作れる
  • 計算式をそれなりに弄れば、現在のサイズからn%リサイズといった関数も作れる
  • 指定したWidthがオリジナルより大きかったらオリジナルを戻すとか加えたほうが良さそう