Rubyのバージョンを上げたときの手順メモ

自端末と自作サービスのRubyのバージョンを2.4.1へ上げたのでそのときのメモを残しておきます。

rbenvとruby-buildの更新

自分はRubyのバージョン管理をrbenvで行っているので、まず、rbenvの更新します。

rbenvを更新

cd ~/.rbenv
git pull origin master

ruby-buildを更新

cd ~/.rbenv/plugins/ruby-build
git pull origin master

Ruby 2.4.1 をインストー

rbenv install 2.4.1

端末のスペックによってそれぞれですが、結構時間がかかります。

Ruby 2.4.1 を設定

環境全体への設定

rbenv global 2.4.1

自作サービスのRubyバージョンも設定しておきたいので、以下のコマンドで .ruby-version を作成します。

rbenv local 2.4.1

バージョンが上がっている確認

以下のコマンドでバージョンが上がっているか確認できます。 .ruby-version があるディレクトリで実行した場合は .ruby-version で設定されている値が優先されます。

ruby -v       
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]

以上でRubyのバージョンアップは完了です。

AngularJS1.6に上げたらルーティング時のパスがhash (#/)からhash-bang (#!/)になっていた

AngularJS1.6に上げたらルーティング時のパスがhash (#/)からhash-bang (#!/)になっていた

自己学習用に作成していたAngularJS 1.4アプリをAngularJS 1.6 に上げた際に、 ルーティング時のパスがhash #/ からhash-bang #!/ に変換されるようになってしまい、うまくルーティングが働かなくなってしまう問題に遭遇しました。 対応したのでメモとして残しておきます。

現象

以下のURLに直接またはリンクから遷移しようとしたとき、例えば以下のような

http://localhost:9000/#/search

にすると、ルーティング定義したページを表示して欲しいのですが、

http://localhost:9000/#!/#%2Fsearch

上記のように自動的にスラッシュが %2F 変換され、hash-bang #!/ が追加されます。 自分の環境では、上記のURLはルーティング定義していないので、 otherwise に設定しているページに遷移されるようになってしまいました。

ちなみに、ルーティングの定義は以下のように行っています。

.when('/search', {
  templateUrl: 'views/search.html',
  controller: 'SearchCtrl',
  controllerAs: 'search'
})
.otherwise({
  redirectTo: '/'
});

原因

AngularJS 1.6が $location サービスのURLのデフォルト値を変更したことが原因です。

Googleクローラー対策みたいです。(#!/を使用することを推奨している。)

ちなみに、以下のコミットで修正されています。 https://github.com/angular/angular.js/commit/aa077e81129c740041438688dff2e8d20c3d7b52

解決方法1

リンクのURLの#のあとに!を加えるようにします。 これが一番素直な解決方法かと。私はこちらで対応しています。

<a ng-href="#/search">Search</a>

を以下のように#/#!/にしてあげます。

<a ng-href="#!/search">Search</a>

解決方法2

AngularJS 1.6 以前と同じく #/ でいけるようにするには、 $locationProvider.hashPrefix('') を設定することで対応できます。

appModule.config(['$locationProvider', function($locationProvider) {
  $locationProvider.hashPrefix('');
}]);

参考

https://docs.angularjs.org/guide/migration#commit-aa077e8 https://github.com/angular/angular.js/commit/aa077e81129c740041438688dff2e8d20c3d7b52 http://stackoverflow.com/questions/41211875/angularjs-1-6-0-latest-now-routes-not-working http://stackoverflow.com/questions/41272314/angular-all-slashes-in-url-changed-to-2f

Gemに渡す町コードを一括保存するバッチを作成した

先日、以下の記事で紹介した、ごみ収集曜日を取得する処理をGemへ渡すためのコードを取得するバッチを作成しました。

takapi86.hatenablog.com

なぜ作成したか。

先日書いた記事でも紹介しましたが、このGemでごみ収集曜日を取得するためには以下のパラメータとして以下のコードを渡す必要があります。

しかし、上記コードを見つけるためには、実際に横浜市のHPにアクセスし、URLを確認してパラメータを設定しなければなりません。 それだと、そもそも横浜市のHPにアクセスしているので「普通にで確認すれば良いじゃん」になるので、一括で上記コードを取得し、DBに保存するバッチを作成しました。

ページの構造

ごみ収集曜日のページ(横浜市資源循環局)は以下のような階層構造になっています。

収集曜日ページのトップです。区を選択することができます。 区の名前をクリックすると、その区の町名の頭文字を選択する画面に遷移します。

  • 頭文字の選択ページ

例:港北区(区コード:16) http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16

町(収集場所)の頭文字を選択する画面です。以下のような構成で各行ごとにリンクが貼られています。

[1] あ~お
[2] か~こ
[3] さ~そ
[4] た~と
[5] な~の
[6] は~ほ
[7] ま~も
[8] やゆよ
[9] ら~ろ、わ
  • 町名の選択ページ

例:港北区(区コード:16) さ行(頭文字コード:3) http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3

選択した区の選択した行(あかさたな)に該当する町名一覧が表示されます。

港北区のさ行では、以下の一覧が表示されます。

町名にリンクが貼られており、クリックすると、該当の町の収集曜日が表示されます。

篠原北1丁目
篠原北2丁目
篠原台町
篠原町
篠原西町
篠原東町1丁目
篠原東町2・3丁目
下田町1・2・3丁目
下田町4・5・6丁目
新横浜1・2・3丁目
新吉田町
新吉田東1~6丁目
新吉田東7・8丁目
  • ごみ・資源物の収集日ページ

ここで、ごみ・資源物の収集日を確認することができます。

例:港北区(区コード:16) さ行(頭文字コード:3) 町コード 1073 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3/1073

Gemは、このURLを見てページからごみ収集曜日を取得します。

バッチの取得対象

先述した通り、

がわかれば、ごみ収集曜日を取得することができるので、 区から町名を選択する画面まで、リンクに含まれる各コードと名称を取得しながら再帰的にデータを取得していきます。

収集場所までバッチで持ってくれば良いのでは?とお思いの方もいらっしゃると思うのですが、 すべてのページのデータを持ってくるためには、 18区×あかたたな(9行)×町名分必要であり、横浜市のサーバへかなりのリクエストが発生してしまうため含めておりません。

あとは、もうGemつくちゃったしな・・・と、なんかすいません。といった感じです。

また、負荷については怒られないように、配慮していてバッチ内では5秒一回しかリクエストを飛ばさないようにしています。

作ってみた所感

今回は、基本となる機能は雑に一気に仕上げ、それから例外処理の追加やリファクタリングをしていくという流れで実装していきました。 ほぼ勢いで実装しましたが、丁寧に積み上げていく方式よりも自分の感覚的に合っているなと感じました。

というのは、処理を書く上で色々考えなければならないことがシンプルになるということ * Rubyっぽく書くには * クラスをどう分ける? * わかりやすい変数名は?

と、普段は実装する上で同時に考えることが多いので、まず一気に仕上げてから細かい部分を調整していくやり方にしてみて、こっちの方が性にあっているのでは?という気持ちになっていますす。(精神的にも疲れにくく、集中して取り組めたのではないかと感じました。)

すべてのケースには当てはまらないと思いますが、実装の仕方はこれからも試行錯誤しながらやっていきたいと思います。

ごみ収集曜日を取得する処理をGem化した

ごみ収集曜日を取得する処理だけを切り出し、Gem化したので、使い方をメモしておきます。

機能は随時更新していく予定です。

どのようなGemか?

横浜市資源循環局のページから、ごみ収集曜日の情報を取得しHashの配列で返します。

データ量の少ないモバイル版から取得しています。 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/

使い方

例えば、港北区篠原町の収集曜日は以下のURLです。 http://cgi.city.yokohama.jp/shigen/kaishu/mobile/16/3/1073

区、頭文字、町名にそれぞれコードが振られており、上記URLの場合は、

  • 区: 16
  • 頭文字: 3
  • 町名: 1073

になっています。

情報を取得するには、 Yokohama::Gomi::Schedule::App#fetch_schedule メソッドを叩く必要があり、 引数にそれぞれ区、頭文字、町名、 ward_id:,initial_code,town_id を指定します。

戻りは、取得した曜日(漢字)に対して、曜日に応じたごみの情報が入ったHashの配列(月〜土)で返ってきます。

作ってみて

Gemを作成するのは初めてでしたが、 bundle gem コマンドを使うことで簡単に作成することができました。 現在は区、頭文字、町名のコードをがわかっていないと使えないので、今後は文字列から各コードを参照するような機能などを追加していきたいと思います。

DatabaseRewinderを追加した

【ごみ収集曜日APIシリーズ】FactoryGirlを追加した - takapi86のブログ

の続きです。 前回は、テストを2回目実行しようとすると、ユニーク制約に引っかかりエラーになってしまう問題が残ってしまいました。 今回はその対応として、DatabaseRewinderというGemを使い、Rspecのテスト後に、 Createで作成したTestデータをデータベースから削除するようにしていきます。

github.com

上記のREADME.mdを参考にし導入しました。

導入

Gemfiledatabase_rewinder を追加して、bundle installします。

テスト環境のみで使用するので、 grouptest だけでよいでしょう。

group :test do
  gem 'database_rewinder'
end

rails_helper に以下の記述を追加します。

複数DBを扱うわけではないので、基本的な設定でいきます。

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseRewinder.clean_all
  end

  config.after(:each) do
    DatabaseRewinder.clean
  end
end

試してみる

データがない状態で確認をしたいので、 以下のコマンドで、DBをリセットしておきます。

RAILS_ENV=test bundle exec rake db:migrate:reset

実行してみます。

bundle exec rspec spec/requests/towns_spec.rb
..

Finished in 2.18 seconds (files took 3.28 seconds to load)
2 examples, 0 failures

bundle exec rspec spec/requests/towns_spec.rb
..

Finished in 1.43 seconds (files took 3.2 seconds to load)
2 examples, 0 failures

データを見てみます。

[1] pry(main)> Town.all
  Town Load (3.0ms)  SELECT "towns".* FROM "towns"
=> []
[2] pry(main)>

2回以上実行しても、正常に動作し、テスト後のデータもすべてクリアされていました。

バッと書いてしまいましたが、今回はこんなところで。

FactoryGirlを追加した

いつもの、ごみシリーズです。

今週土日は、自分の所属しているバドミントンクラブのHPの作成で終わってしまいました。 デザイン難しい。

さて、今回は前回 RspecでHTTPのリクエスト・レスポンスのテストを行った - takapi86のブログ で追加したテストに、テストデータを追加していきます。

こちらを参考に導入を行いました。

File: GETTING_STARTED — Documentation for factory_girl (4.8.0)

導入

  • Gemfilefactory_girl_railsを追加して、bundle installします。
group :test do
  gem 'factory_girl_rails'
end
  • factory_girlのテストデータを作成するコードを保管する場所として、spec/factories/ を作成しておきます。

ファイルを作成する

区モデル(ward)、町モデル(town)のFactoryGirlファイルを作成します。 ※ward、townは1:nの関係です。

詳しくは、

  • spec/factories/wards.rbを作成
FactoryGirl.define do
  factory :ward do # モデル名を指定
    sequence(:id)
    ward_code 16
    name '港北区'
  end
end
  • spec/factories/towns.rbを作成
FactoryGirl.define do
  factory :town do # モデル名を指定
    sequence(:id)
    ward_code 16
    initial_code 3
    town_code 1073
    name '篠原町'
  end
end

試してみる

  • test環境にコンソールで入ります。
RAILS_ENV=test bundle exec rails c

wardのデータを作ってみる

[1] pry(main)> ward = FactoryGirl.create(:ward)
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "wards" ("id", "ward_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["id", 1], ["ward_code", 16], ["name", "港北区"], ["created_at", 2017-03-26 14:57:30 UTC], ["updated_at", 2017-03-26 14:57:30 UTC]]
   (145.8ms)  commit transaction
=> #<Ward:0x00559a99c3ccb8
 id: 1,
 ward_code: 16,
 name: "港北区",
 created_at: Sun, 26 Mar 2017 14:57:30 UTC +00:00,
 updated_at: Sun, 26 Mar 2017 14:57:30 UTC +00:00>

townのデータを作ってみる

[2] pry(main)> FactoryGirl.create(:town, ward: ward)
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "towns" ("id", "ward_code", "initial_code", "town_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?)  [["id", 1], ["ward_code", 2], ["initial_code", 3], ["town_code", 1073], ["name", "篠原町"], ["created_at", 2017-03-26 14:59:00 UTC], ["updated_at", 2017-03-26 14:59:00 UTC]]
   (60.9ms)  commit transaction
=> #<Town:0x00559a9daa47f8
 id: 1,
 ward_code: 2,
 initial_code: 3,
 town_code: 1073,
 name: "篠原町",
 created_at: Sun, 26 Mar 2017 14:59:00 UTC +00:00,
 updated_at: Sun, 26 Mar 2017 14:59:00 UTC +00:00>

うまくいったっぽいです。 念の為、testのDBはクリアしておきます。

RAILS_ENV=test bundle exec rake db:migrate:reset

前回のテストに追加

require 'rails_helper'

RSpec.describe TownsController, type: :request, json: true do
  let(:town) { FactoryGirl.create(:town, ward: create(:ward)) }

  describe 'GET /towns/:name.json' do
    before do
      get URI.escape("/towns/#{town.name}.json")
    end

    it '200 OK を返す' do
      expect(response.status).to eq(200)
    end
  end
end

こちらもうまくいったようです。

ちなみに、ここではFactoryGirl.createと記載していますが、クラス名は、 spec_helper.rbの Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } のコメントを外し、以下のようにファイルを追加するか、そのままconfig.include FactoryGirl::Syntax::Methods と追記することで、省略できます。

# spec/support/factory_girl.rb
RSpec.configure do |config|
  config.include FactoryGirl::Syntax::Methods
end

ただ、このままでは2回目実行すると、以下のようなエラーが発生してしまうようです。

ActiveRecord::RecordNotUnique:
       SQLite3::ConstraintException: UNIQUE constraint failed: wards.id: INSERT INTO "wards" ("id", "ward_code", "name", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)
     # ./spec/requests/towns_spec.rb:2:in `block (2 levels) in <top (required)>'
     # ./spec/requests/towns_spec.rb:7:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # SQLite3::ConstraintException:
     #   UNIQUE constraint failed: wards.id
     #   ./spec/requests/towns_spec.rb:2:in `block (2 levels) in <top (required)>'

どうやら、続けてテストする場合は、テスト後にテストしたデータを削除する処理を入れる必要があるようです。 こちらは、次回対応していきたいと思います。

RspecでHTTPのリクエスト・レスポンスのテストを行った

rspecでHTTPのリクエスト・レスポンスのテストを行った。

今回も、ごみシリーズです。

ごみ収集日の検索をLineからできるようにした - takapi86のブログで、APIを作っていく中で、rspecでのテストでHTTPのリクエスト・レスポンスのテストができるということを知ったので、メモして起きます。

事前準備

  • 以下のgemはインストール済みである前提で行います。

対象

  • ごみ収集日の検索をLineからできるようにした。にも書いてありますが、ゴミ収集APIには以下のエンドポイントがあります。
    • 収集場所コードから収集場所のスケジュールを返すAPI
    • 収集場所名を前方一致で検索し、一覧で返すAPI
    • 上記2つのAPIを使い、Lineでメッセージを受け取り、スケジュールや収集場所一覧を返すAPI

今回は上記のうち、収集場所名を前方一致で検索し、一覧で返すAPIのテストを書きたいと思います。

導入

rspecのテストファイルは以下の場所 * spec/requests/towns_spec.rb

  • 今回はTownsControllerのテストで、リクエストを投げて確認するため、以下のように記述します。
describe TownsController, type: :request do
end
  • エンドポイントはGETメソッドで、/towns/:nameなので以下のように記述します。
describe TownsController, type: :request do
  describe 'GET /towns/:name' do
  end
end
  • ステータスコードは200で返ってくるのを期待するテストを追加します。
  • ここでは省略していますが、事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。
describe TownsController, type: :request do
  describe 'GET /towns/:name' do
    it '200 OK を返す' do
      get URI.escape("/towns/#{"日吉"}")
      expect(response.status).to eq(200)
    end
  end
end
  • 事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。
  • テストを実行してみましょう。
bundle exec rspec spec/requests/towns_spec.rb
Finished in 0.50264 seconds (files took 3.08 seconds to load)
1 example, 0 failures

正常にテストが終了したようです。

今回は、test環境にデータが入った状態でテストを行いましたが、 先ほど言ったように事前にFactoryGirl等でテストデータを生成するようにすると良いでしょう。