Ruby」カテゴリーアーカイブ

Capybara(Rspec)で指定したセレクタ要素が存在しないことを検証する

以前は以下を参考にして例外を発生させて検証してたが、

Capybaraで要素がないことをテストする方法 | 自転車で通勤しましょ♪ブログ http://319ring.net/blog/archives/2546/

以下で充分検証することができた。例外待ちしない分こちらのほうが高速。

def cant_find(selecter)
  expect(page.all(selecter).empty?).to eq true
end

[Ruby] 外部からWordPressの記事を取得する

前提

以下の環境で実装/動作確認済み

要素 バージョン
ruby 2.3.1
gem 2.6.12
bundle 1.15.1
rubypress 1.2.2

概要

  • Rubyを用いて外部のWordPressの記事一覧、詳細を取得する
  • 対象WordPressの有効なアカウント情報が必要
  • 記事の投稿、編集など基本的な全ての処理が可能だが、本記事では取得までに抑える

XML-RPCの有効化

本記事ではWordPressのXML-RPC機能を用いて外部から操作を行う。ただしこの機能はセキュリティの観点から無効になっている場合が多い。その場合はwp_config.phpの以下の行をコメントアウトすることで有効化出来る。サーバに入れない環境の場合は諦めるしか無い。

add_filter('xmlrpc_enabled', '__return_false');

rubypressの導入

RubyでXML-RPCを用いてWordPressを操作する時に最も手軽に利用できそうなgemライブラリであるrubypressを導入する。ここでは空のGemfileを作成するところから始める

source "https://rubygems.org"
gem "rubypress"

ディレクトリ内にインストール

$ bundle install --path vendor/bundle

WordPressクライアントオブジェクトの作成

Rubypressを用いてClientオブジェクトを作成する。ここでは対象WordPressのホスト名、ユーザ名、パスワードが必要になる。

require 'rubypress'
class WordPress

  @@hostname = 'qs.nndo.jp'
  @@username = 'sasaki'
  @@password = 'XXXXXXXXXXXXXXXX'

  def initialize
    @wp_client = Rubypress::Client.new(
      host: @@hostname,
      path: '/xmlrpc.php',
      username: @@username,
      password: @@password,
    )
  end

end

記事を取得する主なメソッドの使い方

post_idを指定して取得

@wp_client.getPost(post_id: 13)

出力。長いので一部のみ掲示

{"post_id"=>"13",
 "post_title"=>"サンプル記事3",
 "post_date"=>
 "post_status"=>"publish",
 "post_content"=>"サンプル記事3本文",
 "post_parent"=>"0",
 "post_mime_type"=>"",
 "post_format"=>"standard",
 "terms"=>[],
 "custom_fields"=>[]}

一部のフィールドのみ取得

タイトルと本文さえあれば良いという場合は以下のように

@wp_client.getPost(post_id: 13, fields: [:post_title, :post_content])
{"post_id"=>"13", "post_title"=>"サンプル記事3", "post_content"=>"サンプル記事3"}

全ての記事一覧を取得

出力が長くなってしまうので、タイトルのみ取り出すようにする

@wp_client.getPosts(fields: [:post_title])
[{"post_id"=>"18", "post_title"=>"サンプル記事1"},
 {"post_id"=>"20", "post_title"=>"サンプル記事2"},
 {"post_id"=>"22", "post_title"=>"サンプル記事3"}]

カスタム投稿タイプを指定して記事一覧を取得

例えば”news”というカスタム投稿タイプがあるとして、newsの一覧を取得する

@wp_client.getPosts(filter: {post_type: :news}, fields: [:post_title])
[{"post_id"=>"8", "post_title"=>"サンプルニュース1"},
 {"post_id"=>"10", "post_title"=>"サンプルニュース2"}]

参考

[Ruby] nilの可能性があるオブジェクトでメソッド呼び出しを安全に行う

例えば文字列に対してlengthメソッドを呼び出すと、その文字数を取得できる

irb(main):001:0> a = 'sasaki'
=> "sasaki"
irb(main):002:0> a.length
=> 6

一方、aがnilだった場合はlengthメソッドが存在しないので例外が発生する

irb(main):001:0> a = nil
=> nil
irb(main):002:0> a.length
NoMethodError: undefined method `length' for nil:NilClass
	from (irb):2
	from /root/.rbenv/versions/2.3.1/bin/irb:11:in `<main>'

しかし、変数名の末尾に&をつけることで、対象がnilだった場合メソッド呼び出しを行わずにnilを戻すようにできる

irb(main):001:0> a = nil
=> nil
irb(main):002:0> a&.length
=> nil

nilチェックが不要になるのでコードをスマートに書けるようになる

[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にアクセスすると、イメージがアップロードされていることが確認できる。

参考

[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ならでは。気持ちよく使える