20190524のRubyに関する記事は21件です。

Railsで基本情報技術者試験の過去問題サイトを作る(3:親子関係、登録編)

はじめに

ゆる〜く学ぶ。みんなのWeb勉強コミュニティー。 「にゅ〜ぶる会」を運用中です。
https://newburu.github.io/

そこで、何か教育用のコンテンツが欲しいなぁ〜と思い立ち、今回の企画をスタートしました!

Railsで基本情報技術者試験の過去問題サイトを作ります!

最終目標

  • 問題・回答の登録は、Scaffoldで簡易でOK
  • APIを用意して、ランダムに問題を抽出する機能を追加する
  • TwitterBOT、LINEBOT、SlackBOTが出来たら良いな

履歴

1:構築編
  https://qiita.com/newburu/items/ed59f47ac645b19620f6
2:日本語化(i18n)編
  https://qiita.com/newburu/items/4f12fdb61bf6cd601545/
3:親子関係、登録編
  本ページ

今回やる事

  • 親子関係を設定し、登録しやすくする

※レイアウトをやろうと思いましたが、こちらの方が優先なので、予定を変更させて頂きました。

親子関係を設定し、登録しやすくする

1. gem 'cocoon', gem 'jquery-rails'を追加します。

Rails5.1からjQueryがなくなったため、jquery-railsも追加する必要があります。

./Gemfile
gem 'cocoon'
gem 'jquery-rails'
bundle-install
$ bundle install

2. Modelに、親子関係を設定します。

・親(question)には、「has_many :answers」を追加します。
・親が削除された時に、一緒に子も削除されるように、「dependent: :destroy」をつけます。
・accepts_nested_attributes_forを使うと、親と子を一緒にcreate/update出来るようにします。
・削除できるよう「allow_destroy: true」をつけます。

app/models/question.rb
  has_many :answers, dependent: :destroy
  accepts_nested_attributes_for :answers, allow_destroy: true

子(models/answer.rb)は、作成時にreferencesとしているため、自動で設定されています。

app/models/answers.rb
  belongs_to :question

3. Viewに、親子同時に登録・更新出来るように設定します。

application.jsに以下を追加します。

app/javascripts/application.js
//= require jquery
//= require cocoon

viewを変更します。

app/views/questions/_form.html.slim
  .answers
    = f.fields_for :answers do |answer|
      = render 'answer_fields', f: answer
    = link_to_add_association "追加", f, :answers

子供の情報登録用フォームになるviewファイルを新規作成します。

app/views/questions/_answer_fields.html.slim
.nested-fields
  .field
    = f.label :msg
    = f.text_area :msg
  .field
    = f.label :correct
    = f.check_box :correct

  = link_to_remove_association "削除", f

4. コントローラーで登録可能にします。

以下のように子のパラメータを受け取れるようにしましょう。

app/controllers/questions_controller.rb
    def question_params
      params.require(:question).permit(:category1, :category2, :category3, :msg, answers_attributes: [:id, :msg, :correct, :_destroy])
    end

5. 確認します。

新規作成画面はこんな感じになります。
新規作成画面

「追加」ボタンを押すたびに、子供の入力エリアが追加されます。
追加

今回はここまで

ありがとうございました!
次回は、参照画面などに、子供の情報を追加していこう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby][Git] git log -L で特定の関数 (メソッド) の変更履歴が見られるらしいので Ruby でも試してみた

概要

次のツイートを見て便利そうだと思った。

どうやら

git log -L :<関数名>:<ファイルパス>

という形式で特定の関数の変更履歴が見られるらしい。
このツイートでは Python の関数の変更履歴を表示していたが、Ruby のメソッドでもできるか試してみた。

試しに Active Support コア拡張機能の String#truncate の変更履歴を確認してみる。ローカルに Rails のリポジトリを git clone して、次の git コマンドを実行する。

git log -L :truncate:activesupport/lib/active_support/core_ext/string/filters.rb

01.png

02.png

本当に truncate のログと差分だけ表示された!メソッドの変更を追跡するのにファイルごとだと粒度が大きいので、メソッドごとに表示できるのは便利だ ☺️

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rubocop について

rubocop とは

コーディング規約に準拠してるかチェックするgemです!
簡単に言いますと、インデントやメソッド名、改行などのチェックをしてくれるものです。
今回は、このrubocopさんについてを会社のプロダクトにいれた時(後入れ)のやり方にも交えて書きます。

流れ

  • $gem install rubocop or Gemfile にかいて $bundle install
  • $rubocop で実行(たぶんめちゃめちゃ怒られます。。。)
  • 怒られた箇所修正!!!

.rubocop.yml を作成!

プロジェクト直下に「.rubocop.yml」を作成!!

rubocop.yml
Rails:
  Enabled: true

AllCops:
  Exclude:
    - 'db/schema.rb'
    - 'vendor/**/*'

# 日本語でのコメントを許可
AsciiComments:
  Enabled: false

このようなrubocop に対して制御するファイルを作成することで、ひとつひとつを修正していける!
(実際はその人や会社によってもっと設定がいることもあります。)

と簡単そうですが、後入れの場合、まぁーめちゃくちゃ怒られます!

そこで、私が実際にやったやり方は、

$bundle exec rubocop -R --auto-gen-config --exclude-limit 999999

オプションつけてrubocop コマンドを叩くと、「.rubocop_todo.yml」が作られます!!
このファイルは、現コードに対する一時回避の設定をしてくれます!これによって、怒られなくなります。
つまり、このファイルがなくてもエラーが起きないことがゴールです〜

このファイルをもとに「.rubocop.yml」を設定していきます!


実際の流れ

0.はじめに

rubocop_todo.yml
inherit_from: .rubocop.yml

これを記述して、両方のファイルが参照されるようにしておく!
or
.rucocop_todo.ymlの内容を.rubocop.ymlに全コピー

  1. copを一個コメントアウト(削除)

  2. $rubocop何で怒られているかチェック
    -> 直したくない(これからもこのcop に対しては無視したい)->3A
    -> 直さないといけない->3B

3A.
例: AsciiCommentsに関しては、全ページで許可したい!

rubocop.yml
# cop でどのようなことが怒られたかとか、どうしたいかを明記しておいたほうが良さそう
AsciiComments:
  Enabled: false

を追記
もしくは、

rubocop.yml
AsciiComments:
  Exclude:
    - 'app/models/hoge.rb'

3B.
$rubocop -a自動で修正してくれる(これがめちゃ便利)単純にコードミスや可読性の高いコードにも直してくれる!
注:メソッドの名前などは直してくれない!!

ちなみに、私は$rubocop回した段階で何で怒られているのかがわからないやつは、とりあえず$rubocop -aを叩きました。
それで、diff でみてどう変更されたかを確認してました!!

流れに関してはこんなもんです!ひたすら、怒られているところの確認-> 修正 or 「cop」 無視

.rubocop.ymlの書き方について

rubocopの対象から除外するファイル指定

rubocop.yml
AllCops:
  Exclude:
    - db/schema.rb
    - 'vendor/**/*'

copの無効化・有効化

rubocop.yml
Rails:
  Enabled: true
rubocop.yml
Lambda:
  Enabled: false

基本的には、cop は有効化されているのでEnabled: trueはそこまで使わないかも
Enabled: falseはcop 自体を無視したいときに使います!

warning のみを検知

rubocop.yml
Bundler/OrderedGems:
  Severity: warning

warning レベルから取得

自動化

rubocop を最大活用するためには、自動化するしかないと思いまして、CircleCIと連携することにしました!
CircleCIに関しては、ここでは飛ばします笑

circleci/config.yml
jobs:
  build:
    steps:
      - run: bundle exec rubocop

と設定するだけです!
自動で回してくれます!
github と CircleCI の連携と CircleCI ファイルの中にrubocop のコマンドを設定することでgithub 上で結果を確認できます!

まとめ

そもそも導入した経緯-> レビューする人が忙しい!!細かい修正で何度もやり取りするのが無駄!

  • チームみんなのコードで既存のcop の設定とは違う物が来たときなどのメンテナンスは、必要かなと。
  • 私自身、プロダクトに導入したばかりで結果がまだわかっていないので、便利なのか、あまり良くないかはまだわかっていません(今更ですが)

非常に良かった点

  • まだ、コードを書き始めて長くない私にとって、$rubocop -aによって修正されたり、怒られたりすることでコードの違った書き方を勉強するいい機会になった!(実際これが一番でかい)

参考

https://qiita.com/kyohei_shimada/items/e739dec967eb5e61721c
https://blog-ja.sideci.com/entry/2015/03/12/160441

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

よく見かけるattr_accessorについて

はじめてのqiita投稿ですが、がんばりたいと思います。

1.ゲッターとセッターについて

railsを学び始めて早4ヶ月。
ただrailsのコードを書くだけではなく、rubyの基礎を学ぼうと思いました。
そこで気になったのがattr_accessor、これはよく見かけるがなんだろうと。

さて本題。

ゲッターくんとセッターくんの役割を実際のコードで見てみようと思います。

class Book
  def book=(book)
    @book = book
  end

  def book
    @book 
  end
end

1つ目のメソッドは引数で受け取ったデータをインスタンス変数に代入します。
このインスタンス変数を代入するためのメソッドのことを「セッター」と呼びます。

2つ目のメソッドではbookメソッドの中身を変更して、設定した名前を返しています。
このインスタンス変数の内容を参照するためのメソッドを「ゲッター」と呼びます。

2.attr_accessorについて

先程のゲッターとセッターを毎回毎回設定するのは大変ですよね
しかしrubyには便利なメソッドがあります。
それがattr_accessorという訳です。

class Book
  attr_accessor :book
end

というようにすっきり書けたのでは無いでしょうか。
attr_accessorメソッドを用いると、シンボルで :book と書けるようになりました。

3.さいごに

ちなみに、ゲッターのみの場合はattr_reader,セッターのみの場合はattr_writerと書けます。

という風に、普段何気なく使っているメソッドでもこれだけの仕事をしていたとは驚きですね。

これからも気になったら、どんどんqiitaでアウトプットしようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsコマンド

はじめに

備忘録としてまとめていきます。
随時追記していきます。

Railsプロジェクトを作成
$ rails new [プロジェクト名] -d [データベース名]
データベース作成
$ rails db:create
サーバー起動
$ rails server
コントローラー作成
$ rails generate controller [コントローラー名(複数形)][アクション名・アクション名..][オプション名]
モデル作成
$ rails generate model [モデル名(単数形)][属性名:データ型][属性名:データ型]
マイグレーション
$ rails db:migrate
テスト検証
$ rails test
integration作成
$ rails g integration_test [テストファイル名]
統合テスト検証
$ rails test integration
indexを追加
$ rails g migration add_index_to_[テーブル名]_[カラム名]
$ rails db:migrate             #最後にDBをマイグレートする。 
カラム追加
$ rails g migration add_[カラム名]_to_[テーブル名] [カラム名][データ型]

DBリセット

$ rails db:migrate:reset
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsとLivedoor Weather Web Serviceを使って各地点の天気を表示させてみた

はじめに

前回の記事で、Rubyから天気情報を取得することができました。
RubyでWeather Hacksを使って天気を取得してみた
この取得処理とRailsを使って、Web上に天気情報を表示します。

環境

macOS Mojave(10.14.4)
Ruby 2.6.3
Rails 5.2.3

取得処理

以下の順で各地点の天気情報を取得します。

  1. 表示させたい地点名を読み込む
  2. Livedoor Weather Web Serviceで提供されている都市と1を使って、地点のidを取り出す
  3. 各地点の天気情報を取得する

表示させたい地点名を読み込む

テレビの全国天気で、よく表示される各地点のjsonファイルを作成します。

main_city.json
{
  "city":[
    {
      "name": "釧路"
    },
    {
      "name": "旭川"
    },
・・・略・・・
    {
      "name": "那覇"
    },
    {
      "name": "石垣島"
    }

これを読み込む処理を作成します。

  def read
    file_path = File.expand_path('config/main_city.json', __dir__)
    @city_list = []
    File.open(file_path, 'r') do |text|
      @parse_text = JSON.parse(text.read)
      @parse_text['city'].each { |city|
        @city_list.push(city['name'])
      }
    end
  end

Livedoor Weather Web Serviceで提供されている都市と1を使って、各地点のidを取り出す

提供されている地点のIDをjsonファイルで書いておきます。

location_id.json
{
    "area":
    [
        {
            "name": "北海道",
            "prefs":[
                {
                    "name": "北海道",
                    "city":[
                        {
                            "name": "稚内",
                            "id": "011000"
                        },
                        {
                            "name": "旭川",
                            "id": "012010"
                        },
・・・略・・・
                        {
                            "name": "石垣島",
                            "id": "474010"
                        },
                        {
                            "name": "与那国島",
                            "id": "474020"
                        }
                    ]
                }
            ]
        }
    ]
}

このjsonファイルを読みこみ、各地点のidを取得します。
単純にforループで回すだけですね。
もっといい方法がありそうですが、今はこの方法でやります。

  def read_main_location_id
    parse_text = read_location_id

    location_list = LocationList.new
    reader = MainCityReader.new
    reader.read
    area = parse_text['area']
    for area_no in 0..area.count - 1
      prefs = area[area_no]['prefs']
      for pref_no in 0..prefs.count - 1
        city = prefs[pref_no]['city']
        for city_no in 0..city.count - 1
          city_name = city[city_no]['name']
          if reader.contain?(city_name)
            location = Location.new
            location.area_name = area[area_no]['name']
            location.pref_name = prefs[pref_no]['name']
            location.location_name = city_name
            location.id = city[city_no]['id']
            location_list.add(location)
          end
        end
      end
    end
    return location_list
  end

  private

  def read_location_id
    file_path = File.expand_path('config/location_id.json', __dir__)
    parse_text = ''
    File.open(file_path, 'r') do |text|
      parse_text = JSON.parse(text.read)
    end
    return parse_text
  end

各地点の天気情報を取得する

idを使ってURLを作成します。

  BASE_URL = 'http://weather.livedoor.com/forecast/webservice/json/v1?city='.freeze
  def create(location_id)
    return BASE_URL + location_id.to_s
  end

URLから天気情報を持ったjsonファイルを取得します。

  def read(url)
    response = URI.open(url)
    @parse_text = JSON.parse(response.read)
  end

あとはjson内から天気情報を取得して終わりです。

表示結果

全国の5/24の天気を簡単に表示させた結果です。
スクリーンショット 2019-05-24 16.51.53.png

5/24は全国的に晴れのようですね。

今後

次にやっていきたいのは以下の3つですね。

  • 最高気温、最低気温が取得できるようなので画面に追加
  • 各地点をクリックすると、その地方の詳細な天気が見れるようにする
  • ログイン機能を持たせ、ログインすると明日、明後日の天気も表示できるようにする

独学でRubyを書いているので、指摘事項ありましたらコメントに記載をお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ruby on rails: Can't connect to local MySQL server through socket '/tmp/mysql.sock' (2)となった時の対処法

前提

僕のようなプログラミング初心者に良くあることで、例えばMacのアップデートするときに再起動するとローカル環境で立ち上げていたMySQLサーバーが切れるのですが、それを知らず何気なくrails sとするとタイトルのようなエラーメッセージが表示されるかと思います。
その際の対処法として、備忘録として残しておきます。そんなの知っているよっている人はスルーしていただければ幸いです。

対処法

ターミナルで下記のコードを打ち込んでください。これだけで解決します。

$ mysql.server start
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsアプリケーションのRubyとBundlerのバージョンをアップデートする

環境

$ cat /etc/system-release
Amazon Linux AMI release 2018.03

# プロジェクトのディレクトリ内にて
$ rbenv -v
rbenv 1.1.1-30-gc8ba27f

$ rbenv versions
* 2.5.3 (set by /var/www/myapp/.ruby-version)

$ bundle -v
Bundler version 1.17.1

# すでにアップデートされたRailsアプリケーション
$ bin/rails -v
Rails 5.2.3

はじめに

Railsアプリケーションをアップデートするにあたり、RubyとBundlerのバージョンもアップデートする必要があったので備忘録として残しておきます。

Rubyのバージョンを確認

rbenvにインストールしたいバージョンが存在するか確認します。

$ rbenv install -l

以上のコマンドで該当バージョンが出てこなければ、ruby-buildをアップデートする必要があります。

$ cd /usr/local/rbenv/plugins/ruby-build && git pull && cd -

アップデートしたいRubyのインストール

# ruby2.6.2をインストール
$ rbenv install 2.6.2

# ruby2.6.2に切り替え
$ rbenv global 2.6.2

# 再読み込みし、切り替えの反映
$ rbenv rehash

# 反映されたことを確認
$ rbenv versions
  system
  2.5.3
* 2.6.2 (set by /var/www/myapp/.ruby-version)

トラブルシューティング

インストールしたRubyのバージョンに切り替わらない

# 正しい参照先
$ which ruby
/usr/local/rbenv/shims/ruby

rbenvでRubyをインストールした場合、以上のように正しい参照先は/.rbenv/shims/rubyとなります。それ以外だと参照先が間違っている可能性があるため、PATHを通します。EC2では全てのユーザーがログイン時にrbenvを使えるようにするために、/etc/profile.d/rbenv.shにPATHを通している場合があります。

# /etc/profile.d/rbenv.shにPATHの記載があるかもしれない場合は要確認!
$ cat /etc/profile.d/rbenv.sh
export RBENV_ROOT="/usr/local/rbenv"
export PATH="${RBENV_ROOT}/bin:${PATH}"
eval "$(rbenv init -)"

# 以上にPATHがなければ、~/.bash_profileに記述
$ echo 'export PATH=~/.rbenv/bin:$PATH' >> ~/.bash_profile
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile

# ~/.bash_profileの変更を反映
$ source ~/.bash_profile

Rubyのバージョンをアップデートしたものの、bundle installができない

Gemfile.lockに記載されているBUNDLE_WITHとbundlerのバージョンが異なる場合、bundle installをした時に以下のようなエラーが出てしまいます。

$ bundle install --path vendor/bundle -j4
・・・
find_spec_for_exe': can't find gem bundler (>= 0.a) (Gem::GemNotFoundException)
・・・

まずはGemfile.lockにて、BUNDLED WITHを確認します。確認したバージョンのbundlerをインストールします。
bundlerの参照先を確認し、/.rbenv/shims/bundlerとなっていればOKです。

# BUNDLE_WITHでbundlerのバージョンを確認
$ vim Gemfile.lock
・・・
BUNDLED WITH
   1.17.3

# Gemfile.lockに記載されているバージョンと同じbundlerを指定してインストール
$ rbenv exec gem install bundler -v 1.17.3

# 参照先の確認
$ which bundler
/usr/local/rbenv/shims/bundler
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

google-drive-permission-searchを使うまでのエラー対処メモ

会社で使用しているGoogleドライブの各ファイルのオーナーをエクスポートしたく、
google-drive-permission-searchを使用してみることに。詳しい使い方はこちら

インストール

こちらの記事を参考に、事前にGoogle API ConsoleからDriveAPIのOAuthクライアントIDを発行し、
ファイル名をclient_secrets.jsonで保存しておく。

言われた通りインストール手順を踏む。

$ git clone https://github.com/morimorihoge/google-drive-permission-search.git
$ cd google-drive-permission-search
$ cp ~/client_secrets.json ./
$ bundle

エラー発生

error
Fetching gem metadata from https://rubygems.org/........
 Using i18n 0.6.11
 Fetching json 1.8.1
 Installing json 1.8.1 with native extensions
 Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

     current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator
 /Users/*****/.rbenv/versions/2.6.2/bin/ruby -I /Users/*****/.rbenv/versions/2.6.2/lib/ruby/2.6.0 -r ./siteconf20190524-91989-tn4e6c.rb extconf.rb
 creating Makefile

 current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator
 make "DESTDIR=" clean

 current directory: /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1/ext/json/ext/generator
 make "DESTDIR="
 compiling generator.c
 In file included from generator.c:1:
 ./../fbuffer/fbuffer.h:175:47: error: too few arguments provided to function-like macro invocation
     VALUE result = rb_str_new(FBUFFER_PAIR(fb));
                                               ^
 /Users/*****/.rbenv/versions/2.6.2/include/ruby-2.6.0/ruby/intern.h:814:9: note: macro 'rb_str_new' defined here
 #define rb_str_new(str, len) RB_GNUC_EXTENSION_BLOCK(   \
         ^
 In file included from generator.c:1:
 ./../fbuffer/fbuffer.h:175:11: warning: incompatible pointer to integer conversion initializing 'VALUE' (aka 'unsigned long') with an expression of type 'VALUE (const char *,
 long)' (aka 'unsigned long (const char *, long)') [-Wint-conversion]
     VALUE result = rb_str_new(FBUFFER_PAIR(fb));
           ^        ~~~~~~~~~~
 generator.c:840:25: error: use of undeclared identifier 'rb_cFixnum'
     } else if (klass == rb_cFixnum) {
                         ^
 generator.c:842:25: error: use of undeclared identifier 'rb_cBignum'
     } else if (klass == rb_cBignum) {
                         ^
 1 warning and 3 errors generated.
 make: *** [generator.o] Error 1

 make failed, exit code 2

 Gem files will remain installed in /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/gems/json-1.8.1 for inspection.
 Results logged to /Users/*****/.rbenv/versions/2.6.2/lib/ruby/gems/2.6.0/extensions/x86_64-darwin-17/2.6.0-static/json-1.8.1/gem_make.out

 An error occurred while installing json (1.8.1), and Bundler cannot continue.
 Make sure that `gem install json -v '1.8.1' --source 'https://rubygems.org/'` succeeds before bundling.

 In Gemfile:
   activesupport was resolved to 4.1.6, which depends on
     json

以下のログに注目

error
Installing json 1.8.1 with native extensions
 Gem::Ext::BuildError: ERROR: Failed to build gem native extension.
 ~~
 In Gemfile:
    activesupport was resolved to 4.1.6, which depends on
      json

activesupportjsonの依存関係について言われているので、bundle updateで解消する。

$ bundle update

参考:https://stackoverflow.com/questions/40776575/an-error-occurred-while-installing-json-1-8-1-and-bundler-cannot-continue-r

再度インストール

$ bundle

エラーも出ず、インストールが成功した。

いざ使用

書き出し実行!

$ bundle exec ./google-drive-permission-search.rb --verbose --type excel

またエラー発生

error
bundler: failed to load command: ./google-drive-permission-search.rb (./google-drive-permission-search.rb)
 LoadError: cannot load such file -- google/api_client
    /Users/~~/google-drive-permission-search/google-drive-permission-search.rb:4:in `require'
    /Users/~~/google-drive-permission-search/google-drive-permission-search.rb:4:in `<top (required)>'

LoadError: cannot load such file -- google/api_client

google/api_client のバージョンがいけないようなので使用できた報告のある0.7に指定する。

Gemfile
gem 'google-api-client', '0.7'

インストール

$ bundle

再度実行するもまたエラー

error
bundler: failed to load command: ./google-drive-permission-search.rb (./google-drive-permission-search.rb)
 LoadError: cannot load such file -- retriable

LoadError: cannot load such file -- retriable

retriableが入っていなかったのでインストール。
こちらも使えたと報告のあるバージョン1.4.1に指定してインストール

Gemfile
gem 'retriable', '1.4.1'

参考:
https://github.com/googleapis/google-api-ruby-client/issues/132
https://github.com/googleapis/google-api-ruby-client/issues/107

もう一度実行するとGoogleのOAuth確認画面がブラウザで表示された。
しかし

惜しいところでエラー発生

error
ArgumentError: header User-Agent has field value "google-drive-permission-search/1.0.0 google-api-ruby-client/0.7.0 Mac OS X/10.13.6\n (gzip)", this cannot include CR/LF 

rubyのバージョンが2.4.3だと動くと書いてあったのでバージョンを下げてみる(今まで2.6.2)

$ rbenv install 2.4.3
$ rbenv local 2.4.3
$ ruby -v
# 2.4.3

$ bundleを実行しようとしたらエラー

rbenv: bundle: command not found

The `bundle' command exists in these Ruby versions:
   2.6.2

下記を実行すればいいらしい。

$ rbenv exec gem install bundle

参考:
https://bugs.ruby-lang.org/issues/14664
https://qiita.com/_am_/items/c1dbeb11f40bbbac8fd9

そろそろいってくれー!

$ bundle exec ./google-drive-permission-search.rb --verbose --type excel

ログが出てきた!!成功!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

form_withでvalidationエラーが出ない原因と対処法

はじめに

Rails5.1からform_forとform_tag非推奨になり、form_withが推奨になりました。
form_forの感覚でform_withを使ってハマったのでまとめていきます。

問題: validationエラーがviewに表示されない

下記のようにコーディングしており、
validationエラーがnewのviewに発生するはずなのにエラーメッセージが発生しない。

new.html.erb
<% provide(:title, "Sign up")%>
  <h1>Sign up</h1>

  <div class="container">
    <div class="row">
      <div class="col-md-6 col-md-offset-3">
        <%= form_with model: @user do |f| %>
        <%= render 'shared/errors_messages' %>

        <%= f.label :name %>
        <%= f.text_field :name, class: 'form_control' %>

        <%= f.label :email %>
        <%= f.email_field :email, class: "form_control" %>

        <%= f.label :password %>
        <%= f.password_field :password, class: 'form_control' %>

        <%= f.label :password_confirmation, "Confirmation" %>
        <%= f.password_field :password_confirmation, class: 'form_control' %>

        <%= f.submit "Create my account", class: "btn btn-primary" %>
      <% end %>
    </div>
  </div>
</div>
_errors_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

原因: form_withはデフォルトオプションがremote:trueになっている

Ajaxを行うときに、form_tagやform_forはオプションでremote: trueで対応していたが、
form_withではデフォルトでremote: trueになっている(デフォルトでAjaxになっている)。

解決: form_withのオプションをlocal: trueを指定する

formでAjaxを使用しない時はオプションlocal: trueを指定する。
form_withのオプションでlocal: trueを指定するとvalidationエラーをviewに表示できる。

参考リンク・資料

Rails 5.1のform_withでViewにvalidationエラー表示
ActionView::Helpers::FormHelper
actionview/lib/action_view/helpers/form_helper.rb

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Ruby] fizzbuzzの複数コード紹介

概要

fizzbuzz問題はいろんな解法があるな、と思ったので少しまとめ。
今回、3つのパターンを紹介しますが、結果はどれも同じになります。

パターン1

i = 1
while i <= 30
  if i % 15 == 0
      puts "FizzBuzz"
  elsif i % 3 == 0
      puts "Fizz"
  elsif i % 5 == 0 
      puts "Buzz"
  else
      puts i
  end
  i += 1
end

素直につくったらこうなりますよね。
数字を配列にせず、1を変数に代入したあと、繰り返しのなかで自己代入演算子を用いてひとつずつ数字を増やしていくパターンです。

パターン2

num = 1
while num <= 30 
  str = ""
  if num % 3 == 0
    str = str + "fizz"
  end
  if num % 5 == 0
    str = str + "buzz"
  end
  if str == ""
    str = str + num.to_s
  end
  puts str
  num += 1
end

こちらはelsifを使わないスタイルです。
空の変数を宣言し、各条件に応じてその変数に文字や数値を付け足しています。

パターン3

(1..30).each do |i|
 number = ""
 number = "fizz" if i % 3 == 0
 number << "buzz" if i % 5 == 0
 number = i unless i % 3 == 0 || i % 5 == 0
 puts number
end

こちらは配列になっているので、eachで繰り返しを行なっています。
それ以外はパターン2に似ていますが、文字列と数値をくっつけることを避けているため、to_sメソッドを使わずに済みました。

ただ、unlessはあまり使いたくないですね。

まとめ

私ごとですが、はじめパターン1の方法でfizzbuzz問題を解き、その模範回答がパターン2だったので、それを改良した(つもり)パターン3をつくりました。

もっとおすすめのfizzbuzzコードがあれば教えていただきたいです。

追加

コメントより教えていただいたパターンを追加します。

puts (1..30).map {|n|
  case [n % 3, n % 5]
  in [0, 0] then 'FizzBuzz'
  in [0, _] then 'Fizz'
  in [_, 0] then 'Buzz'
  else n
  end
}.join "\n"

ruby 2.7にするとパターンマッチングがつかえるようになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

js.erbファイルにコメントを書くときは//を使わない

修正前

index.js.erb
// コメントが見えてるよ!
$("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");

結果

ツールを使ってファイルを見ると...
(画像はChromeのデベロッパーツールのNetworkパネルから確認)
スクリーンショット 2019-05-24 14.01.35.png

コメントに重要な内容が含まれていると、敵の攻撃を手助けしてしまう可能性があり危険です。

修正

erbファイルのコメントの書き方に修正します。

index.js.erb
<%# コメントが見えないだと... %>
$("#ajax_panel").html("<%= escape_javascript(render partial: "ajax_panel") %>");

修正後

コメントが表示されなくなりました。
スクリーンショット 2019-05-24 14.04.56.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsアプリ作成手順まとめ

はじめに

備忘録としてまとめていきます。
随時追記していきます。

Railsプロジェクトを作成
$ rails new <プロジェクト名> -d <データベース>
ローカルリポジトリを作成、リモートリポジトリにプッシュ
$ git init
$ git add 
$ git commit -m "Initial commit"
$ git remote add origin https://github.com.ユーザー名.リポジトリ名
$ git push -u origin master
データベース作成
$ rails db:create
Herokuへデプロイ
$ heroku create
$ git push heroku master
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのCI環境でChildProcess::Errorが発生する場合の対処法

結構長いこと悩まされていたエラーだったのですが、原因がわかりましたので共有します。

CircleCIが安定しない

テストは全て通っているのですが、時々というか結構な頻度で以下のようなエラーが出てCIがコケていました。

/home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/abstract_process.rb:188:in assert_started': process not started (ChildProcess::Error)
6: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/platform.rb:150:in
block in exit_hook'
5: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:110:in stop'
4: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:110:in
ensure in stop'
3: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:163:in stop_process'
2: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/selenium-webdriver-3.141.5926/lib/selenium/webdriver/common/service.rb:180:in
process_exited?'
1: from /home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/unix/process.rb:31:in exited?'
/home/circleci/project_name/vendor/bundle/ruby/2.6.0/gems/childprocess-1.0.1/lib/childprocess/abstract_process.rb:188:in
assert_started': process not started (ChildProcess::Error)

このエラーメッセージ等でググっていたのですが、解決方法が見当たらず…。時々失敗するけれど時々成功するため、ときどき調査しようとしては諦めていました。

他のエラーメッセージに気づく

弊社の他のプロジェクトでも同様の症状が出ており、同僚が調査していたのですが、そのときに見つけたメッセージがこれ。

Text file busy - /home/circleci/.webdrivers/chromedriver

私はエラーメッセージのほうばかりを見ていて気づいてなかったのですが、RSpecが落ちたテストをリトライするところに出ていました。

こちらでググると、Rails 6.0系のissueとPRがヒットしました。

https://github.com/rails/rails/pull/36292

Rails 6系ではデフォルトで並列テストをサポートするという認識ですが、弊社のCIもparallel_testsでテストを回していて、同じ症状のようです。

症状の詳細は、webdriversがchromedriverのアップデートをしようとするが、並列でそれが行われてしまい、片方のプロセスが起動できなかったということです。

そこで、並列テストが実行される前にchromedriverのアップデートをしておけば、この問題は発生しなくなると考えました。(上記のPRもそういうことをやっていますが)

修正方法

並列テストが実行される前にchromedriverを更新するよう.circleci/config.ymlを修正しました。parallel_tests等でテストを起動するとその時点で並列化されているから、その前にやっておきます。

.circleci/config.yml
steps:
  # 略。ただし、DB作成後でないとrails runnerが失敗するので注意。
  - run:
      name: Update chromedriver
      command: env RAILS_ENV=test bin/rails runner "Webdrivers::Chromedriver.update"
  # 略。テストを実行

結果

10回連続で同じテストを実行しましたが、全部成功しました:v:

もし並列テストが不安定だ〜という方はこれを追加してみましょう!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyのString#%について

はじめに

以下のようなRubyのコードを見かけました。

sample.rb
names = ['Jon', 'Bob', 'Alice']
puts "Their names are %s, %s and %s" % names

#=> Their names are Jon, Bob and Alice

文字列中に含まれている%sやその後の%namesが何を意味しているのか分からなかったため、調べてみました。

String#%

リファレンスを確認したところ、Stringクラスのインスタンスメソッドで%がありました。
https://docs.ruby-lang.org/ja/2.5.0/method/String/i/=25.html

self % args -> String
printf と同じ規則に従って args をフォーマットします。
args が配列であれば Kernel.#sprintf(self, *args) と同じです。 それ以外の場合は Kernel.#sprintf(self, args) と同じです。

引数argsを変換してくれるようです。
文字列 % argsというのが最初に見たコードの

"Their names are %s, %s and %s" % names

の部分と一致してそうですが、イマイチよく分かりません。
printfと同じ規則に従ってとは?
printfって何だっけ?
ということで調べてみました。

printfとは

こちらもリファレンスを確認すると、Karnelモジュールのモジュール関数に定義されていました。
https://docs.ruby-lang.org/ja/2.5.0/method/Kernel/m/printf.html

C 言語の printf と同じように、format に従い引数を文字列に変換してportに出力します。

どうやらprintfは元々C言語の関数で、ある規則に従って引数を変換し、文字列として出力してくれるようです。C言語は全く分からないのでスルーします。

つまり、String#%はprintfで使われている規則に従って引数のargsを変換してくれるメソッドのようです。

では、printfで使われている規則とは何か?

sprintf フォーマットとは

String#%のリファレンスに規則について記載されていました。
Rubyの場合はC言語のものと少し違うが、ほとんど一緒のようです。
このsprintfフォーマットの中で指示子という項目があり、その中に文字列を表す指示子としてsがありました。

指示子は引数の型の解釈を示します。指示子を省略することはできません。 指示子には大きく分けて
・文字列を表す指示子: c, s, p
・整数を表す指示子: d, i, u, b, B, o, x, X,
・浮動小数点数を表す指示子: f, g, e, E, G
があります

こちらのサイトが分かりやすかったです。
http://www9.plala.or.jp/sgwr-t/c/sec05.html

まとめ

以上の知識を得てレベルアップしたため、最初に書いたコードを再度見てみると、以下の事が分かりました。

names = ['Jon', 'Bob', 'Alice']
"Their names are %s, %s and %s" % names
  • 二行目でString#%メソッドが使用されて、 self % args の形になっている。
  • 一行目で定義された配列 names が%メソッドの引数(args)になっている。
  • self(文字列)の中で文字列を表す指示子sが使用されている。

したがって、%メソッドの引数に指定された配列namesの各要素が、文字列中の%s(指示子)により、文字列として表示されているようです。(間違っていたらすいません)

おまけ

今回の例のように配列を引数にする場合、配列の要素数に対して文字列中の指示子の数が多いと、引数が不足しているというエラーになりました。

names = ['Jon', 'Bob', 'Alice']
puts "Their names are %s, %s, %s and %s" % names
# 配列の要素数は3に対して、%sを4つ使用しています。

#=> too few arguments (ArgumentError)

逆に、配列の要素数が多い場合は余剰分が無視されます。

names = ['Jon', 'Bob', 'Alice']
puts "Their names are %s and %s" % names
# 配列の要素数は3に対して、%sを2つ使用しています。

#=> Their names are Jon and Bob
#余計な存在の'Alice'は無視されます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RubyのJITコンパイラを理解したい!rubykaigiの香りを添えて

まえがき (ポエムなので読み飛ばせます)

image.png

「Rubyは遅い!」と言われがちですが、来たる Ruby3.0 に向けて、今までよりも3倍の高速化を図る試み、 Ruby 3x3が進行しています。
先月行われたrubykaigi2019でもその取り組みが発表され、多くの期待の眼差しが向けられていました。

とりわけ、高速化の肝になるであろう JITコンパイラはセッションだけでなくキーノートでも触れられ、大きな注目を集めていました。

しかし、電子計算機の仕組みを独学で(しかも基本情報技術者試験のため)ふわっと勉強した筆者(非情報系卒)にとっては、いささか難しい内容で、オープンされるスライドのたびに変わる会場の空気を「完全に理解した」とは口が避けても言えませんでした。

それでも、JITコンパイラをはじめとする数々の試みが、Rubyエンジニアにとって、とても興味深く、同時に楽しいことであることは十分に伝わってきました
今回はrubykaigi2019の残り香を楽しみつつ、多くのセッションでも取り上げたれたJITコンパイラについて、詳しく見ていきたいと思います。

この記事では、JITの概念をふわっと理解するために、ふわっと理解しようとしている筆者が書いています。
もし、誤りや怪しい部分がございましたら、忌憚ないご意見をいただければ幸いです! (>_<)

JITコンパイラ is なに?

JITコンパイラは Ruby2.6 からオプションで追加された、Rubyを高速に実行しようとする仕組みです。
JIT(Just-In-Time Compiler)とは、コードがまさに実行されるそのときにコンパイルされる仕組みのことで、RubyだけでなくJavaの実行環境でも取り入れられている仕組みです。

JITコンパイルという用語は、ソフトウェアを構成するモジュールやクラス、関数などの、ある単位のコードがまさに実行されるその時に、コンパイルされることから「Just In Time」の名前が付けられた
wikipediaより https://ja.wikipedia.org/wiki/%E5%AE%9F%E8%A1%8C%E6%99%82%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9

RubyのJITはMJITという名前がよく出ていますが、しくみを理解する上ではRubyのJITの実装周りの総称として覚えておいて良さそうです。
参考:https://k0kubun.hatenablog.com/entry/ruby26-jit

これまでとの処理の違い

Ruby1.9~Ruby2.5 ,デフォルト設定のRuby2.6での実行

スクリーンショット 2019-05-24 13.21.04.png

Rubyはインタープリタ型言語なので、コンパイルして直接機械語に変換されるわけではありません。どうやって実行されているというと、Rubyのコードは字句・構文解析を経てYARVバイトコードというものに変換されます。バイトコードはプログラム言語と機械語との中間にあたるようなコードです。

でもYARVバイトコードはあくまでバイトコードであって機械語ではないので、CPUはこの YARVバイトコード を直接解釈して実行することはできません。
そこで、CPUに変わってYARVバイトコードを解釈し、CPUに命令を発行してくれるのがバーチャルマシン(VM)であるYARVです。

こうしてRubyのコードは、明示的な機械語へのコンパイルを必要とせずに、実行することができます。

MJITを有効にしたRuby2.6

(これらの理解の拠り所として Cコンパイラを利用したRubyのJITコンパイラ を参考にさせていただきました。)

YARVバイトコード が生成されるところまではこれまでと変わりません。
そして、バーチャルマシンであるYARVもこれまで通り登場しますが、JITコンパイラーという役者が増えています。

image.png

あるプログラムが実行されたとき、YARV(以下VM) は生成された YARVバイトコードを解釈し、CPUが理解できる命令を発行し、実行してくれます。

ここで、あるメソッドが5回以上呼ばれたとします。そのときVMのスレッドは、JITのキューに、このメソッドを積みます。
JITはVMとは別のスレッドで動いて、積まれたメソッドをYARVバイトコードからCのコードに変換します。
生成されたCのコードはやがて機械語に変換され.soファイルが生成されます。
.soファイルの中身はバイナリコード(=機械語)です。これは動的にVMから呼ばれるようにリンクされます。

なので、もし次のタイミングでVMが処理しようとしているYARVバイトコードの中に、先ほどJITコンパイラが処理したのと同じメソッドがあった場合、
VMはYARVバイトコードを解釈してメソッドを実行するのではなく、機械語にコンパイル済みのメソッド.soファイルを関数ポインタを通じて読み込むことで、より高速に同メソッドを実行することができます。

現状での速さ/ベンチマーク

JITの仕組みによりRubyの高速化が図られたわけですが、いったいどれだけ早くなったのかというデータは検証方法によってバラツキがあるようです。

これはCコードを生成する際の最適化、Cコードから機械語に翻訳する際の最適化など、様々な要素が絡みあっているからのようで、単純に「何倍早くなった!」と言えるわけではないようでした。

また、JITを有効にしてRuby on Railsで作成したWebアプリケーションを実行すると、かえって遅くなるようなデータもrubykaigiでは示されていました。
ただ最新の実験ではJITを有効化しても、無効化時と同程度のスコアが出るようにはなったそうです....ここからが本番。(2019/4 rubykaigi)

(これは筆者の感覚的理解ですが、単純に処理が増えたのだからそれは遅くなっても仕方なさそうだし、プログラムの実行時間が長くなり機械語にコンパイル済みのメソッドが増えれば増えるほど高速化してゆくようなパラダイムでもあるし....納得、という感じです。)

さらに、JIT以外の速度改善についても、「rubyのインタープリタをrubyで書く」といった内容があり、非常に興味のそそるものでした。RubyKaigi 2019: Write a Ruby interpreter in Ruby for Ruby 3

おわりに

rubykaigiを振り返ると自分はいかにRubyを知らなかったのか思い知らされます。日常的にRailsに触っていると、Railsが行ってくれている魔術が当たり前になってしまうことがあったかもしれません。
一方、周りを見渡すと、Railsを使いながらもRubyで内製ツールを作って開発効率を上げている例が多くあることを知り、そういった活動が組織の技術力を作り、またOSSの活動へと広がってゆくことを実感しました。

Rubyというプログラミング言語を通し、技術に向き合うとうことを再確認したrubykaigiの3日間でした。

参考にた書籍/Webページ

Rubyのしくみ -Ruby Under a Microscope-
Ruby 2.6にJITコンパイラをマージしました|k0kubun's blog
プロと読み解く Ruby 2.6 NEWS ファイル|クックパッド開発者ブログ
Cコンパイラを利用したRubyのJITコンパイラ / Programming Symposium 60


LITALICOではエンジニアを積極採用中です。
新卒・第二新卒(未経験含)/中途採用、いずれも行なっていますので、ご興味のある方は下記URLをご確認ください。
https://www.wantedly.com/projects/309158

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

devise で基本的なアカウント機能のみを実装してみた

はじめに

アカウント機能を容易に実装することができる gem である devise の基本的な使用方法を
数回に分けて投稿します。

devise gem には様々な機能がありますが、
今回はあえて基本的なアカウント機能のみを実装してみようと思います。

動作対象
・Ruby 2.5
・Rails 5.1
・SQLite3

今回実装する機能
・アカウント作成機能
・ログイン及びログアウト機能
・アカウント編集機能

devise とは?

devise はログイン・ログアウト機能やアカウント作成機能などを簡単に実装できる gem です。
機能がモジュール化されているので、管理しやすいのが特徴です。
今回は2018/3/18にリリースされた4.4.3を使っていきます。

Rails のインストール

まずは雛形を作成しましょう。
私はいつもディレクトリを真っ先に作成して、そのディレクトリに移動して、
Gemfile を作成するという手順を取っています。

今回は devise_learning というアプリを開発していきます。

# 任意のディレクトリに移動して、devise_learning ディレクトリを作成する
$ mkdir devise_learning

# devise_learning ディレクトリに移動する
$ cd devise_learning

# Gemfile を作成する
$ bundle init

次に、Gemfile に今回使用する gem を貼り付けます。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'rails', '~> 5.1.5'
gem 'turbolinks', '5.2.0'
gem 'puma', '~> 3.7'

# database
gem 'sqlite3', '~> 1.3.6'

# devise
gem 'devise', '4.4.3'
# gem 'bcrypt', git: 'https://github.com/codahale/bcrypt-ruby.git', require: 'bcrypt'

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '3.1.5'
end

# windows 環境の方は以下のコメントを外してください
# gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

devise はパスワードを暗号化する際に bcrypt という gem を使用しています。
ruby のバージョンが 2.3 系の場合、あるいは Windows 環境で本アプリを作成する場合は、
デフォルトでインストールされる bcrypt が動作しない場合があります。
動作しない場合はこのコメントを外してください。

Gemfile
# gem 'bcrypt', git: 'https://github.com/codahale/bcrypt-ruby.git', require: 'bcrypt'

完了したら bundle install をしましょう。

$ bundle install

ここでアプリケーションの雛形を作成します。
既に gem はインストールしたので、-B を付属して bundle install をスキップします。

$ rails new ./ -B

Gemfile の対応について尋ねられると思いますが、もちろん n と答えてください。
アプリケーションの雛形が完成したら、とりあえずアプリを起動してみましょう。

$ rails server

「Yay! You’re on Rails!」と表示されていれば OK です。

devise のインストール

早速、devise を雛形にインストールしましょう。
rails g devise:install を実行すると devise の導入手順が出力されるので、これを元に進めていきます。

$ rails g devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

先ほどのコマンドで 2 つのファイルが作成されましたが、これらのファイルの使い方は後ほど解説します。
まずは 1 番目の説明を読んでみましょう。

devise の導入手順の和訳 (1)
環境ファイルにデフォルトの URL オプションが定義されていることを確認してください。
config/environments/development.rb の開発環境に適した default_url_options の例を次に示します:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

プロダクション環境では、host: はアプリケーションの実際のホストに設定する必要があります。

config/environment/development.rb に defalut_url_options を設定しましょう。
default_url_options については Rails チュートリアルの 11.2.2 でも取り扱われています。
私はローカル環境で開発しているので、devise の導入手順の通りに記述します。

config/environment/development.rb
Rails.application.configure do
  # .
  # .
  # .
  config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
  # .
  # .
  # .
end

メール認証機能は今回実装しないので、これ以上の設定は行いません。
終わったら 2 番目の説明を読んでみましょう。

devise の導入手順の和訳 (2)
config/routes.rb に何らかの root_url を定義していることを確認してください。
例えば:

root to: "home#index"

config/routes.rb に root_url を定義しましょう。
まずは controller を定義します。
今回は devise の導入手順に示されている例のとおり、
ホーム画面用の home コントローラと index ページを用意します。

$ rails g controller home index

次は config/routes.rb に root_url を設定します。
自動で作成されるルーティングを次のように書き換えてください。

config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end

以上で root_url の定義は完了です。
終わったら 3 番目の説明を読んでみましょう。

devise の導入手順の和訳 (3)
app/views/layouts/application.html.erb にフラッシュメッセージがあることを確認してください。
例えば:

<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

任意のフラッシュメッセージを定義していきましょう。
といっても、今回は app/views/layouts/application.html.erb に、
導入手順にかかれているコードをそのまま貼り付けてしまいます。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseLearning</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <div class="flash">
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
    </div>
    <%= yield %>
  </body>
</html>

bootstrap を使用したスタイルの調整などは今回行わないので、以上で任意のフラッシュメッセージの定義は完了です。
終わったら 4 番目の説明を読んでみましょう。

devise の導入手順の和訳 (4)
以下のコマンドを実行することによって、
Deviseビュー(カスタマイズ用)をアプリにコピーすることができます:

rails g devise:views

Devise ビュー(カスタマイズ用)をアプリにインストールしてみましょう。
導入手順に記されているコマンドを実行すると大量の Devise ビューが作成されます。

$ rails g devise:views
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_links.html.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/email_changed.html.erb
      create    app/views/devise/mailer/password_change.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

作成されたファイルで、ログインやアカウント作成時などで使用される初期ビューを変更することができます。
今回は変更しません。
以上で devise の初期設定は完了です。これでアカウント機能をもつテーブルを作成する手順が整いました。

devise を用いてアカウント機能を実装

devise を用いてアカウント機能をもつ users モデルを作成していきます。
devise でアカウントを持つモデルを作成する場合は次のコマンドを実行します。

$ rails g devise user
      invoke  active_record
      create    db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
      insert    app/models/user.rb
       route  devise_for :users

まずは、route devise_for :users からみていきます。

config/routes.rb が次の通りになっていることを確認してください。

config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'home#index'
end

devise_for :users というルートが設定されていることが確認できます。
どんなルートが作成されているのか確認してみましょう。

$ rails routes
                  Prefix Verb   URI Pattern                    Controller#Action
        new_user_session GET    /users/sign_in(.:format)       devise/sessions#new
            user_session POST   /users/sign_in(.:format)       devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format)      devise/sessions#destroy
       new_user_password GET    /users/password/new(.:format)  devise/passwords#new
      edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
           user_password PATCH  /users/password(.:format)      devise/passwords#update
                         PUT    /users/password(.:format)      devise/passwords#update
                         POST   /users/password(.:format)      devise/passwords#create
cancel_user_registration GET    /users/cancel(.:format)        devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)       devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)          devise/registrations#edit
       user_registration PATCH  /users(.:format)               devise/registrations#update
                         PUT    /users(.:format)               devise/registrations#update
                         DELETE /users(.:format)               devise/registrations#destroy
                         POST   /users(.:format)               devise/registrations#create
                    root GET    /                              home#index

devise_for :users は使用されているモジュールに応じて、devise の機能に必要なルートを設定します。
使用するモジュールは app/models/user.rb で設定します。
次は create app/models/user.rb をみてみましょう。
devise のモジュールはこのファイルで管理します。

app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
end

devise に実装されている各モジュールの機能をここに記載します。

devise modules 機能
database_authenticatable サイン時にパスワードを暗号化してDBに登録
registerable ユーザーが自身のアカウントの編集と削除を可能にする
recoverable パスワードのリセットを可能にする
rememberable Remember Me 機能を有効化する
trackable サインインの回数やIPアドレスなどを記録
validatable メールとパスワードのバリデーションを行う
confirmable メール認証機能を有効化
lockable 規定回数ログインに失敗したらアカウントをロックする
timeoutable 一定時間でセッションを破棄する
omniauthable Twitter や Facebook など、外部サービスのアカウントで認証を可能にする

今回は現時点で使用しないモジュールを全てコメントアウトします。

app/models/user.rb
class User < ApplicationRecord
  # Not use devise modules are:
  # :recoverable, :trackable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable, :rememberable, :validatable
end

使用するモジュールを 4 つに絞りました。
この 4 つのモジュールだけで Rails チュートリアルの 10 章 までの機能を実装することができます。
ここでもう一度ルートを確認してみてください。先ほど存在していたルートの一部がなくなっているはずです。

$ rails routes
                  Prefix Verb   URI Pattern               Controller#Action
        new_user_session GET    /users/sign_in(.:format)  devise/sessions#new
            user_session POST   /users/sign_in(.:format)  devise/sessions#create
    destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
cancel_user_registration GET    /users/cancel(.:format)   devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format)  devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)     devise/registrations#edit
       user_registration PATCH  /users(.:format)          devise/registrations#update
                         PUT    /users(.:format)          devise/registrations#update
                         DELETE /users(.:format)          devise/registrations#destroy
                         POST   /users(.:format)          devise/registrations#create
                    root GET    /                         home#index

次に進む前に、ログインやログアウトなどに必要なリンクを作成しておきましょう。
rails routes の出力からリンクを作成します。
app/wiews/layouts/_session.html.erb を新たに作成して次のように記述してください。

app/wiews/layouts/_session.html.erb
<% if user_signed_in? %>
  <p><%= link_to "アカウント編集", edit_user_registration_path %>
  <p><%= link_to "ログアウト", destroy_user_session_path, method: "delete" %></p>
<% else %>
  <p><%= link_to "ログイン", new_user_session_path %></p>
  <p><%= link_to "アカウント作成", new_user_registration_path %></p>
<% end %>

user_signed_in? はセッションを登録しているかどうかを真理値で返してくれる、
devise が提供するメソッドです。
user の箇所は作成したテーブル名で変化するので注意してください。

app/views/layouts/application.html.erb にこのパーシャルを追記しましょう。

app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>DeviseLearning</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <div class="flash">
      <p class="notice"><%= notice %></p>
      <p class="alert"><%= alert %></p>
    </div>
    <%= render 'layouts/session' %>
    <%= yield %>
  </body>
</html>

次は create db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb を見ていきましょう。
※ xxxxxxxxxxxxxxには作成日時が入ります。

このマイグレーションファイルには、アカウント機能で使用されるテーブルが用意されています。

db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      t.integer  :sign_in_count, default: 0, null: false
      t.datetime :current_sign_in_at
      t.datetime :last_sign_in_at
      t.string   :current_sign_in_ip
      t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

今回は app/models/user.rb で設定した 4 つのモジュールに必要なカラムだけを作成します。
現時点で使用しないカラムをコメントアウトしましょう。

db/migrate/xxxxxxxxxxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      # t.string   :reset_password_token
      # t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    # add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

この作業が完了したらデータベースを作成して、マイグレートしましょう。

$ rails db:migrate

db/schema.rb をみると、
アカウント機能を実現するのに必要な最低限のカラムが用意されていることがわかります

以上で基本的なアカウント機能は全て実装できました

「え?これだけ?」
はい、本当にこれだけです。
これだけでアカウント登録・編集・削除、ログイン・ログアウト、Remember Me 機能の実装が完了です。
サーバを起動してみて、実際に挙動を確認してみてください。

さいごに

今回は基本的なアカウント機能を devise を使って実装しました。
devise を扱う際、必要以上にカラムを作らないように気をつけましょう。

参照・参考

Rails チュートリアル 11.2.2
Rails チュートリアル 10章
STEP21:Rails5にdeviseでログイン機能を実装しよう! #Rails #Ruby | TickleCode

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

歴史から知る「なぜ絵文字対応は面倒なのか」

はじめに

企画さんから、
「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー:tada:→ハッピー*)」
という要望がきたので、それに対応するまでの流れをまとめたいと思います。(対応したとはとは言ってない)

Unicodeに絵文字が導入されるまで

こういった一部の文字をエスケープしたい場合、for文でcharをチェックするなり正規表現で弾くなりすると思いますが、絵文字の場合はそう上手くはいきません。

これには Unicode が絵文字を表示する仕組みが関係します。
絵文字が導入されるまでを時系列で振り返りながら、仕組みを知っていきましょう。

1990年代前半

制作当初、Unicode では1文字を2バイト固定で表現しようと計画されていました。
これによって、収録できる文字が最大65536文字になりますが、

  • Shift_JIS などの経験から可変長文字コードが面倒なことがわかっていたこと
  • 当時、中国・日本・韓国などの文字をまとめても20000個程度だったこと

などが理由で、問題ないだろうと判断されていました。
実際、Unicode 1.0 が策定された際は65536個以内に収まっていました。

これ以降、2バイト固定長、のちに UTF-16 と呼ばれるルールはC#やJavaなどのプログラミング言語に採用されるようになります。(char を2バイトとして扱う)

1990年代後半

当初、2バイト(65536文字)もあれば十分だろうと判断されていた Unicode ですが、技術者だけでなく言語学者も参画してもらったところ、65536文字では足りないことが明らかになります。

そこで、2文字1組(4バイト)として扱うサロゲートペアというルールが定められました。
これによって、扱える文字は増えたわけですが、当初定めていた2バイト固定長が崩れる形となり、UTF-16 は1文字2バイトまたは4バイトの可変長となってしまいます。

この結果、発生したのがUnicodeを使用したプロジェクトでのバグです。
当初、2バイトで1文字を扱っていたことで、サロゲートペアなどの4バイト文字がきた時に対処できなくなってしまいました。
しかし、サロゲートペアで表現する必要がある文字は一般人が知らないようなマイナー文字がほとんどだったため、特に対策されないプロジェクトが存在する形となりました。

2010年代前半

この状況に一石を投じたのが絵文字でした。

もともと日本の携帯電話などで浸透していた絵文字は、その扱いやすさから Emoji として世界でも扱われるようになります。(大統領からも Emoji を生み出したことに感謝の意を示されたらしいです)

絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。
これによって、今まで対応されていなかったマイナー文字も対応される形となり、結果的にバグ対策がなされる形となっていきます。

なぜ絵文字対応は面倒なのか

今回の記事のタイトルでもある、「なぜ絵文字対応は面倒なのか」
これは、

絵文字は4バイトで表現されますが、上記の通り世界各国で使用されているため、技術者はサロゲートペアに対応せざるを得なくなりました。

が理由となっていることがわかりました。
プログラミング言語的に置き換えると、charが2バイトで扱われている場合、絵文字などの4バイトは正しく1文字と判定されなくなるなどの問題が発生します。

なので、絵文字対応をするためには、この4バイト文字を正しく1文字扱いできればいいことがわかります。
幸い、IsSurrogateなどのサロゲートペアを検知する関数が各言語に存在するかと思われるので、そちらで判定すれば、上記の問題は解決します。

これで絵文字対応は完了...ということにはなりませんでした。
これには絵文字が導入されてからの歴史が関係します。

絵文字が導入されてから

もともとは日本の携帯電話だけにあったものが、iPhone の登場を筆頭に絵文字は世界的に人気となりました。
人気のコンテンツはより人の目を集め、それによって起こる問題も存在します。
主な例としては、人種差別やLGBT問題などが挙げられるでしょう。

参考記事:「絵文字に平等をサポートしてください」人種差別の指摘にゆれるUnicode

ただ、こういった「複数の肌色を用意してほしい」「男と男の絵文字も用意してほしい」といった要望は、日本の携帯電話での使用をベースに作られた絵文字には含まれていませんでした。

この解決策として、これまでの経緯を振り返ると、新たに4バイト文字として肌色別の絵文字を追加するという方法が考えられますが、これでは絵文字を検索する際に面倒になってしまいます。

そこで取られたのが、複数の絵文字を組み合わせて一つの文字を作るという方法でした。

複数の絵文字からなる絵文字

これまでの話では、たとえ1文字が2バイトであろうと4バイトであろうと1コードポイントで表現されていました。(異体字セレクタだと2コードポイントになりますが...)

ここでいうコードポイントとは、文字の集合の中でどこに配置されているかの位置になります。
これは、Unicode だと、U+○○○ のような形で表現され、単純計算で U+0000 ~ U+10FFFF が存在します。
例えば、「?」だと U+1F389 に該当します。

さて、先ほど挙げた「複数の絵文字からなる絵文字」ですが、これは複数のコードポイントから1文字として扱われます。

具体例を挙げます。
6AA54B84-84B9-403A-9C1F-6E46309D4F35.png

肌色違いなどの絵文字を表現する場合は、人の顔を表現する絵文字に色を表現する絵文字を追加することで表現できます。
また、この画像では2文字の絵文字を結合していますが、3文字4文字を結合して1文字を表現することもあります。(「?‍?‍?‍?」4人家族など)

この現象は簡単に確認ができます。実際に肌色違いを試してみましょう。

?+ ?=??

こちらは上記の画像の上の肌色違い生成式を文字に起こしたものです。
「?」をコピーして検索バーなどの適当の場所に貼り付けた後、「?」をコピーして貼り付けた後ろに「 ?」を貼り付けてください。
肌色違いが生成されたでしょうか?

このように、ある決まった文字が連続で続いた場合、1文字として扱う仕組みがあります。
それが書記素クラスタです。

なお、今回は絵文字を例に挙げていますが、書記素クラスタは「が」などにも利用されています。「か」に濁点の文字が結合されているのです。(1コードポイントの「が」も存在します)

# Ruby
irb(main):001:0> "が".length
=> 2

# C#
> WriteLine("が".Length);
2

Rubyはコードポイント、C#は2バイト単位でカウントするので、「が」は2バイト文字が2つ並んでいることがわかります。(U+304B U+3099

つまり、真に絵文字に対応するということは、こういった1コードポイントを超えた文字にも対応することになります。
これが面倒な理由になります。

各言語での挙動

1コードポイントを超えた文字に対応するためには、前述の通り書記素クラスタを用いる必要があります。
これが使われていなかったり、バージョン違いだったりすると正しく対応することができません。

言語別に挙動を見ていきましょう。

Ruby

まずは Ruby 2.3.8 を試してみます。

補足
Ruby のstring.lengthではコードポイントを数えてしまいます(例:肌色違いならU+1F471 U+1F3FBのため2文字扱い)。
なので、結合してできる肌色違いなどの文字を1文字扱いする正規表現\Xを使用しています。

irb(main):001:0> RUBY_VERSION
=> "2.3.8"
irb(main):002:0> "が".scan(/\X/).count
=> 1
irb(main):003:0> "?‍?‍?‍?".scan(/\X/).count
=> 7

どうやら正しく文字数をカウントすることができなかったようです。

これは Ruby 2.3.8 での正規表現の\Xがこれらの絵文字に対応していないことが原因となります。
\Xが絵文字に対応するのは Ruby 2.4.0 以降になります。

参考:https://docs.ruby-lang.org/ja/latest/doc/news=2f2_4_0.html

そのため、このバージョンで書記素クラスタを用いるためにはActiveSupportを使う必要があります。

irb(main):001:0> require 'active_support/multibyte/unicode'
=> true
irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("が").count
=> 1
irb(main):002:0> ActiveSupport::Multibyte::Unicode.unpack_graphemes("?‍?‍?‍?").count
=> 4

今度は、書記素クラスタを用いた文字数カウントになりますが、一部変更はかかっているものの、やはりおかしなカウントがされています。
これは、今回使用した ActiveSupport 5.0 が Unicode 8.0.0 以下のバージョンに基づいて文字数カウントを行なうためです。

正しくカウントするためには、Unicode 9.0.0 以降に対応した Ruby か ActiveSupport が必要になります。
なお、Unicode 9.0.0 に対応したのも Ruby 2.4.0 以降になります。

次は Ruby 2.6.2 での挙動を見ていきましょう。

irb(main):001:0> RUBY_VERSION
=> "2.6.2"
irb(main):002:0> "が".scan(/\X/).count
=> 1
irb(main):002:0> "?‍?‍?‍?".scan(/\X/).count
=> 1

Ruby 2.6.2 では正規表現\Xが絵文字に対応しており、Unicode 9.0.0 にも対応しているので、正しく絵文字をカウントできているようです。

C#

次は C# の挙動を見ていきましょう。

補足
C# のstring.Lengthでは char の数を数えています。(例:肌色違いならU+1F471 U+1F3FBで、どちらも4バイト文字なので、2+2で4文字扱い)
なので、4バイト文字や結合してできる肌色違いなどの文字を1文字扱いするStringInfoを使用します。

> System.Console.WriteLine(new System.Globalization.StringInfo("が").LengthInTextElements);
1
> System.Console.WriteLine(new System.Globalization.StringInfo("?‍?‍?‍?").LengthInTextElements);
7

結果が Ruby 2.3.8 で正規表現\Xを使用した時と同じ挙動ですね...
StringInfoのリファレンスを見てみましょう。

リファレンスを見た限り、特に絵文字に関する記述はありません。
ただ、

.NET Framework 4.6.2 では、文字の分類に基づくUnicode 標準、バージョン 8.0.0します。 .NET Framework 4.6.1 から .NET Framework 4 向けに基づくはUnicode 標準、バージョン 6.3.0します。 .NET Core でに基づくはUnicode 標準、バージョン 8.0.0します。

ということなので、どちらにせよStringInfoでも正しく絵文字を取り扱うことが難しそうです。

では、C#ではどうやって絵文字を扱うのか。
厳密にやるためには、外部のライブラリを頼る方法が一番かと思います。

++C++; // 未確認飛行 C さんのサイトで公開されているGraphemeSplitterを使うなどがいいかもしれません。

参考:https://ufcpp.net/blog/2017/10/graphemesplitter/

まとめ

絵文字の歴史と対応方法を紹介しました。
こういった絵文字の記事、閲覧環境によっては正しく表示されないことがあるので辛いところです...

おまけ

冒頭で企画さんから依頼されたもので、

「名前入力の際に絵文字を * でエスケープしてほしい(例:ハッピー:tada:→ハッピー*)」

こちらは最終的に、絵文字は入力を受け付けないという仕様に変更されたため、System.Globalization.UnicodeCategoryにて絵文字関連の文字は弾くという対応になりました。

今回はUnityでの絵文字対応で、最終的にはこういったコードに落ち着きました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class NewBehaviourScript : MonoBehaviour
{
    [SerializeField] InputField _inputField;

    private void Start()
    {
        _inputField.onValidateInput += ValidateInput;
    }

    private char ValidateInput(string text, int charIndex, char addedChar)
    {
        var category = char.GetUnicodeCategory(addedChar);
        switch (category)
        {
            case System.Globalization.UnicodeCategory.Surrogate:
            /*
            case System.Globalization.UnicodeCategory.Control:
            case System.Globalization.UnicodeCategory.OtherNotAssigned:
            など、その他必要なものがあれば追加
            */
                return '\0';
        }

        return addedChar;
    }
}

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

carrierwave+S3で本番環境への画像アップロード機能実装

はじめに

プロフィール画像をアップロードする機能をcarrierwavemini_magickfog-awsを用いてAWS S3にアップロードするまでの設定方法を書いていく。
本記事では前提としてユーザー管理にdevise、ビューにはSlimを使用しており、 AWS S3でバケットの作成が済んでいる状態で進める。

準備

あらかじめ本記事で用いるgemをインストールしておく。

Gemfile
gem 'carrierwave'
gem 'mini_magick'
gem 'fog-aws'
Terminal
bundle

また、Userモデルにプロフィール画像保存用のカラムとして、avatarカラムを追加しておく。
コマンドでbin/rails g migration AddAvatarToUsers avatar:stringを入力し、マイグレーションファイルを生成。

2019***********_add_avatar_to_users.rb
class AddAvatarToUsers < ActiveRecord::Migration[5.2]
  def change
    add_column :users, :avatar, :string
  end
end
Terminal
bin/rails db:migrate

手順

carrierwaveの設定

まずは画像のアップローダーを作成します。
コマンドでbin/rails g uploader Avatarと入力。
ここでは最低限の設定をしていきます。

app/uploaders/avatar_uploader.rb
class AvatarUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick

  # 環境毎の画像保存先
  if Rails.env.development?
    storage :file
  elsif Rails.env.test?
    storage :file
  else
    storage :fog
  end

  # S3のディレクトリ名
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # 許可する画像の拡張子
  def extension_whitelist
     %w(jpg jpeg gif png)
  end

  # 保存するファイルの命名規則
  def filename
     "#{secure_token}.#{file.extension}" if original_filename.present?
  end
end

また、user.rbに以下のコードを追記し、Avatarカラムとアップローダーを紐づけ。

user.rb
class User < ApplicationRecord
  mount_uploader :avatar, AvatarUploade

続いてconfig/initializers/carrierwave.rbを作成し、AWS S3の設定書いていく。
credentialの設定方法については、credentials.yml.encでシークレットキーを管理にまとめた。

config/initializers/carrierwave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
      aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
      #S3のリージョン #ap-northeast-1はアジアパシフィック(東京)
      region: 'ap-northeast-1'
    }
    # S3のバケット名
    config.fog_directory  = 'hogehoge'
    # S3に保存しておく期間
    config.fog_attributes = { cache_control: "public, max-age=#{365.days.to_i}" }
  end
end

ストロングパラメータの設定

avatarに要素が入った状態でUserモデルが更新されるのを許可するために、ストロングパラメータの設定をする。

application_controller.rb
  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
    devise_parameter_sanitizer.permit(:account_update, keys: [:username, :description, :avatar])
  end

ビューの設定

app/views/devise/registrations/edit.html.slim
 .circle-avatar.field
   label for="user_avatar"
     | プロフィール画像
   #img_field onclick="$('#file').click()"
     - if current_user.persisted? && current_user.avatar?
       = image_tag current_user.avatar.to_s
       = f.file_field :avatar, style: "display:none;"
     - else
       = image_tag "no_avatar.png"
       = f.file_field :avatar, style: "display:none;"

current_userdeviseの独自メソッド。
ログインユーザーがプロフィール画像を設定している場合はそれを表示し、設定していない場合に表示する画像ファイル(ここではno_avatar.pngはあらかじめ用意する
display:none;とすることで、プロフィール画像をクリックすると画像選択ができるようにする。

app/assets/stylesheets/users.scss
.circle-avatar.field img {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  object-fit: cover;
}

#img_field:hover {
  transition: 0.5s ease-out;
  opacity: 0.5;
}

プロフィール画像を丸く表示し、ホバー時に半透明になるよう設定する。

最後に選択された画像を表示するための設定をする。

users.js
$(document).on("turbolinks:load", function(){
  $fileField = $('#file')
  $($fileField).on('change', $fileField, function(e) {
    file = e.target.files[0]
    reader = new FileReader(),
    $preview = $("#img_field");

    reader.onload = (function(file) {
      return function(e) {
        $preview.empty();
        $preview.append($('<img>').attr({
          src: e.target.result,
          width: "100%",
          class: "preview",
          title: file.name
        }));
      };
    })(file);
    reader.readAsDataURL(file);
  });
});

以上でアップロード昨日の実装完了。
test1.gif

参考

【Rails5】Deviseのregistrations#editで画像をアップロードする
Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails6 のちょい足しな新機能を試す23(I18n fallbacks編)

はじめに

Rails 6 に追加されそうな新機能を試す第23段。 今回のちょい足し機能は、 I18n fallbacks 編です。
Rails 6.0 では、 config.i18n.fallbacks の設定で、明示的に I18n.default_locale を fallback として指定していないとDEPRECATION WARNING が出ます。fallbacks の挙動は、Rails 6.0 と Rails 6.1 で違いがあると思われます。

Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は gem install rails --prerelease でインストールできます。

$  rails --version
Rails 6.0.0.rc1

Rails プロジェクトを作る

$ rails new rails6_0_0rc1
$ cd rails6_0_0rc1

Controller と View を作る

今回はモデルなしで、試します。

$ bin/rails g controller i18n_fallbacks index

db:create をしておく

$ bin/rails db:create

development.rb に fallbacks の設定を追加する

config/environments/development.rb に fallbacks を設定します。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [{de: :ja}]
end

locale 変換用のファイルを用意する

英語(en)、日本語(ja)、ドイツ語(de) の3つを用意します。
1つの yml ファイルに全部の訳語が揃ってしまうと fallbacks の動作を確認できないので、揃わないようにします。

config/locales/en.yml
en:
  morning: "Good Morning"
config/locales/ja.yml
ja:
  afternoon: こんにちは
config/locales/de.yml
de:
  night: Gute Nacht

index.html.erb を編集する

本来、 with_locale は、ApplicationController で使うべきだと思いますが、今回は手抜きで View だけでやります。

app/views/i18n_fallbacks/index.html.erb
<% I18n.with_locale(:de) do %>
  <h1>I18n Fallbacks Test</h1>
  <h2>I18n settings</h2>
  <ul>
    <li>I18n.default_locale = <%= I18n.default_locale %></li>
    <li>I18n.locale = <%= I18n.locale %></li>
    <li>I18n.fallbacks = <%= I18n.fallbacks %></li>
  </ul>

  <h2>I18n translations</h2>
  <ul>
    <li>morning=<%= t(:morning) %></li>
    <li>afternoon=<%= t(:afternoon) %></li>
    <li>night=<%= t(:night) %></li>
  </ul>
<% end %>

rails server を実行してブラウザで表示する

rails server を実行して、 http://localhost:3000/i18n_fallbacks/index にアクセスします。
ログに DEPRECATION WARNING が表示されます。

DEPRECATION WARNING: Using I18n fallbacks with an empty `defaults` sets the defaults to include
the `default_locale`. This behavior will change in Rails 6.1. If you desire the default locale 
to be included in the defaults, please explicitly configure it with `config.i18n.fallbacks.defaults 
= [I18n.default_locale]` or `config.i18n.fallbacks = [I18n.default_locale, {...}]`. If you want 
to opt-in to the new behavior, use `config.i18n.fallbacks.defaults = [nil, {...}]`. 
(called from <main> at /app/config/environment.rb:5)

ブラウザの表示はこんな感じになります
2019-05-24-083631_383x299_scrot.png

fallbacks の設定を変更する

fallbacks の設定を変更してみます。明示的に I18n.default_locale を追加します。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [I18n.default_locale, {de: :ja}]
end

再度 rails server を起動し直して、ブラウザでページを表示すると今度は、 DEPRECATION WARNING が表示されません。
ブラウザの表示内容は変わりません。
DEPRECATION WARNING の意味するところは、「 I18n.default_locale を設定していなくても、Rails 6.0 では、 Rails内部で I18n.default_locale を追加するけど、Rails6.1 では挙動が変わるので、明示的に I18n.default_locale を追加するようにしてね」ということみたいです。

fallbacks の設定を再度変更する

DEPRECATION WARNING の最後に

If you want to opt-in to the new behavior, use `config.i18n.fallbacks.defaults = [nil, {...}]

とありますので、恐らくこれが、Rails 6.1 での挙動になると思われます。設定を変更して試してみます。

config/environments/development.rb
Rails.application.configure do
  ...
  config.i18n.fallbacks = [nil, {de: :ja}]
end

ブラウザの表示内容が以下のように変わります。
en (I18n.default_locale) が fallback のリストから消えています。
また、 morningGoog Morning に変換されていません。
2019-05-24-090101_369x290_scrot.png

fallbacks の設定を再度変更する

自動生成された config/environments/production.rb では、

config/environments/production.rb
  config.i18n.fallbacks = true

となっているので、 fallbacks が true のときにどうなるのか確認します。

このときは、 I18n.default_locale が fallbacks に含まれています。
2019-05-24-090406_349x282_scrot.png

まとめ

Rails 6.0 で DEPRECATION WARNING が出た場合は、 I18n.default_locale を設定するのが良さそうです。

試したソース

試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try023_i18n_fallbacks

参考情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails コマンドの実行の流れをたどる旅

Rails コマンド群がどのように呼び出されているのか気になったので、ソースコードを追いながらその流れを整理してみました。うーんとっても長い。

とっても長いので、ざっくり要約を載せておきます。

  • Rails コマンドはそれぞれコマンド名に対応したファイルを実行する
    • 例) rails generate → /rails/railties/lib/rails/commands/generate/generate_command.rb

もう、これだけ!後はホントに沼なので書く気にならん!w

さて、ここから先は自分の頭の中の思考をダダ漏らしにした感じでお送りするので、暇な方で覗いてみてください。

まずは rails コマンドの実行パスを確認します。こいつが何をしてるかっていう話ですよね。

$ which rails
=> /usr/local/bundle/bin/rails

実行パスが分かったので cat /usr/local/bundle/bin/rails を実行して中身を確認します。

/usr/local/bundle/bin/rails
#!/usr/bin/env ruby
#
# This file was generated by RubyGems.
#
# The application 'railties' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0.a"

str = ARGV.first
if str
  str = str.b[/\A_(.*)_\z/, 1]
  if str and Gem::Version.correct?(str)
    version = str
    ARGV.shift
  end
end

if Gem.respond_to?(:activate_bin_path)
load Gem.activate_bin_path('railties', 'rails', version)
else
gem "railties", version
load Gem.bin_path("railties", "rails", version)
end
  • Gem.activate_bin_path('railties', 'rails', version)
  • Gem.bin_path("railties", "rails", version)

このファイルでは上記のどちらかを実行して終わってるんですけど、どちらも同じ値 "/usr/local/bundle/gems/railties-5.2.3/exe/rails" を返してます。

Gem.activate_bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"
Gem.bin_path('railties', 'rails', '>= 0.a')
# => "/usr/local/bundle/gems/railties-5.2.3/exe/rails"

ということなので、このパスでロードして読み込まれるコードの中身を見てみます。

/railties/exe/rails
#!/usr/bin/env ruby
# frozen_string_literal: true

git_path = File.expand_path("../../.git", __dir__)

if File.exist?(git_path)
  railties_path = File.expand_path("../lib", __dir__)
  $:.unshift(railties_path)
end
require "rails/cli"
/railties/lib/rails/cli.rb
# frozen_string_literal: true

require "rails/app_loader"

# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
Rails::AppLoader.exec_app

require "rails/ruby_version_check"
Signal.trap("INT") { puts; exit(1) }

require "rails/command"

if ARGV.first == "plugin"
  ARGV.shift
  Rails::Command.invoke :plugin, ARGV
else
  Rails::Command.invoke :application, ARGV
end

ここでコメントアウトに

If we are inside a Rails application this method performs an exec and thus
the rest of this script is not run.

と書いてあるので、 Rails アプリケーション内から読み出した場合は、 Rails::AppLoader.exec_app 以降のコードは実行されないようです。ので、この #exec_app メソッドの中身を見てみます。

/railties/lib/rails/app_loader.rb
# frozen_string_literal: true

require "pathname"
require "rails/version"

module Rails
  module AppLoader # :nodoc:
    extend self

    RUBY = Gem.ruby
    EXECUTABLES = ["bin/rails", "script/rails"]
    BUNDLER_WARNING = <<EOS
…(長いので省略)…
EOS

    def exec_app
      original_cwd = Dir.pwd

      loop do
        if exe = find_executable
          contents = File.read(exe)

          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?("bin/rails") && contents.include?("This file was generated by Bundler")
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
            require File.expand_path("../boot", APP_PATH)
            require "rails/commands"
            break
          end
        end

        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) && return if Pathname.new(Dir.pwd).root?

        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir("..")
      end
    end

    def find_executable
      EXECUTABLES.find { |exe| File.file?(exe) }
    end
  end
end

ここちょっとビックリしたんですけど、 extend self を書いとくと自身に特異メソッドを生やすことできるっぽい。だから Rails::AppLoader.exec_app を直接実行できるのね。へー…。

ここでは、 EXECUTABLES = ["bin/rails", "script/rails"] このどちらかのパスが存在したらそれを実行というロジックになってます。 bin/rails の中身を見てみます。

/path/to/work_dir/bin/rails
#!/usr/bin/env ruby
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

みたところ、 APP_PATHENGINE_PATH がなかった場合でも定数 APP_PATH の初期化と config/application.rbconfig/boot.rb の読み込み、そして最終的に rails/commands を読み込んでいるっぽい。

/railties/lib/rails/app_loader.rb
Object.const_set(:APP_PATH, File.expand_path("config/application", Dir.pwd))
require File.expand_path("../boot", APP_PATH)
require "rails/commands"

ということで、 rails/commands の中身を見てみます。

/railties/lib/rails/commands.rb
# frozen_string_literal: true

require "rails/command"

aliases = {
  "g"  => "generate",
  "d"  => "destroy",
  "c"  => "console",
  "s"  => "server",
  "db" => "dbconsole",
  "r"  => "runner",
  "t"  => "test"
}

command = ARGV.shift
command = aliases[command] || command

Rails::Command.invoke command, ARGV

うおお、ここで Rails コマンドのエイリアスが出てきたぞ…。ここで ARGV に含まれていた値を取り出して、それを引数に Rails::Command.invoke command, ARGV を実行してます。

ここで言う ARGV は「 Ruby スクリプトに与えられた引数を表す配列」とのことで、実行ファイル名より後の値を配列でファイル内に渡してます。
https://docs.ruby-lang.org/ja/2.6.0/method/Object/c/ARGV.html

たとえば、

$ rails generate model

の実行時の引数 ARGV は以下のように保存されます。

['generate', 'model'] 

さて、 Rails::Command.invoke command, ARGV の中身を見てみます。
たとえば、 rails generate model を実行する場合、

full_namespace
# => 'generate'
args
# => ['model']

がそれぞれ代入されているはずです。
なお、これ以上の処理は、すべて以下のコマンドを実行する場合を前提とします。

$ rails generate model Foo foo:string

よし、 #invoke メソッドの中身見ていきます。

/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end

前段にいろいろとコマンド名の処理がありますが、最終的には command.perform(command_name, args, config) の中身が分かればいいので、まずは変数 command に代入している #find_by_namespace メソッドを見ます。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

冒頭 3 行の処理で変数 lookups には以下の値が代入され、 #lookup メソッドに渡されます。

lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]

それでは #lookup メソッドの中身を見てみます。どうやらここでは namespaces を元に各コマンドの処理が入ってるファイルを require してるようです。なるほど、メソッド名通りの処理だ。

/railties/lib/rails/command/behavior.rb
def lookup(namespaces)
  paths = namespaces_to_paths(namespaces)

  paths.each do |raw_path|
    lookup_paths.each do |base|
      path = "#{base}/#{raw_path}_#{command_type}"

      begin
        require path
        return
      rescue LoadError => e
        raise unless e.message =~ /#{Regexp.escape(path)}$/
      rescue Exception => e
        warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
      end
    end
  end
end

ここでは、 #namespaces_to_paths / #lookup_paths / #command_type 3 つのプライベートメソッドが呼ばれています。各メソッドの中身は単純なので割愛しますが、それぞれ以下の値を返します。

namespaces_to_paths
# => ["generate/generate", "generate", "generate/generate/generate", "rails/generate/generate", "rails/generate", "rails/generate/generate/generate"]
lookup_paths
# => ["rails/commands", "commands"]
command_type
# => "command"

ほんで、 require が true を返すのは "rails/commands/generate/generate_command" の時ですね。なんなんだろう、すごいトリッキーなことやってる気がするw

そうこうして #find_by_namespace メソッドに戻ってきました。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

さて、次に実行される #subclasses メソッドが謎を呼ぶんですが、中身はこうなっています。

def subclasses
  @subclasses ||= []
end

これ pry でデバッグ中に試しに実行してみると分かるんですが、なんともう値 [Rails::Command::GenerateCommand] が入ってます。い、いつ代入されたの…??????って話なんですが、実は require path の時点で代入されていました。

そもそも xxxx_command.rb の中身はざっくり以下のような構成になっており、必ず Rails::Command::Base クラスを継承するようになっています。

module Rails
  module Command
    class HogehogeCommand < Base
    end
  end
end

この < Base のタイミングで Rails::Command::Base.inherited が実行されます。メソッドの中身を見ると分かりますが、 @subclassesbase が追加されてるのが分かるかと思います。

module Rails
  module Command
    class Base < Thor
      class << self
        
        def inherited(base) #:nodoc:
          super
          if base.name && base.name !~ /Base$/
            Rails::Command.subclasses << base
          end
        end
     …
      end
    end
  end
end

ソースコードリーディングっていろんなファイルを飛び回るからアタマ混乱するなあ…。
さて、その @subclasses に #index_by をかけて Hash 化します。

/railties/lib/rails/command.rb
def find_by_namespace(namespace, command_name = nil) # :nodoc:
  lookups = [ namespace ]
  lookups << "#{namespace}:#{command_name}" if command_name
  lookups.concat lookups.map { |lookup| "rails:#{lookup}" }

  lookup(lookups)

  namespaces = subclasses.index_by(&:namespace)
  namespaces[(lookups & namespaces.keys).first]
end

ここで最後の処理に使う namespaceslookups の値を確認しておきましょう。現時点でこんな感じになっています。

namespaces
# => {"rails:generate"=>Rails::Command::GenerateCommand}
lookups
# => ["generate", "generate:generate", "rails:generate", "rails:generate:generate"]

なので、最後の行の返り値はこんな感じになります。

namespaces[(lookups & namespaces.keys).first]
# => Rails::Command::GenerateCommand

やっと #find_by_namespace(namespace, command_name) の返り値がわかったので、 #invoke メソッドに戻ります。今まで見てきたのは変数 command にどんな値が代入されるかということでした。上記で見た通り、 Rails::Command::GenerateCommand が代入されることが分かりました。

/railties/lib/rails/command.rb
def invoke(full_namespace, args = [], **config)
  namespace = full_namespace = full_namespace.to_s

  if char = namespace =~ /:(\w+)$/
    command_name, namespace = $1, namespace.slice(0, char)
  else
    command_name = namespace
  end

  command_name, namespace = "help", "help" if command_name.blank? || HELP_MAPPINGS.include?(command_name)
  command_name, namespace = "version", "version" if %w( -v --version ).include?(command_name)

  command = find_by_namespace(namespace, command_name)
  if command && command.all_commands[command_name]
    command.perform(command_name, args, config)
  else
    find_by_namespace("rake").perform(full_namespace, args, config)
  end
end

仮に command が nil だった場合は Rails::Command::RakeCommand を元に rake コマンドが走るようですが一旦それは置いておきます。 command.perform(command_name, args, config) の中身を見ていきます。なお引数の値はこんな感じになっています。

command_name
# => "generate"
args
# => ["model"]
config
# => {}
/railties/lib/rails/command/base.rb
def perform(command, args, config) # :nodoc:
  if Rails::Command::HELP_MAPPINGS.include?(args.first)
    command, args = "help", []
  end

  dispatch(command, args.dup, nil, config)
end

ここの #dispatch は Rails ではなく Thor のメソッドです。Thor 内部の動きについてはまた色々ありますが、ここでは割愛します。なんだかんだあった後に Rails::Command::GenerateCommand#perform メソッドが呼び出されます。

/railties/lib/rails/commands/generate/generate_command.rb
def perform(*)
  generator = args.shift
  return help unless generator

  require_application_and_environment!
  load_generators

  ARGV.shift

  Rails::Generators.invoke generator, args, behavior: :invoke, destination_root: Rails::Command.root
end

さて、ここから沼に入っていきます。しばらく自分がどこにいるか分からなくなります。ちょっと最後の方まで書いてたんですが説明するのがめんどくさくなったので、興味のある方は覗いてみてください。 Rails って大きいなあ(小並)ってのがひしひしと分かります。

要するに Rails コマンドとそれを実行するファイル名は対応していて、 rails generate なら generate_command.rb、 rails console なら console_command.rb を見に行けばなんとなく中で何をやっているかが分かる、というしくみです。
いつもやっているソースコードリーディングの思考をそのまま書いたみたいな感じでまとまりないのは申し訳ないですが、備忘録程度に書いてるので、まあそんなもんだと思ってください。

Thor について(おまけ)

なお、各コマンドのオプションについては、 class_option で大半を定義しています。これは Thor の機能で http://whatisthor.com/#class-options 、Thor クラスを継承したクラスにこのメソッドを書くと、クラス全体で定義しておきたいオプションを設定することができる、というものです。

たとえば、

sample.rb
#!/usr/bin/env ruby
require 'thor'

class Nya < Thor
  class_option :nyanchu, type: :boolean, default: false

  desc 'hello NAME', 'say hello to NAME'
  def hello(name)
    if options[:nyanchu]
      puts "#{name}, nyanchu~!"
    else
      puts "#{name}, nya~!"
    end
  end
end

Nya.start(ARGV)

としておくと、勝手に [--nyanchu], [--no-nyanchu] オプションをつけてくれるようになります。コマンド名を指定せずにファイルを実行すると、よく見かける README を出力してくれます。なにこれ便利。

root@10161f5ac926:/hoge# ./bin/sample
Commands:
  sample hello NAME      # say hello to NAME
  sample help [COMMAND]  # Describe available commands or one specific command

Options:
  [--nyanchu], [--no-nyanchu]  

なので、このオプションにしたがって以下のように実行してみると、ちゃんと引数を解釈してくれます。

root@10161f5ac926:/hoge# ./bin/sample hello Waku --nyanchu
Waku, nyanchu~!

もっと詳しく Thor について知りたい方は公式ドキュメントか GitHub のリポジトリを見に行っても良いかもしれません。
http://whatisthor.com/
https://github.com/erikhuda/thor

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む