20191224のRubyに関する記事は26件です。

EC2でsassc gemがインストール アップデートできなかった

解決策

古いgccを捨てて新しいgccをインストールする

yum remove gcc libstdc++48-devel
yum install gcc72 gcc72-c++ libstdc++72

事象

lto1: fatal error: LTO_tags out of range: Range is 0 to 365, value is 51456
compilation terminated.
lto-wrapper: g++ returned 1 exit status
/usr/bin/ld: lto-wrapper failed
collect2: error: ld returned 1 exit status
make: *** [libsass.so] Error 1

make failed, exit code 2

Gem files will remain installed in /home/enechange/enechange/rails/vendor/bundle/ruby/2.6.0/gems/sassc-2.2.1 for inspection.
Results logged to /home/enechange/enechange/rails/vendor/bundle/ruby/2.6.0/extensions/x86_64-linux/2.6.0/sassc-2.2.1/gem_make.out

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

In Gemfile:
  activeadmin was resolved to 2.4.0, which depends on
    sassc-rails was resolved to 2.1.2, which depends on
      sassc

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

Railsアプリ、EC2の本番環境でMySQLを直接いじる方法

自分の作ったアプリを本番環境で起動させた時に、エラーが出てしまうことはあると思います。
currentフォルダ内のlogを見て、エラーを発見したとしても、それがMySQLのエラーだと直接コマンド打たないと行けないので、修正が結構めんどくさいんですよね!

そこで、よく使うMySQLのコマンドをまとめました!

ログイン

$ mysql -u root -p
Enter password: パスワードを入力(実際に文字は見えません)

データベースの確認

show databases;

以下のように出てきます

+----------------------------------+
| Database                         |
+----------------------------------+
| information_schema               |
| xxxxxxxxxxxxxxxxxx|
| mysql                            |
| performance_schema               |
+----------------------------------+

いじるデータベースの選択

上のコマンドで確認したものの中から選びます

use データベース名;

これが出ればOKです

Database changed

テーブル一覧表示

show tables;

データの検索

とりあえずテーブルの中身を全部見たい場合は

SELECT * FROM テーブル名;

データの削除

僕がよく使うのはこれ、とりあえずエラー引き起こすデータは消してしまえ精神

DELETE FROM テーブル名 WHERE id = 数字;

補足

この方法はあくまで、直接いじらないと直せないエラーなどを解決する方法です!
Railsの場合は、カラムの追加やテーブルの新規作成などは
マイグレーションファイルを使っていつものようにやれば大丈夫です。

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

キーワード引数というコントリビュートチャンス

Ruby2.7へ向けて

そろそろRuby2.7の季節がやってきましたね!
すでにアップデート準備はすませましたでしょうか?

Ruby3へ向けた移行パスの一環として、Ruby2.7からキーワード引数の仕様が変更されます。
これは多少の破壊的な変更があり、実際にRubyのアップデートを考えるときには悩みどころとなりそうです。

ここでは実際に手元のアプリケーションをRuby2.7.0-rc2に移行する際に遭遇したケースをヒントに、自分なりに想定できる対処方法などを記してみます。

※ なおキーワード引数の新仕様や、その具体的な挙動については、こちらに素晴らしくわかりやすい解説記事があるので参考にしてみてください。

https://tmtms.hatenablog.com/entry/201912/ruby27-keyword-argument

https://qiita.com/jnchito/items/c15ac23791e0320e0fc2

なおこの移行に伴い、たくさんのgemにおいてコントリビュートチャンス祭りが発生すると思われます
Rubyエンジニアの皆様にはぜひぜひこの機会を生かしていただければと思っています!

なぜ変更されるのか

(私自身はただの傍観者なのですが)、外野から理解できている範囲では、Rubyコアコミッターであるmametterさんが2018年ごろから精力的に提案されていた問題提起がきっかけとなっているのかなと思います。

詳細は上にも書いた素晴らしい記事があるので省略しますが、ここにある3種類のポイントが主に解決すべき問題ということのようです。

非常に雑にアウトラインをまとめると、

  • 現在のキーワード引数には、いくつかのケースにおいてbuggyな挙動がある
  • これらは現在の仕様に全体的な整合性がないことに起因している
  • キーワード引数の現仕様になんらかの破壊的変更をして新仕様に移行するしか、これらの問題を解決できない

という話のようです。

該当チケットを追いかけると、変更のインパクトをなるべく最小限に抑えつつ(下方互換を最大限に保ちつつ)、全体的に整合性のある仕様にする(さらにRuby2.7において適切なマイグレーションパスをつくる)ことにむけた議論がなされています。

Ruby仕様に精通したコミッターチームであっても、これはかなり難解な挑戦になっていたようで、様々なアイデアや非常に細かいコーナーケースの検証を経てようやく着地したのが現在の仕様のようです。(チケット上で紹介されているこちらの資料には、新仕様の方向性についてのわかりやすいまとめがあります)

仕様の勘所

具体的には冒頭でリンクした良記事や、公式リリースにおける解説を参考にしていただくのがよさそうです。

大きくはRuby3におけるキーワード引数の勘所として、以下のことを理解しておくといいかと思います。

前提

  • メソッドは以下の3種類の引数を持てる
    • ポジショナル引数 ・・・ 位置関係によってきまる(順序を変更すると異なる引数になる)
    • キーワード引数  ・・・ キーワードラベルによって指定できる(キーワードの順序を変更できる)
    • ブロック引数   ・・・ 1つだけブロックを受け取れる

この中で、主にキーワード引数の受け渡しに関連して、以下のように仕様が整理されました。

仕様A(大原則)

  • キーワード引数を受け取るメソッドに対して、キーワード引数を渡す際には、ハッシュオブジェクトではなくキーワード引数として渡さなければならない
    • Ruby2.6までは、ハッシュオブジェクトを(ポジショナル引数の最後にセットして)渡すと、キーワード引数として解釈してくれた
    • Ruby2.7ではポジショナル引数の最後にハッシュオブジェクトを渡すと警告付きでキーワード引数として解釈する
    • 来年に出る予定のRuby3からは、ハッシュオブジェクトを渡すとポジショナル引数として解釈される

仕様B(例外ルール)

  • キーワード引数を受け取らないメソッドに限っては、キーワード引数を渡した場合に、ハッシュオブジェクトに変換したものをポジショナル引数として受け取れる
    • このケースについてはRuby2.6以下と下方互換性が保たれる

仕様C

  • キーワード引数のキーは、シンボル以外のオブジェクトでも受け付ける(Ryby2.7以降の新仕様)

何が困るのか

主に、キーワード引数が必要なメソッドにハッシュオブジェクトを渡しているパターンがあります。
たとえばRailsなんかでよくあるコントローラーだと、ハッシュを別のメソッドに引数として渡す局面があります。

def create
  user = assign_attributes(user_params.to_h.merge(role: :admin)) # 呼び出し側はハッシュオブジェクトを渡す
  user.save!
end

private
def assign_attributes(**params) # 受け取り側はキーワード引数を要求
  User.new **params 
end

これに対して警告が出ます(Ruby3でエラーになる予定)

warning: The last argument is used as keyword parameters; maybe ** should be added to the call
warning: The called method `assign_attributes' is defined here

対策としては、

def create
  user = assign_attributes(**user_params.to_h.merge(role: :admin)) # **演算子でハッシュオブジェクトを開く(キーワード化する)
  user.save!
end

ということが必要になります。

実際にアプリケーションで使用していたActsAsTenantというgemにおいてこのタイプの警告が出ていたので、簡単なパッチを書きました

delegationに気をつけろ

さて、この手の警告に関して少し対処が難しいケースがdelegationです。

ここでいうdelegationとは、「あるメソッドが受け取った引数(の全部または一部)を引数として別メソッドを呼び出す」というパターンを指します。

たとえば典型的にこういうコードがあります。

class APIWrapper
  def execute(*args)
    ExternalAPIClass.new.delegated_method(*args) # 丸投げする
  end
end

ExternalAPIClass#delegated_methodがキーワード引数をとるメソッドだった場合、これでは正しく呼び出すことができなくなります。
(Ruby2.7では警告付きで動きますが、Ruby3系で死ぬことになります)

いくつか対処法がありますが、Ruby2.7以降を使う場合は

class APIWrapper
  def execute(...)
    ExternalAPIClass.new.delegated_method(...)
  end
end

引数を「丸投げ」する記法、...で対応できます。

ただし、この...だと選択的に一部の引数だけ取り出す、ということができません。たとえば・・

class APIWrapper
  def execute(*args, external: true)
    if external
      ExternalAPIClass.new.delegated_method(*args) # 丸投げする
    else
      # ...snip
    end
  end
end

この局面では...で一部の引数を引き取ることができないため、Ruby2.7以降で動かしたい場合は明示的に

class APIWrapper
  def execute(*args, **kwargs, external: true)
    if external
      ExternalAPIClass.new.delegated_method(*args, **kwargs) # 丸投げする
    else
      # ...snip
    end
  end
end

という対処が必要になります。

なお、ここでいうdelegationとは「委譲と継承」という文脈における委譲ではありません。
つまり継承においてもこの問題が起こる可能性があります。(実際にあるgemで踏みました)

class ParserBase
  def parse!(*args, failure: :exception)
    # ...snip
  end
end

class Parser < ParserBase
  def parse!(*args)
    super # 暗黙のうちにargsを渡している
  end
end

このようにdelegationが絡むと問題が発生することがあるのでアップデート時には気をつけてください。

難しいケース

通常のアプリケーションにおいては、「RubyアップデートのPRで、全てのコードをRuby2.7仕様に変更」という対処が可能です。

ただ様々なRubyバージョンで利用されるgemについては、現在のバージョンでの後方互換を保ちつつ、新バージョンにも対応できるパッチを入れたい、ということで少し難しい対処が必要になることがありそうです。

複数のRubyバージョン対応を意識したdelegation

これが現時点わかっている限り、かなり難しい対処を要求されそうなパターンです。

詳細の議論は省きますが、この記事に非常に良いまとめがあるので一読をオススメします。

https://eregon.me/blog/2019/11/10/the-delegation-challenge-of-ruby27.html

(なお記事で触れられているpass_keywordsというアイデアはおそらくなくなったようで、Ruby2.7以降においては、ruby2_keywordsRuby2.6 EOLの時点までの期間限定で導入されるという方向になった模様です)

※ なおRuby2.6以下での対処について、Ruby2.6以下にバックポートされると書きましたが、実際はバックポートされないとのことです。旧バージョンでも対応するためには、下のコード例にあるように自前でruby2_keywordsを定義するか、ruby2_keywords gemを利用することで対応することになります 1

対処例

このサンプルは、上の記事から引用しています。
もしwrapper_methodというメソッドが(引数を丸投げした上で)target_methodというメソッドを呼び出しているとして・・・

以下のようなコードを書くことで、Ruby2.6までの挙動を担保しつつ、今回のRuby2.7や、Ruby3(とくにRuby2.6がサポート対象外となるとされるRuby3.2系以降の世界)においても、意図通りに「引数を正しく丸投げするdelegation」を実現ことができそうです。

def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true)

if RUBY_VERSION < "3"
  ruby2_keywords def wrapper_method(*args, &block)
    target_method(*args, &block)
  end
else
  def wrapper_method(*args, **kwargs, &block)
    target_method(*args, **kwargs, &block)
  end
end

なおこのRUBY_VERSIONを元にした条件分岐ですが、Ruby2.6がEOLになった未来(Ruby3.2登場のタイミング?)において、「ruby2_keywordsという(醜い)移行用メソッドは消滅させる」ということをmatzが明言しているために存在しています。
もし「Ruby3.1あたりでRuby2.6の下方互換を捨てる」という判断であればこの分岐は必要なさそうです。(その時点でelse節だけにすればよい)

実世界での対処

すでに一部のgem、たとえばrails本体ではこのメソッドをつかった数々の改修がなされているのを見ることができます。他にもdry-rbにおいてもこの手の改修を見ることができますね。

言いたいこと

ぜひ手元のアプリケーションをRuby2.7にアップデートして、警告を注視してください。

最初のうちは、アプリケーションだけでなくgem関連でも警告が出ると思います。これらが全て改修対象になります。

我々のような一開発者でも、ガシガシとコントリビュートできるこの大チャンスを生かさない手はない!(鼻息)

みんなであちこちのgemをアップデートして、きたるRuby3へ向けてスムーズな移行ができる世界を目指せるといいですね。

(蛇足ながら、もし記事の誤りに気づかれたら、コメント欄などでご指摘いただければ助かります)

最後に

この記事を書くにあたって、この二年間の様々な努力の跡を眺めながら巨人の足跡をたどらせていただきました。

様々な問題を根絶すべく、この難解な仕様変更を根気よく詰めてこられたRubyコミッター陣にはただただ頭が下がります。
またいち早くこの難しい変更を取り込み、移行の努力をされているRailsやRubocopなど著名gemのメンテナーの方々には感謝しきれません。

ありがとうございます。

検証環境

この記事では、手元の(Rails)アプリケーションに試験的にruby2.7.0-rc2を投入して、Ruby2.7.0-rc2へのアップデートにトライしています。

Ruby2.6.5でRSpecがオールグリーンの状態から出発して、Ruby2.7.0-rc2において、キーワード引数関係の警告がどうなるかを検証し、いくつか引っかかったケースを元に対処法を記事にしています。
もともとはRails6.0.2系のアプリケーションでしたが、キーワード引数の変更に対応させるためRailsバージョンは2019/12/21時点のmasterまで引き上げています。


  1. コミッターのn0kadaさんが指摘されているツイートを偶然目にして、記事を訂正させていただきました。ありがとうございます。 

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

データベース--メモ

DBでよく使う型

string 文字列型
text テキスト(不定長文字列)型
integer 整数型
float 浮動小数点数型
decimal 固定長整数型
datetime 日時型
timestamp タイムスタンプ型
time 時刻型
date 日付型
binary バイナリ文字列型
boolean 真偽値型
references 他のテーブルへの外部キーの定義、_id が付いた整数

電話番号の型を設定するとき

integer型だと10桁まで。電話番号は11桁の時もある。なのでDBに登録できないことがある。
また、09012345678の場合、最初の0が消えることがある。

なので string型をおすすめします。
string型の文字列だとハイフン(ー)もDBに保存ができる。

def change
create_table :addresses do |t|
t.references :user, foreign_key: true, null: false
t.string :post_code, null: false
t.string :prefectures, null: false
t.string :city, null: false
t.string :address, null: false
t.string :building_name
t.string :tel_no
t.timestamps
end
end

参考記事

https://qiita.com/Yinaura/items/cede8324d08993d2065c
https://chinatz.hatenablog.com/entry/2018/07/20/210000

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

Ruby testコマンド じゃんけん

じゃんけんのプログラムのテストを実行

1.テストする、じゃんけんのプログラム

参照
https://qiita.com/ktpnobu/items/a8525f8cceddd486c219

#Rock-Paper-Scissors
#0-1-2

p 'rock-paper-scissors'
player=gets

a=0


if player=="rock"
    player_hand=0
    a=1
elsif player=="paper"
    player_hand=1
    a=1
elsif player=="scissors" 
    player_hand=2
    a=1
end


program_hand=rand(3)


if program_hand==0
    program="rock"
elsif program_hand==1
    program="paper"
elsif program_hand==2
    program="scissors"
end


if a==0
    p 'one more please'
if player_hand==program_hand
    'draw'
elsif (player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 and program_hand==1) 
    'you win'
else 
    'you lose'
end

p "player",player,"program",program

このプログラムの勝敗の部分について、test実行

2.test/unit

test/unitの読み込み

require 'test/unit'

3.引数から答えをだす関数を定義←これが正しいのかテストする

今回、じゃんけんの勝敗のテストなので、二つの手を引数とし、勝敗が正しく表示されるか

class Rock_paper_scissors
     def self.hantei(player_hand,program_hand)
            #ここにプログラムを書く
     end
end

player_handとprogram_handの2値が決まれば、正しい勝敗が出る(であろう)関数である
↑これが本当に正しいのかをtestするイメージ

関数の中身

if player_hand==program_hand
    'draw'
elsif (player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 and program_hand==1) 
    'you win'
else 
    'you lose'
end

↑player_handとprogram_handの2つから、結果を出すもの
上二つをまとめると、

class Rock_paper_scissors
     def self.hantei(player_hand,program_hand)


if player_hand==program_hand
    'draw'
elsif (player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 and program_hand==1) 
    'you win'
else 
    'you lose'
end

     end
end

4.検証プログラム

2つの引数からでた値は本当に正しいのか検証するために、
すべての出力とその結果を書いたプログラムを作る

class TestRock_paper_scissors < Test::Unit::TestCase
  def test_Rock_paper_scissors
    assert_equal('draw', Rock_paper_scissors.hantei(0, 0))
    assert_equal('draw', Rock_paper_scissors.hantei(1, 1))
    assert_equal('draw', Rock_paper_scissors.hantei(2,2))
    assert_equal('you win', Rock_paper_scissors.hantei(1, 0))
    assert_equal('you win', Rock_paper_scissors.hantei(2, 1))
    assert_equal('you win', Rock_paper_scissors.hantei(0, 2))
    assert_equal('you lose', Rock_paper_scissors.hantei(2, 0))
    assert_equal('you lose', Rock_paper_scissors.hantei(0, 1))
    assert_equal('you lose', Rock_paper_scissors.hantei(1, 2))
  end
end

5.まとめ

require 'test/unit'

class Rock_paper_scissors
def self.hantei(player_hand,program_hand)



if player_hand==program_hand
    'draw'
elsif (player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 and program_hand==1) 
    'you win'
else 
    'you lose'
end

end
end

#Rock-Paper-Scissors
#0-1-2

class TestRock_paper_scissors < Test::Unit::TestCase
  def test_Rock_paper_scissors
    assert_equal('draw', Rock_paper_scissors.hantei(0, 0))
    assert_equal('draw', Rock_paper_scissors.hantei(1, 1))
    assert_equal('draw', Rock_paper_scissors.hantei(2,2))
    assert_equal('you win', Rock_paper_scissors.hantei(1, 0))
    assert_equal('you win', Rock_paper_scissors.hantei(2, 1))
    assert_equal('you win', Rock_paper_scissors.hantei(0, 2))
    assert_equal('you lose', Rock_paper_scissors.hantei(2, 0))
    assert_equal('you lose', Rock_paper_scissors.hantei(0, 1))
    assert_equal('you lose', Rock_paper_scissors.hantei(1, 2))
  end
end

6.参考文献

https://qiita.com/repeatedly/items/727b08599d87af7fa671#assert%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9

https://teratail.com/questions/197215

http://portaltan.hatenablog.com/entry/2015/10/28/111757

https://teratail.com/questions/164134

https://teratail.com/questions/51977

https://qiita.com/kazuki0714/items/b54d006af7672bb54eac

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

世の中の1%の人の為になるかもしれないRails tips

はじめに

本記事はZeals Advent Calendar 2019の24日目です。メリークリスマスイブ!

本記事では僕が日々開発しているなかで、どうしてもなんとかしなければいけない…という時に活用したtipsをまとめたものです。
タイトルにある通り、エッジケースすぎるのでもし同じような状況で困っている人の為になれたら幸いです。

環境

Rails 5.2.3
Ruby 2.5.5

tips

といっても今回紹介するのは2つです

  • カラムにaliasをかける
  • caller

カラムにaliasをかける

Railsではcolumnに対してaliasを貼ることができます。

モデル名 カラム名
Button name
Choice label

このようなモデルが2つあるとします。
カラム名はそれぞれ違いますが同じものとして扱いたい時があると思います。
そんな時はalias_attribute が使えます。

class Choice < ActiveRecord::Base
  alias_attribute :name, :label

alias_attributeを使うことでChoiceのlabelnameとして扱うことができるようになります。
ただnameとして扱うことができるようになるだけではなく

Choice.first.name?
=> true

といったActiveRecordの述語メソッドやゲッターやセッターを生成してくれます。
ただ同名として扱いたいだけで、述語メソッドを生成するまでもない場合は以下のようにシンプルに定義するだけで良さそうです。

class Choice < ApplicationRecord
  def name
    label
  end

カラム名を変更したい。変更したほうが良い。でもrename_columnしてる時間ないよぉ…
という時に使いました、便利ですねー

caller

モデル名 カラム名
User cliend_id
モデル名 カラム名
Answer client_id
モデル名 カラム名 カラム名
UserAnswer user_id answer_id

このような中間テーブルがあるとします。
以下はmodel

class UserAnswer < ApplicationRecord
  belongs_to :user
  belongs_to :answer

  validates :should_be_same_client

  def self.import_associations!(users, answer)
    associations = users.map do |user|
      UserAnswer.new(user_id: user.id, answer_id: answer.id)
    end
    UserAnswer.bulk_import! associations, validate: false
  end


  def should_be_same_client
    return user.client == answer.client

    errors[:base] << ' client_id of user and client_id of answer are different'
  end
end

定義されている
import_associations!は引数としてuserの配列と指定のanswer(回答)を受け取ります。

カスタムのvalidationメソッドが定義されており、UserAnswerClientに対してbelongs_toの関係で、client_idが違うものが作成されないようにしています。

should_be_same_clientはUserAnswer単体を生成する時は動いてほしいものですが、bulk importで作成したい時はvalidationをskipしたい。カスタムvalidationも動いてほしくないとします。

Railsのversionは5.2系なので(早く6に上げたい…)bulk importはactiverecord-importを利用します。

bulk_importはoptionでvalidate: falseを渡すことでmodelのvalidationをskipさせることができます。
しかし今回定義したようなカスタムvalidationはskipしてはくれません。
bulk importする件数が数件であれば都度カスタムvalidationが走ったところで問題はないのですが、件数が多くなればなるほどN+1で都度カスタムvalidationが走るにより実行時間がどんどん長くなるので避けなければなりません。

つまりbulk_importを呼び出すメソッドのときのみカスタムvalidationをskipする必要があります。

そこでcallerを使いました。
callerはバックトレースの情報を確認することができるので、どのメソッドから呼び出されたのか,どのメソッドを経由してきたのかを確認することができます

callerで呼び出されたバックトレースを正規表現を使って、メソッド名だけを見れるようにします

caller.map { |c| c[/`([^']*)'/, 1] }
=> ["eval",
 "evaluate_ruby",
 "handle_line",
 "block (2 levels) in eval",
 "catch",
 "block in eval",
 "catch",
 "eval",
 "block in repl",
 "loop",
 "repl",
 "block in start",
 "__with_ownership",
 "with_ownership",
.......

これでどのメソッドを経由してきたのかがわかります。
取得したバックトレースないにvalidateをskipしたいメソッドが含まれているかを確認するメソッドを追加し、

def call_from_import_associations?
  caller.map { |c| c[/`([^']*)'/, 1] }.include?('import_associations!')
end
validates :should_be_same_client, unless: call_from_import_associations?

カスタムvalidationの発火条件として追加し、import_associations!から呼び出されるときのみカスタムvalidationが発火しないようにすることができます。

まとめ

実際にこのユーザーに対してtagを一括でつける機能は使用頻度の高い機能だったので、早急に修正する必要があり、本当になんとかするためにひねり出した苦肉の策ではあります…w

早急に対応しなければいけない。なんとかしなければいけない。という同じような問題に詰まっている人たち、もしくは同じようなケースで困っているの助けに少しでもなっていれば幸いです!

明日はついにAdvent Calendarの最終日です!
最終日は@pannpersです!
それでは良いクリスマスイブを!

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

Youtuber向けにサービス開発したが需要が無かった話

はじめに

プログラミング始めて半年くらいで自分の好きなタイ在住日本人Youtuberに使ってもらいたくて開発した時の話です。結論から言うと上手くいかなかったです。その原因としてリサーチ不足と自己満の開発であったからです。今後サービス開発をする人にとって今回の自分の経験が役立てばいいなと思い、今回のことの経緯をまとめておきます。

結論

・競合サービスのリサーチが大切
・ユーザーにしっかりとリサーチすることも大切
・自己満のサービスになっていないか

何を作ったのか

私は1つのMAP上でYoutuberは撮影した場所にピンをさすことができ、そこをクリックすると詳細情報が見れて最終的にはYoutubeチャンネルに移動する。そして視聴者はYoutuberに撮影して欲しい場所に対してピンを挿してリクエストできる双方向コミュニケーションサービスを開発しました。

難易度

サービス自体はRuby on Railsで開発を行いProgate卒業すれば作れるような簡単なものでした。
MapにはGoogle map APIを使用しました。
そして機能自体もシンプルだったので開発自体はそれほど時間がかかりませんでした。

実際に提案してみて

この時の最大にミスとしては自分が作りたいものを作ってから、これYoutuber向けに改良すれば良いだろうと仮説を立て開発してから売り込んだことです。そのため世界的な大企業が競合となるとも知らずに自信満々にサービスの提案をYoutuberの方に行いました。

結果

・Google MapにあるMy Mapと言う似たような機能使っているが、それとは何が違うか聞かれ答えられなかった。
→正直自分でもGoogle Mapで十分だと感じました。
・Youtuberがこのサービスを「使うメリットが全くなかった。

そこからの改善

せっかく作ったので諦めたくないなと思い、Youtuberの方にヒアリングなどを行い何か改善点はないか探りました。その結果1つだけありました。

それは視聴者の目線に立つことでした。

そのYoutubeチャンネルの視聴者は日本人だけではなく、タイ人の方もおられました。
そのことに関してリサーチを行った結果、Yotuberの方はタイ人視聴者からの撮影して欲しいリクエストが多くあるみたいですが、詳細情報が聞けずに諦めていたようです。さらに視聴者としてもここを動画にして欲しいなどの思いがあったみたいです。ここから私はタイ人視聴者と日本人Yotuberがコミュニケーションできるサービスに方向転換をしました。

最終結果

最終的には良いサービスじゃん!とYoutuberの方に言って頂きました。
そしてリリースに向けて準備していたときに、Youtuberの方が解散をされてしまいました、、、
しかもやりとりをしていた方が脱退という形だったので大打撃でした。
結局その後いろいろありましたが、幻のサービスとして世に出ることはありませんでした。

学び

結果的には失敗だったのですがたくさんの学びがありました。
それはサービスを開発する上でユーザーの立場になって考えるのは大切ということです。
こんな技術を使いたい!これ面白そう!って言うのが理由で開発も素晴らしいです。しかし誰かに使ってもらう前提で作るなら、徹底的にリサーチを行い他に似たサービスがないか調査するのは必要だなと思います。

これは今後開発以外の場面であっても使えそうな考え方かなと思います。

最後に

今回かなり短い文面にはなりましたが最後までみていただきありがとうございます。
インターネットに関して全くの知識がなかった自分がサービスを作れるようになったと考えると感慨深いです。テクノロジーを使えば世の中にとって役に立つことを生み出すことができます。
しかし役に立つものしか生み出してはいけないかと言われると違うと考えてます。
意味のわからない、誰が使うかもわからないサービスであっても誰かの役に立つかもしれません。

今回リサーチは大事や自己満で作るな。どと言っておきながら、
最後に矛盾したことを言いますが楽しかったら全てokです!

メリークリスマス!!

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

Railsチュートリアルを必要コマンドだけをまとめて記しました。第1章

        Railsチュートリアル  
対象者 中級者向け(最低でもRuby, HTML/CSS, Railsの基礎)
何ができるか 本格的なアプリを開発でき、最終的にWeb上に公開できる
開発環境 AWS Cloud9 or ローカル環境

1.2.2 Railsをインストールする

リスト 1.1: バージョンを指定してRailsをインストールする
$ gem install rails -v 5.1.6
リスト 1.3: rails newを実行する (バージョン番号を指定)
$ cd ~/environment
$ rails 5.1.6 new hello_app

cdはchange directly(このディレクトリに移動する)
〜はホームディレクトリ(自分のアカウントのデフォルト)

1.3.1 Bundler

リスト 1.5: Ruby gemごとにバージョンを明示的に指定したGemfile(リスト 1.4のGemfileをリスト 1.5に置き換えます。)
$ c9 open Gemfile
source 'https://rubygems.org'

gem 'rails',        '5.1.6'
gem 'puma',         '3.9.1'
gem 'sass-rails',   '5.0.6'
gem 'uglifier',     '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks',   '5.0.1'
gem 'jbuilder',     '2.6.4'

group :development, :test do
  gem 'sqlite3',      '1.3.13'
  gem 'byebug', '9.0.6', platform: :mri
end

group :development do
  gem 'web-console',           '3.5.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.0.2'
  gem 'spring-watcher-listen', '2.0.1'
end

# Windows環境ではtzinfo-dataというgemを含める必要があります
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

チュートリアルではRailsを学ぶために勉強しているので、
リスト 1.4: hello_appディレクトリにあるデフォルトのGemfile をリスト 1.5のように固定します。

アプリケーションのGemfileの内容をリスト 1.5で置き換えたら、bundle installを実行してgemをインストール
$ cd hello_app/
$ bundle install

bundle installを実行すると、先程全部のVerを固定するというGemfileの内容を書き換えたので、その固定されたVerだけを取って来てくれます。


Bundler could not find complete versions for gem "activesupport":
In snapshot (Gemfile.lock):

In Gemfile:
rails (=5.1.4) was resolved to 5.1.4, which depends on railties (>= 4.0.0) was resolved to 5.1.6, which depends on activesupport(=5.1.6)
coffee-rails (= 4.2.2) was resolved to 4.2.2, which depends on railties (>= 4.0.0) was resolved to 5.1.6, which depends on activesupport(=5.1.6)

Running 'bundle update' will rebuild your snapshot from scratch, using only the gems in your Gem file, which may resolve the conflict.

ただ実行するとほとんどの場合がうまく動かず、先程あなたはrails 5.1.6を入れたのに5.1.4を入れようとすると依存関係がごちゃごちゃになるから困っちゃった、とbundleから言われます。
「直したいんだったら”bundle update”を実行してください。実行するともう一回Gemfileを一から読み直してくれるので直るんじゃないですか」というのが最後の行に書かれています。

$ bundle update

bundle updateを実行したことにより、全てのgemのVerが揃い、これでrailsが立ち上がります。ここまで実行したことはつまり、リスト1.3のrails newコマンドとリスト1.5のbundle installコマンドを実行したことにより、実際に動かすことのできるアプリケーションが作成されたということです。

1.3.2 rails server

リスト 1.6: Railsサーバーを実行する
$ cd ~/environment/hello_app/
$ rails server

railsサーバーが立ち上がったか確認するために、Preview画面をクリックして、Preview Running Applicationをクリックします。
altアプリケーションをブラウザで開く
altrails serverを実行したときのデフォルトのRailsページ
alt

c9コマンドをインストールするコマンド
$ npm install -g c9

npmはnode.jsに於ける管理ツールでc9はnpmの一環として管理されているので、npmによりc9コマンドが入れられるようになります。

リスト 1.15: クラウドIDE上でHerokuをインストールするコマンド
$ source <(curl -sL https://cdn.learnenough.com/heroku_install)
herokuが正しくインストールできていれば、バージョン番号が表示されるようになります
$ heroku --version
heroku-cli/6.15.5 (linux-x64) node-v9.2.1

1.3.3 Model-View-Controller (MVC)

alt

railsの中にはModel、View、Controllerがあり、上手く作用しています。ソースコードが煩雑にならず、3つそれぞれに切り替えられます。

1.3.4 Hello, world!

リスト 1.7: Applicationコントローラにhelloを追加する
$ c9 open app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  def hello
    render html: "hello, world!"
  end
end

renderは”描画する”という意味です。

リスト 1.9: ルートルーティングを設定する(リスト 1.8から1.9に書き換えます)
$ c9 open config/routes.rb
Rails.application.routes.draw do
  root 'application#hello'
end

routeとrootを区別するため、訳文ではrouteを基本的に「ルーティング」と訳します。「config/routes.rb」をRailsのルーティングファイルと呼びます。
「application#hello」は、Applicationコントローラファイルの中にあるhelloメソッドを呼び出してくださいという意味です。
先程の”Yay! You’re on Rails!”はrootURLで、
したがって、「root 'application#hello'」は、rootURLにアクセスが来たら、Applicationコントローラファイルのhelloメソッドを呼び出してくださいということになります。
リスト 1.9の後、rails serverを実行すると、「hello, world!」と表示されます。

1.4 Gitによるバージョン管理

次は「hello, world!」を開発環境のcloud9じゃなくて本番まで持っていきましょう。本番環境は「heroku」を使いますが、「heroku」という本番環境に自分たちの”hello_app”を載せるためにはGitというソフトウェアが必要になります。
Gitはゲームでいうセーブポイントで、ボス戦が入る前とかにセーブして負けたとしても、ロードしてやり直せる。ような感じです。普通のゲームと違うのはゲームだとセーブポイント10個くらいに収まるが、Gitの場合は1000個とかになリます。

ここからこのGitを使ってセーブやロードができる環境を作る初期化を行うコマンド
$ git init
今Gitに入っていないファイルがこれだけあるよというのが確認できる
$ git status
セーブする対象を定義する
$ git add -A
確認したらこれまで行ったものをセーブする
$ git commit

git commitを実行すると、nanoかVimが出て来ます。
nano から vim に変更するには、こちらの記事を参考に以下のコマンドを実行します。
$ git config --global core.editor 'vim -c "set fenc=utf-8"'
Vimの場合はAなどを押すと、「INSERTモード」になり、後から見て思い出せるようにコメントしておきます。
alt

この後、保存してエディタから出るには、ESCキーを押して、最初のモードに戻り、「:wq」を押します。
「:」はこの後コマンド入力する、「w」はコメントを書き込む、「」はこのエディタから出る、という意味です。

この日にコミットしたコミットlog(これまでのセーブしたプログラム)を見ることができる
$ git log

ただこれらのGitは開発環境の中でしかファイルやセーブデータが見れないため、例えば、アカウントを消してしまうと、このGitのセーブポイントのデータが消えてしまいます。ですので、万が一のために、基本的に作ったGitのレポジトリは自分たちの開発環境とは別のところに置きます。なので、何らかの理由で開発環境が使えなくなったとしても、そのサーバーに残しておけば、みんなで見れてすぐに再現できます。
そういったサーバー(レポジトリ置き場)で有名なのがGitHubです。

1.5 デプロイする

リスト 1.13: 追加や並び替えを行ったGemfile(Gemfileをリスト 1.13に書き換える)
$ c9 open Gemfile
source 'https://rubygems.org'

gem 'rails',        '5.1.6'
gem 'puma',         '3.9.1'
gem 'sass-rails',   '5.0.6'
gem 'uglifier',     '3.2.0'
gem 'coffee-rails', '4.2.2'
gem 'jquery-rails', '4.3.1'
gem 'turbolinks',   '5.0.1'
gem 'jbuilder',     '2.7.0'

group :development, :test do
  gem 'sqlite3', '1.3.13'
  gem 'byebug',  '9.0.6', platform: :mri
end

group :development do
  gem 'web-console',           '3.5.1'
  gem 'listen',                '3.1.5'
  gem 'spring',                '2.0.2'
  gem 'spring-watcher-listen', '2.0.1'
end

group :production do
  gem 'pg', '0.20.0'
end

# Windows環境ではtzinfo-dataというgemを含める必要があります
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

「sqlite3 gem」は開発環境用のDBのGemですが、「heroku」は本番用のサーバーなので、「sqlite3」だとデプロイに失敗してしまいます。なので、「development」では「sqlite3 gem」ですが、「production」では「pg gem」(PostgreSQL)という本番環境用のものを使います。

Gemfileを書き換えた後、bundle installを実行する
$ bundle install
ローカルの場合は、本番環境で使わないGemは入れないようにbundle install --without productionを実行する
$ bundle install --without production

production環境で行うGemは開発環境で使わないからwithoutで外に置いといて、インストールしないようにする

Gitを通してHeroku上にデプロイするため、Gitにコミット(セーブ)する必要があります
$ git add Gemfile Gemfile.lock

先程、bundle installを実行した結果、”$ git status”で確認すると、GemfileとGemfile.lockはmodifiedになっているため、git addで追加します。

またエディタが出てきて面倒なため、「git commit」に「-m(message)」を追加すれば一行で終わります
$ git commit -m "Add pg gem"

git commit -m "Add pg gem"を実行した後、commitしましたというエディタ画面が出ますが、「J」と「K」で上に行ったり、下に行ったりして、「Q」で終了できます。

herokuにログインする
$ heroku login --interactive
パスワード入力をスキップするための秘密鍵
$ heroku keys:add

? Would you like to upload it to Heroku? (Y/n)と出てきたら「Y」を入力します。

本番用のサーバーを作ります
$ heroku create
Creating app... done, ⬢ afternoon-sea-23202
https://afternoon-sea-23202.herokuapp.com/ | https://git.heroku.com/afternoon-sea-23202.git

heroku createを実行すると、2つのURLが出てきて、左側のURLをクリックしてOpenを選択すると開けます。

git remote -vを実行すると、先程作ったサーバーにソースコードを送るためのショートカット(エイリアス)、Herokuができます
$ git remote -v

herokuという送り先を入力すると、サーバーにソースコードが送れます。

今いるmasterというブランチのソースコードをherokuに送る(git push)
$ git push heroku master 

これで先程のURLをリロードすると「hello world!」が表示されます。

1.5.4 Herokuコマンド

アプリケーションの名前を変更
$ heroku rename rails-tutorial-kyotot 

Railsガイド

alt

この記事はYassLabさんの解説動画を参考にさせていただきました。とてもわかりやすく、Railsチュートリアルに心折れそうな方にすごくオススメです!

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

ドロップダウン機能を有効にするため、Railsのapplication.jsファイルを通して、Bootstrapに同梱されているJavaScriptライブラリとjQueryを読み込むようアセットパイプラインに指示

railsチュートリアルで、Accountをクリックしてプルダウンメニュー出すために、少し手こずったのでメモ。

実装させたい機能⬇️
fullsizeoutput_b8c.jpeg
application.jsに2つのライブラリを追加。

application.js
//= require rails-ujs
//= require jquery     ⇦追加
//= require bootstrap   ⇦追加
//= require turbolinks
//= require_tree .

fullsizeoutput_b8b.jpeg
と、Sprockets::FileNotFoundエラーが出る。

Gemfileにgemを追加。

gem 'rails', '~> 5.2.4', '>= 5.2.4.1'
gem 'bcrypt',         '3.1.12'
gem 'bootstrap-sass', '3.3.7'
gem 'jquery-rails', '~> 4.3', '>= 4.3.1'   ⇦追加

追加後bundle installとrails sでサーバー再起動。
fullsizeoutput_b8d.jpeg
無事に、機能実装完了。

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

Railsで作成したアプリケーションにRspec(E2Eテスト)実装してみた

はじめに

本記事は、Railsのテストフレームワーク(Rspec)の手順をまとめたものになります。
また、当記事は以前の投稿記事「Railsでセルフパスワード変更ページを作ってみた」で作成した環境を前提に作成していますので、参考にされる方はご注意ください。

前提条件

  • 開発環境はCloud9
  • Linuxコマンドの使い方がわかる程度の力量
  • Web開発は初学者クラスの力量(ruby on rails開発未経験/Progateのレッスンは修了済)
  • Rspec初学者クラスの力量

参考記事

RSpecとCapybaraを使ってE2Eテストの土台を作ってみる
RailsでRSpecの初期設定を行う際のテンプレートを作ってみる
Rspecの設定はrails_helper.rbにだけ書けばよい
RSpecの初期設定メモ
Railsでrspecを使うように設定する
実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド
RSpec 設定
RSpecコトハジメ ~初期設定マニュアル~
使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
既存のRailsプロジェクトをRSpec 3.0にアップグレードする際の注意点 ~RSpec 3は怖くないよ!~
特定のSpecでだけトランザクションのロールバックを無効にする
rspec-rails 3.7の新機能!System Specを使ってみた
SeleniumからHeadless Chromeを使ってみた
Amazon LinuxでSelenium環境を最短で構築する
RSpecの(describe/context/example/it)の使い分け
RSpec の letとlet!とbeforeの挙動と実行される順番
Ruby on Rails アプリケーションにおけるモンキーパッチの当て方【外部サイト】
rails generate rspec:install時に生成されるhelperの設定【外部サイト】
RailsでのRSpec実行時乱数の状態を実行毎に変わらないように固定する【外部サイト】
Rspec入門編ーテストコードを書いてみよう!ー【外部サイト】
RSpec 3 時代の設定ファイル rails_helper.rb について【外部サイト】
RSpec 3.5 から shared_context の使い方が少し変わっていた [RSpec]【外部サイト】
RSpecのshared_contextで共通処理を1ヶ所にまとめる【外部サイト】

必要ソフトウェア

  • ブラウザエミュレート環境インストール
    • chrome(ブラウザ)
    • GConf2(chromedriver用)
    • chromedriver
  • gemのインストール
    • rspec-rails
    • webdrivers
    • selenium-webdriver
    • capybara

ディレクトリ構成

今回のプロジェクトでは下記ディレクトリ構成となります。

本プロジェクトのディレクトリ構成
spec
├── controllers
│   └── users_password_controller_spec.rb
├── helpers
│   └── capybara.rb
├── lib
│   └── ssh_interactive_spec.rb
├── rails_helper.rb
├── spec_helper.rb
└── system
    └── users_password_spec.rb

Selenium環境構築

chromeインストール

現在の環境がyumで新しいchromeがインストールできないので別の方法でインストールを実施します。

chromeインストール
$ sudo curl https://intoli.com/install-google-chrome.sh | bash

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  9526  100  9526    0     0  14260      0 --:--:-- --:--:-- --:--:-- 14239
Working in /tmp/google-chrome-installation

【以下省略】
Extracting graphite2...
Successfully installed google-chrome-stable, Google Chrome 78.0.3904.108 .

余談ですが、chromeをアンインストールしたくなりましたら下記コマンド実行してください。

chromeアンインストール
$ sudo yum --setopt=tsflags=noscripts -y remove google-chrome-stable
$ sudo rm -rf /opt/google/chrome/

chromedriverインストール

chromeインストール
$ sudo yum -y install GConf2
Loaded plugins: priorities, update-motd, upgrade-helper
You need to be root to perform this command.

【以下省略】

Installed:
  GConf2.x86_64 0:2.28.0-7.el6                                                                                                                                                                                                 

Dependency Installed:
  ConsoleKit.x86_64 0:0.4.1-6.el6      ConsoleKit-libs.x86_64 0:0.4.1-6.el6      ORBit2.x86_64 0:2.14.17-7.el6     dbus-glib.x86_64 0:0.86-6.10.amzn1     eggdbus.x86_64 0:0.6-3.el6     libIDL.x86_64 0:0.8.13-2.1.4.amzn1    
  polkit.x86_64 0:0.96-11.el6_10.1     sgml-common.noarch 0:0.6.3-33.5.amzn1    

Complete!

gemインストール

Gemfileへ設定を追記します。

Gemfile追記
$ cd PasswordChange
$ vim Gemfile

【変更前】
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

【変更後】
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'selenium-webdriver'
  gem 'webdrivers'
  gem 'capybara'
  gem 'rspec-rails'
end

bundleインストールを実行します。

bundleインストール
$ bundle install

RspecとCapybara設定の追加

Rails用のRspec初期ファイルを作成します。

Rspec初期ファイル作成
$ bundle exec rails g rspec:install
      create   .rspec
      create  spec/spec_helper.rb
      create  spec/rails_helper.rb

余談ですが、railsに組み込まないで使用する場合、下記コマンドでRspec初期ファイルを作成できます。

普通のRspec初期ファイル作成
$ bundle exec rspec --init
  create   .rspec
  create   spec/spec_helper.rb

Capybara用のファイルを作成します。

Capybaraファイル作成
$ mkdir -p spec/helpers

$ touch spec/helpers/capybara.rb

Capybaraの設定を追加します。

Capybara設定追加(capybara.rb)
require 'capybara/rspec'
require 'selenium-webdriver'

RSpec.configure do |config|
    config.include Capybara::DSL

    # javascript無
    config.before(:each, type: :system) do
        driven_by :rack_test
    end

    # javascript有
    config.before(:each, type: :system, js: true) do
        driven_by :selenium_chrome_headless, screen_size: [1280, 800], options: {
           browser: :chrome
        } do |driver_option|

            # Chrome オプション追加設定
            driver_option.add_argument('disable-notifications') 
            driver_option.add_argument('disable-translate')
            driver_option.add_argument('disable-extensions')
            driver_option.add_argument('disable-infobars')
            driver_option.add_argument('disable-gpu')
            driver_option.add_argument('no-sandbox')
            driver_option.add_argument('lang=ja')
            driver_option.add_argument('headless')
        end

        # Capybara設定
        Capybara.javascript_driver = :selenium_chrome_headless
        Capybara.run_server = true
        Capybara.default_selector = :css
        Capybara.default_max_wait_time = 5
        Capybara.ignore_hidden_elements = true
    end
end

次に「rails_helper.rb」「spec_helper.rb」の設定を変更します。

rails_helper.rbの設定変更
【コメント部分は全部削除しています】

require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'

require File.expand_path('../config/environment', __dir__)

abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
begin
  ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
  puts e.to_s.strip
  exit 1
end

RSpec.configure do |config|
  config.fixture_path = "#{::Rails.root}/spec/fixtures"
  config.use_transactional_fixtures = true
  config.infer_spec_type_from_file_location!
  config.filter_rails_from_backtrace!
end
spec_helper.rbの設定変更
【コメント部分は全部削除しています】

require 'helpers/capybara.rb'
RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.shared_context_metadata_behavior = :apply_to_host_groups
  config.failure_color = :red
  config.fail_fast = false
  config.color = true
  config.formatter = :documentation

テストコード記述

テストコード保管先とテストコード用のファイルを作成します。

ディレクトリとファイル作成
$ mkdir -p spec/lib spec/system

$ touch spec/lib/ssh_interactive_spec.rb spec/lib/users_password_spec.rb

実際にテストコードを記述します。

自作モジュールテスト(Rspec)

SSH接続して対話式にコマンドを実行する機能のテストを実施します。

モジュールテスト(ssh_interactive_spec.rb)
require 'rails_helper'
require 'ssh_interactive'
require 'settings'

RSpec.describe SshInteractive do
    describe "SSH対話式(passwdコマンド)" do 

        let!(:user_id) {'ユーザー名'}
        let!(:pass_old) {'現行パスワード'}
        let!(:pass_new) {'新しいパスワード'}
        let!(:host) {Settings.ssh_params.host}
        let!(:port) {Settings.ssh_params.port}
        let!(:keys) {Settings.ssh_params.keys}
        let!(:passphrase) {Settings.ssh_params.passphrase}
        let!(:ssh) { SshInteractive.new }


        context 'SSH接続先の情報が誤っている場合' do

            # 前処理
            before do
                ssh.set_host(host)
                ssh.set_port(22)
                ssh.set_publickey_auth(keys, passphrase)
            end

            it 'SSH接続エラーが発生する事' do
                result = ssh.password_change(user_id, pass_old, pass_new , pass_new)
                expect(result).to eq (-10)
            end
        end

        context '存在しないユーザーが設定されている場合' do

            # 前処理
            before do
                ssh.set_host(host)
                ssh.set_port(port)
                ssh.set_publickey_auth(keys, passphrase)
            end

            it 'SSH接続エラーが発生する事' do
                result = ssh.password_change('test1', pass_old, pass_new , pass_new)
                expect(result).to eq (-1)
            end
        end

        context '旧(現行)パスワード間違っている場合' do

            # 前処理
            before do
                ssh.set_host(host)
                ssh.set_port(port)
                ssh.set_publickey_auth(keys, passphrase)
            end

            it '認証エラーが発生する事' do
                result = ssh.password_change(user_id, pass_old  + 'dgh', pass_new , pass_new)
                expect(result).to eq (-2)
            end
        end

        context '辞書攻撃チェックに引っかかった場合' do

            # 前処理
            before do
                ssh.set_host(host)
                ssh.set_port(port)
                ssh.set_publickey_auth(keys, passphrase)
            end

            it 'パスワード変更エラーが発生する事' do
                result = ssh.password_change(user_id, pass_old, pass_new + 'abcdefghijk' , pass_new + 'abcdefghijk')
                expect(result).to eq (-3)
            end
        end


        context '全ての条件がクリアされている場合' do

            # 前処理
            before do
                ssh.set_host(host)
                ssh.set_port(port)
                ssh.set_publickey_auth(keys, passphrase)
            end

            it 'パスワード変更処理が正常に完了する事' do
                result = ssh.password_change(user_id, pass_old, pass_new, pass_new)
                expect(result).to eq (0)
            end
        end
    end
end

E2Eテスト(System Spec + Selenium)

Webアプリケーション(画面)のテストを実施します。

E2Eテスト(users_password_spec.rb)
require 'rails_helper'

RSpec.describe 'top_form', type: :system, js: true do
    describe '/top' do 

        let!(:save_path) {'tmp/screenshots/'}
        let!(:user_id) {'test'}
        let!(:pass_old) {'_8Ac-E5s'}
        let!(:pass_new) {'X9a@ywV5'}

        before do
            visit '/top'
        end

        context '存在しないユーザーが入力されている場合' do

            before do
                find('#user_id').set(user_id + '1')
                find('#password_old').set(pass_old)
                find('#password_new').set(pass_new)
                find('#password_verify').set(pass_new)
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "ユーザーID又はパスワードが間違っています"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case01.png')
            end
        end

        context '旧(現行)パスワード間違っている場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old + 'abc')
                find('#password_new').set(pass_new)
                find('#password_verify').set(pass_new)
                find_button('変更').click
            end

            it "警告メッセージが表示される事" do
                expect(page).to have_content "パスワードが間違っています"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case02.png')
            end
        end

        context '旧(現行)パスワードと新しいパスワードが一致する場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set(pass_old)
                find('#password_verify').set(pass_new)
                find_button('変更').click
            end

            it "警告メッセージが表示される事" do
                expect(page).to have_content "古いパスワードと新しいパスワードが同じです"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case03.png')
            end
        end

        context '新しいパスワードと新しいパスワード(確認)が一致しない場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set(pass_new)
                find('#password_verify').set(pass_new + 'abc' )
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "新しいパスワードと新しいパスワード"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case04.png')
            end
        end

        context '新しいパスワードが複雑性を満たすパスワードではない場合(数字無)' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('Xda@ywVb')
                find('#password_verify').set('Xda@ywVb')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "複雑性を満たすパスワードになっていません"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case05.png')
            end
        end

        context '新しいパスワードが複雑性を満たすパスワードではない場合(英小文字無)' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('X9A@YWV5')
                find('#password_verify').set('X9A@YWV5')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "複雑性を満たすパスワードになっていません"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case06.png')
            end
        end

        context '新しいパスワードが複雑性を満たすパスワードではない場合(英大文字無)' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('x9a@ywwv5')
                find('#password_verify').set('x9a@ywwv5')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "複雑性を満たすパスワードになっていません"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case07.png')
            end
        end

        context '新しいパスワードが複雑性を満たすパスワードではない場合(対象記号無)' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('X9aywV5')
                find('#password_verify').set('X9aywV5')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "複雑性を満たすパスワードになっていません"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case08.png')
            end
        end

        context '新しいパスワードの文字数が基準を満たしていない場合(7文字以下)' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('X9a@ywV')
                find('#password_verify').set('X9a@ywV')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "パスワードの文字数が基準を満たしていません"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case09.png')
            end
        end

        context '新しいパスワード内にユーザー名が含まれている場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set('X9a@ywV5az74d52_test')
                find('#password_verify').set('X9a@ywV5az74d52_test')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "新しいパスワードにユーザーIDと同じ文字列が含まれています"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case10.png')
            end
        end

        context 'パスワード内に辞書攻撃に該当する文字列が含まれいている場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set(pass_new + 'abcdefghijk')
                find('#password_verify').set(pass_new + 'abcdefghijk')
                find_button('変更').click
            end

            it '警告メッセージが表示される事' do
                expect(page).to have_content "辞書攻撃チェックに該当します"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_case11.png')
            end
        end

        context '全ての条件がクリアされている場合' do

            before do
                find('#user_id').set(user_id)
                find('#password_old').set(pass_old)
                find('#password_new').set(pass_new)
                find('#password_verify').set(pass_new)
                find_button('変更').click
            end

            it 'パスワード変更完了画面が表示される事' do
                expect(page).to have_content "パスワード変更が完了しました"
            end

            after do
                page.driver.save_screenshot(save_path + 'page_top_normal.png')
            end
        end
    end
end

テスト実行

最後に作成したテストツールを使用してテストを実行してみます。
※エラー結果も見せたいため、あえてエラーを発生させています。
 (嘘です。公開用の設定に変更したため環境周りでエラーが出ています)

テストコマンド実行(ssh_interactive_spec.rbのみ)
$ bundle exec rspec spec/lib/ssh_interactive_spec.rb

SshInteractive
  SSH対話式(passwdコマンド)
    SSH接続先の情報が誤っている場合
      SSH接続エラーが発生する事
    存在しないユーザーが設定されている場合
      SSH接続エラーが発生する事
    旧(現行)パスワード間違っている場合
      認証エラーが発生する事 (FAILED - 1)
    辞書攻撃チェックに引っかかった場合
      パスワード変更エラーが発生する事 (FAILED - 2)
    全ての条件がクリアされている場合
      パスワード変更処理が正常に完了する事 (FAILED - 3)

Failures:

  1) SshInteractive SSH対話式(passwdコマンド) 旧(現行)パスワード間違っている場合 認証エラーが発生する事
     Failure/Error: expect(result).to eq (-2)

       expected: -2
            got: -1

       (compared using ==)
     # ./spec/lib/ssh_interactive_spec.rb:59:in `block (4 levels) in <top (required)>'

  2) SshInteractive SSH対話式(passwdコマンド) 辞書攻撃チェックに引っかかった場合 パスワード変更エラーが発生する事
     Failure/Error: expect(result).to eq (-3)

       expected: -3
            got: -1

       (compared using ==)
     # ./spec/lib/ssh_interactive_spec.rb:74:in `block (4 levels) in <top (required)>'

  3) SshInteractive SSH対話式(passwdコマンド) 全ての条件がクリアされている場合 パスワード変更処理が正常に完了する事
     Failure/Error: expect(result).to eq (0)

       expected: 0
            got: -1

       (compared using ==)
     # ./spec/lib/ssh_interactive_spec.rb:90:in `block (4 levels) in <top (required)>'

Finished in 0.32911 seconds (files took 2.54 seconds to load)
5 examples, 3 failures

ここまでが今回の記事におけるRspec実装の内容になります。
下記以降はRspec実装に当たっての各種参考設定の情報になります。

【参考設定情報】

下記ディレクトリ構成は公式ドキュメント RSpec Rails 3-9 【外部サイト】からの引用となります。

作成時点では、このディレクトリ構造を知らなかったのであまり守られていないです。

ディレクトリ構成

app
├── controllers
│   ├── application_controller.rb
│   └── books_controller.rb
├── helpers
│   ├── application_helper.rb
│   └── books_helper.rb
├── models
│   ├── author.rb
│   ├── book.rb
└── views
    ├── books
    ├── layouts
lib
├── country_map.rb
├── development_mail_interceptor.rb
├── enviroment_mail_interceptor.rb
└── tasks
    ├── irc.rake
spec
├── controllers
│   ├── books_controller_spec.rb
├── country_map_spec.rb
├── features
│   ├── tracking_book_delivery_spec.rb
├── helpers
│   └── books_helper_spec.rb
├── models
│   ├── author_spec.rb
│   ├── book_spec.rb
├── rails_helper.rb
├── requests
│   ├── books_spec.rb
├── routing
│   └── books_routing_spec.rb
├── spec_helper.rb
└── tasks
│   ├── irc_spec.rb
└── views
    ├── books

設定ファイルのメモ

spec_helper.rbの設定内容

Rspecに関する設定を書くための設定ファイルらしいです。

config設定 概要
config.filter_run_when_matching :focus 「rspec --tag focus」コマンド実行時に「:focus」タグが設定されたテストを実行。
config.run_all_when_everything_filtered = true 「:focus」タグがついたものが何もない場合、フィルタを無視。
config.example_status_persistence_file_path = "spec/examples.txt" 「rspec --only-failures」コマンド実行時にテスト結果の一時保管。
config.disable_monkey_patching! モンキーパッチを無効化。
config.profile_examples = 10 実行後、遅いテスト項目を表示(10件)。
config.order = :random 実行結果の順番(random = ランダム)
Kernel.srand config.seed 「rspec --seed 【seed値】」コマンド実行時にランダム結果を固定。
config.failure_color = :red 実行結果のエラー内容をカラー出力(red = 赤色)
config.fail_fast = false 実行中にエラーが発生時の処理(false = 最後までテスト実施)
config.color = true 実行結果の標準出力をカラー出力
config.formatter = :documentation 実行結果のフォーマット指定(デフォルト:progress)
config.shared_context_metadata_behavior = :apply_to_host_groups shared_contextの記述指定(:trigger_inclusionは3.4以前の書き方)。

rails_helper.rbの設定内容

Rails特有の設定を書くための設定ファイルらしいです。

config設定 概要
config.infer_spec_type_from_file_location! specファイルが配置されているディレクトリ内のspecタイプ(model, controller, feature等)を自動判別。
config.filter_rails_from_backtrace! backtrace表示を簡素化。
config.use_transactional_fixtures = true テスト実行後、データベースのデータ削除。

Capybaraの設定内容

RubyでWebアプリケーションのE2Eテストフレームワークを提供する機能らしいです。

capybara.rb設定 概要
config.include Capybara::DSL Capybaraを取り込むのに必要な設定。
driven_by :rack_test rackアプリケーションをテストするための機能。javascriptのテスト不可。
driven_by :selenium_chrome_headless Rubyクライアントライブラリを使用してChromeブラウザでテストするための機能。javascriptのテスト可。
driver_option.add_argument('disable-notifications') Web通知やPush APIによる通知を無視。
driver_option.add_argument('disable-translate') 翻訳ツールバーを無効。
driver_option.add_argument('disable-extensions') 拡張機能を無効。
driver_option.add_argument('disable-infobars') インフォバーの表示を無効。
driver_option.add_argument('disable-gpu') GPU描画処理を無効。
driver_option.add_argument('no-sandbox') sandboxを無効。
driver_option.add_argument('lang=ja') 言語を日本語。
driver_option.add_argument('headless') Headlessモード(画面を表示せずに動作)を有効。
Capybara.javascript_driver = :selenium_chrome_headless chromeのheadlessモードでjavascript実行。
Capybara.run_server = true rackサーバーを実行。
Capybara.default_selector = :css セレクタ(find等)利用時の設定。
Capybara.default_max_wait_time = 5 ajaxやcss等の待ち時間。
Capybara.ignore_hidden_elements = true 非表示要素(display:none)は検出しない。

おわりに

テストツール自体はそれほど難しくありませんでしたが、環境や設定周りがすごく面倒でした。
また、今回はモデルを利用していなかったため「テストデータ作成」関連は手を付けていないため、
どこかで調査して記事にしていこうかなと考えています。

ただ、今回の記事では全てを理解したわけではないので、今後も気づいたことがあれば加筆・修正していく予定です。

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

rails 『アウトプット』

application.scssがおかしいと思い@importの順番を変えたが反映されず名前を変えてもエラーが出なかったためapplication.html.hamlを見たら配置ミスがあった。application.html.hamlはhaml,scss両方を統合しているため確かめる必要がある。

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

Rubyでじゃんけん

Rubyにてじゃんけん (Rock-Paper-Scissors)

1.じゃんけんの構造化

そもそもじゃんけんとは??
グー、チョキ、パーのどれかを互いに選択し、勝敗を決めるものである。
今回はプレイヤーとプログラムで戦う。
グー Rock
チョキ Paper
パー Scissors
で対戦。
(プログラムでは、乱数を返す、randが活躍するが、それは後々)

2.playerの手選択

入力による代入

p 'rock-paper-scissors'
player=gets

↑rock、paper、scissorsのどれかを選んでもらう
プレイヤーがスペルミスしたときの対応も重要になる

3.programの手選択

乱数で決定する。

program_hand=rand(3)

↑program_handは0、1、2を乱数で返すことになる。

4.整理

人間にとっては、Rock-Paper-Scissorsという文字表記が分かりやすいが、乱数を0、1、2でやったように、プログラム(機械)からすると、文字よりも数字が分かりやすい。

よって、じゃんけんをしている2人(player、program)において、手を数字と文字であらわしておく

0 グー Rock
1 チョキ Paper
2 パー Scissors

if program_hand==0
    program="rock"
elsif program_hand==1
    program="paper"
elsif program_hand==2
    program="scissors"
end
if player=="rock"
    player_hand=0
elsif player=="paper"
    player_hand=1
elsif player=="scissors" 
    player_hand=2
end

5.じゃんけんの勝敗をつける

ここで、条件分岐が登場
if文を用いる

if player_hand==program_hand
    p 'draw'
elsif ((player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 or program_hand==0))and a==1
    p 'you win'
elsif 
    p 'you lose'
end

条件の式を書く上で、論理が連続で出てくることがある。(andやor)
論理の優位性などという話があるらしいが、難しい。
先に計算したい方をかっこでくくっちゃうのが早い、簡単、わかりやすい。

6.スペルミスに対する対応

rock、paper、scissorsのどれかを選んでもらうことになってるが、
それ以外の入力には、いかにして対応するのか??

one more please
と表示させることに

aという変数を登場させる。初期値0
rock、paper、scissorsのどれかが記入されたときのみa=1を代入。

aをつかって判断する

↓完成

p 'rock-paper-scissors'
player=gets


a=0

if player=="rock"
    player_hand=0
    a=1
elsif player=="paper"
    player_hand=1
    a=1
elsif player=="scissors" 
    player_hand=2
    a=1
end

program_hand=rand(3)


if program_hand==0
    program="rock"
elsif program_hand==1
    program="paper"
elsif program_hand==2
    program="scissors"
end



if a==0
    p 'one more please'
elsif player_hand==program_hand
    p 'draw'
elsif ((player_hand==0 and program_hand==2) or (player_hand==1 and program_hand==0) or (player_hand==2 or program_hand==0))and a==1
    p 'you win'
elsif 
    p 'you lose'
end

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

railsにてcreated_at の曜日も日本語で表示する方法( lメソッド)

  • 目標①:railsにて、DBからcreated_at の日時を表示(取得)したい。
変数.html.erb
<%= link_to(変数.created_at.strftime('%Y年/%m月/%d日(%a)%H:%M')," リンク先を記入") %>
  • まず試したコード
    (今回は、更にlink_toを使いたい)

    • 「 XXXX年 XX月XX日(mon) XX:XX 」とまで成功。
    • <%= 変数.created_at.strftime('%Y/%m/%d(%a)%H:%M') %> を実行し、曜日が英語標記のままのため苦戦。
    • #{%w(日 月 火 水 木 金 土… 系も試したが、上手くいかない。
    • config/initiallzers/time_formats.rb等も作成し定義を試したが、上手くいかない。

ーーーーーーーーーーーーーーーーーーーーー

<備忘録>
目標② (mon)ではなく、曜日も日本語で表示する簡単な方法

①gem 'i18n_generators' をbundleインストール

②en.ymlと同じ階層ディレクトリにconfig/locales/ja.yml を作成。以下のコマンド実行すると内容が書き換わり大変便利。

rails g i18n_locale ja

③ja.yml 内の

time:

      default: "%Y年%m月%d日(%a) %H時%M分" ←ここを取得したいように自分で変更する

④表示したいhtml.erb内へ lメソッドにて記入。

<%= link_to(l(テーブル名.created_at),"リンク先") %>

リンクではない場合、↓

<%= l テーブル名.created_at %> 

念のため、サーバーを立ち上げ直し、
曜日も (mon)→(月) 日本語になりました。

最終参考

Railsで日時をフォーマットするときはstrftimeよりも、lメソッドを使おう @Junichi Ito

*個人勉強用サイト作成のため、
曜日を表示しないで妥協するか迷いましたが、諦めないで試行錯誤することが大切だと感じています。

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

Ruby数字出力 0埋め

Rubyでの計算結果を0埋めで表示(3→003)

今回、例として、入力された数値の倍の数を三桁の0埋めで考える。
1→002
2→004
6→012

1.まず普通の表示

1→2
2→4
6→12
をまず、できるようにする

puts "数字を入れてください"
number = gets.to_i
number2=number*2
print number2

↑これで、完了。

2.0埋めを考えていく

sprintf("%03d",number)

"%03d" % number

が、numberの0埋め3桁表示となる。

実行するプログラム

puts "数字を入れてください"
number = gets.to_i
number2=number*2
number3=sprintf("%03d", number2)
print number3

1→002
2→004
6→012

が確認できた

参考文献

http://osishow3.hateblo.jp/entry/2017/04/24/235500

https://www.setoya-blog.com/entry/2013/12/06/171143

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

Ruby計算 気を付けるべきポイント(変数の入力)

Rubyにて変数に文字列代入の際のポイント

今回行いたいのは、Rubyにて数値の計算。
(例) 入力した数値の倍の値を表示するプログラム。(2→4)

1.まず入力を考える。

変数に数を代入する。
代入は、入力によって行う。
入力の際は、getコマンドを用いる

nunber=gets

これで入力を行えば、代入される。

2.計算を考える。

今回は倍にするという単純な計算。
*が×を表す。

number2=number*2

3.結果表示

計算結果はnumber2に入ってるので、

print number2

とすると、結果は.....

2
2

あれ??

文字列として、代入されてたことが発覚
.to_iをつけて、文字列→数値にする必要あり

number = gets.to_i
number2=number*2
print number2

これで表示は

4

となった。

4.参考ブログ

https://qiita.com/Hayate_0807/items/2e9705091b181a104621

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

サンプルコードでわかる!Ruby 2.7の主な新機能と変更点 Part 3 - 新機能と変更点の総まとめ

はじめに

Rubyは毎年12月25日にアップデートされます。
Ruby 2.7については2019年12月21日にrc2がリリースされました。

Ruby 2.7.0-rc2 リリース

この記事ではRuby 2.7で導入される変更点や新機能について、サンプルコード付きでできるだけわかりやすく紹介していきます。

ただし、Ruby 2.7は多くの新機能や変更点があり、1つの記事に収まらないのでいくつかの記事に分けて書いていきます。
番号指定パラメータ、パターンマッチ構文、キーワード引数に関する仕様変更についてはすでに他の記事で説明したので、本記事ではそれ以外の変更点を説明しています。

すでに説明したRuby 2.7の新機能や変更点はこちら

Ruby 2.7の新機能や変更点は非常に多いので、いくつかの記事に分けて説明しています。
以下の記事はすでに公開済みです。

本記事の情報源

本記事は以下のような情報源をベースにして、記事を執筆しています。

また、issueを追いかけてもピンと来なかった内容については、ブログ「ruby trunk changes」のコミット解説を参考にさせてもらいました(nagachikaさん、どうもありがとうございます!)。

動作確認したRubyのバージョン

本記事は以下の環境で実行した結果を記載しています。

$ ruby -v
ruby 2.7.0rc2 (2019-12-22 master 75acbd5f00) [x86_64-darwin19]

フィードバックお待ちしています

本文の説明内容に間違いや不十分な点があった場合は、コメント欄や編集リクエスト等で指摘 or 修正をお願いします?

それでは以下が本編です!

構文や言語機能上の変更点

パターンマッチ構文が試験的に導入された(別記事にて説明済み)

Ruby 2.7ではパターンマッチ(またはパターンマッチング)構文が試験的に導入されました。

関数型言語で広く使われているパターンマッチという機能が実験的に導入されました。 渡されたオブジェクトの構造がパターンと一致するかどうかを調べ、一致した場合にその値を変数に代入するといったことができるようになります。

Ruby 2.7.0-rc2 リリース

以下はパターンマッチ構文の使用例です。

case {status: :error, message: 'User not found.'}
in {status: :success}
  puts "Success!"
in {status: :error, message: message}
  puts "Error: #{message}"
end
#=> Error: User not found.

パターンマッチについては以下の記事で詳しく説明しています。

キーワード引数と普通の引数の自動変換が非推奨になった(別記事にて説明済み)

Ruby 2.7ではキーワード引数と普通の引数の自動変換が非推奨になり、警告が発生します。

以下は警告が発生するようになったコードの例です。

def foo(key: 0)
  p key
end
foo({key: 42})
#=> The last argument is used as the keyword parameter
#=> 42

警告が発生するコードはRuby 3で動かなくなります。

詳しい内容はこちらの記事をご覧ください。

ブロックの仮引数として番号指定パラメータが試験的に導入された(別記事にて説明済み)

Ruby 2.7ではブロックの仮引数として番号指定パラメータ(numbered parameter)が試験的に導入されました。
これにより、|s| のように明示的に引数名を指定する代わりに、_1 のような連番でブロックの仮引数を受け取ることができます。

# 番号指定パラメータを使わない場合
%w(1 20 300).map { |s| s.rjust(3, '0') }
#=> ["001", "020", "300"]

# 番号指定パラメータを使う場合
%w(1 20 300).map { _1.rjust(3, '0') }
#=> ["001", "020", "300"]
# 番号指定パラメータを使わない場合
[1, 2, 3, 4].inject(0) { |memo, n| memo + n }
#=> 10

# 番号指定パラメータを使う場合
[1, 2, 3, 4].inject(0) { _1 + _2 }
#=> 10

詳しい内容はこちらの記事をご覧ください。

メソッドの内のブロックを伴わないProc.new/procとlambdaが警告またはエラー扱いとなった

Ruby 2.6まではメソッド内でブロックなしのProc.new/procやlambdaを呼ぶと、暗黙的にメソッド呼び出し時に引き渡したブロックが割り当てられていました。

def proc_without_block
  # このprocにはこのメソッドを呼びだしたときに
  # 引き渡したブロックが暗黙的に割り当てられる
  proc.call * 100

  # Proc.newを使った場合も同様
  # Proc.new.call * 100
end

proc_without_block { 123 }
#=> 12300
def lambda_without_block
  # lambdaの場合も同様
  lambda.call * 100
end

lambda_without_block { 123 }
#=> 12300

Ruby 2.7ではProc.new/procのときに警告が、lambdaのときは例外が発生するようになりました。

def proc_without_block
  proc.call * 100
end

# Ruby 2.7では警告が出る
proc_without_block { 123 }
#=> warning: Capturing the given block using Kernel#proc is deprecated; use `&block` instead
def lambda_without_block
  lambda.call * 100
end

# Ruby 2.7では例外が発生する
lambda_without_block { 123 }
#=> ArgumentError (tried to create Proc object without a block)

開始値省略範囲式(beginless range)が試験的に導入された

Ruby 2.7では開始値省略範囲式(beginless range)が試験的に導入されました。

numbers = [10, 20, 30, 40, 50]

# beginless rangeを使って配列の最初の3要素を取得する
numbers[..2]
#=> [10, 20, 30]

# 従来通り次のように書いても同じ
numbers[0..2]
#=> [10, 20, 30]

Ruby 2.6で導入されたendless rangeと組み合わせると次のようなコードも書けます。

n = -5
ret =
  case n
  when ..-1
    'minus'
  when 1..
    'plus'
  else
    'zero'
  end
ret
#=> minus

特殊変数の$;$,に非nil値を設定すると警告が出るようになった

Ruby 2.7ではsplitメソッドのデフォルトの区切り文字を表す特殊変数$;と、デフォルトの出力フィールド区切り文字列(joinメソッドなどで使われる)を表す$,で、nil以外の値を設定すると警告が出るようになりました。
また、その状態でsplitメソッドやjoinメソッドを呼び出した際も警告が出ます。

# デフォルト値はnil
$; #=> nil

# 引数なしでsplitメソッドを呼ぶと空白文字で分割される
'ab,c d'.split #=> ["ab,c", "d"]

# 非nil値を設定すると警告が出る
$; = ','
#=> warning: non-nil $; will be deprecated

# splitメソッドを呼んだときも警告が出る
'ab,c d'.split #=> ["ab", "cd"]
#=> warning: $; is set to non-nil value
# デフォルト値はnil
$, #=> nil

# 引数なしでjoinメソッドを呼ぶと空文字で連結される
%w(a b c).join #=> "abc"

# 非nil値を設定すると警告が出る
$, = '-'
#=> warning: non-nil $, will be deprecated

# joinメソッドを呼んだときも警告が出る
%w(a b c).join #=> "a-b-c"
#=> warning: $, is set to non-nil value

ヒアドキュメント識別子に付ける'"が同じ行にないと構文エラーが出るようになった

下のコードのようにヒアドキュメントの識別子に付ける'"が同じ行にない場合、Ruby 2.6までは警告が出ていましたが、Ruby 2.7からはsyntax errorが発生するようになりました。

s = <<~"TEXT
"
  Hello!
TEXT
#=> Ruby 2.6では以下の警告が発生する
#   warning: here document identifier ends with a newline

#=> Ruby 2.7ではsyntax errorが発生する
#   unterminated here document identifier
#   s = <<~"TEXT
#   syntax error, unexpected end-of-input
#   s = <<~"TEXT

フリップフロップ構文の警告が撤回された

Ruby 2.6からフリップフロップ構文を使うと警告が発生するようになっていましたが、Ruby 2.7では(正確にはRuby 2.6.4以降では)この変更が撤回され、警告が出なくなりました。

numbers = 1..10
ret =
  numbers.map do |n|
    # この下の行がフリップフロップ構文
    if (n % 3 == 0)..(n % 2 == 0)
      n * 10
    end
  end
#=> Ruby 2.6.0〜2.6.3では以下の警告が発生していたが、
#   Ruby 2.6.4以降とRuby 2.7では発生しなくなった
#   warning: flip-flop is deprecated


ret.compact
#=> [30, 40, 60, 90, 100]

フリップフロップ構文って何?という方は以下の記事をご覧ください。

範囲式を使ったフリップフロップ - Qiita

改行を伴うメソッドチェーンにコメント行を挟み込めるようになった

Ruby 2.7では、以下のように改行を伴うメソッドチェーンにコメント行を挟み込めるようになりました。

200
  # 次の値(つまり201)を得る
  .then(&:succ)
  # 文字列に変換する
  .then(&:to_s)
  # 逆順にする
  .then(&:reverse)
#=> "102"

ドットが後に来るパターンでも大丈夫です。

200.
  # 2倍する
  then(&:succ).
  # 文字列に変換する
  then(&:to_s).
  # 逆順にする
  then(&:reverse)

ちなみに、Ruby 2.6では以下のように構文エラーが発生していました。

syntax error, unexpected '.', expecting end-of-input
  .then(&:succ)

privateメソッドをself付きで呼び出せるようになった

これまでRubyのprivateメソッドは「レシーバを指定して呼び出すことができないメソッド」とされてきました。
そのためクラス内でもself付きでprivateメソッドを呼び出すと例外が発生していました。

class Foo
  def hello
    # nameはprivateメソッドなのでself付きで呼び出すことはできない
    "I am #{self.name}"
  end

  private

  def name
    'Alice'
  end
end

foo = Foo.new

# self.nameと書いていたので、例外が発生する
foo.hello
#=> `hello': private method `name' called for #<Foo:0x00007fb0488b24c8> (NoMethodError)

Ruby 2.7ではself付きでprivateメソッドを呼び出せるようになりました。

class Foo
  def hello
    # Ruby 2.7ではself付きでprivateメソッドを呼び出せる
    "I am #{self.name}"
  end

  private

  def name
    'Alice'
  end
end

foo = Foo.new
foo.hello
#=> "I am Alice"

# もちろん、クラスの外からはレシーバ付きでprivateメソッドを呼び出すことはできない
foo.name
#=> private method `name' called for #<Foo:0x00007f91049094e8> (NoMethodError)

ちなみに、これまでのRubyでもセッターメソッドだけはprivateでも例外的にself付きで呼び出すことができていました。(こちらの記事も参考)

class Foo
  def initialize
    @name = 'Alice'
  end

  def hello
    "I am #{self.name}"
  end

  def change_name
    # Ruby 2.6以前でも、セッターメソッドだけはprivateでもself付きで呼び出せた
    self.name = 'Bob'
  end

  private

  def name=(value)
    @name = value
  end

  def name
    @name
  end
end

foo = Foo.new

foo.hello
#=> I am Alice

foo.change_name

foo.hello
#=> I am Bob

今回のRuby 2.7の変更により、セッターメソッドか、そうでないかに関わらず、一貫してprivateメソッドをself付きで呼び出せるようになります。

rescue修飾子を伴う多重代入の挙動が変わった

Ruby 2.6では以下のようにrescue修飾子と多重代入を行うと次のような結果になっていました。

# ZeroDivisionErrorが発生するが、rescueする
a, b = [1/0, 2/0] rescue [10, 20]
a #=> nil
b #=> nil

これは次のようにパースされていたためです。

(a, b = [1/0, 2/0]) rescue [10, 20]

Ruby 2.7ではパースのされ方が以下のように変更されました。

a, b = ([1/0, 2/0] rescue [10, 20])

これにより、rescue修飾子から返される配列を多重代入できるようになりました。

a, b = [1/0, 2/0] rescue [10, 20]
a #=> 10
b #=> 20

特異クラス構文内でyieldを使うと警告が出るようになった

Ruby 2.7では、以下のように特異クラス構文内でyieldを呼び出すと警告が出るようになりました。(こんなコードを書くことは、まれだと思いますが・・・)

def yield_in_class_syntax
  class << Object.new
    yield
  end
end

yield_in_class_syntax { puts 'nihao!' }
#=> warning: `yield' in class syntax will not be supported from Ruby 3.0. [Feature #15575]
#   nihao!

全引数を別のメソッドに引き渡す...引数が導入された

Ruby 2.7ではあらゆる引数を受け取って、別のメソッドに引き渡す...引数が導入されました。

def add(a, b)
  a + b
end

def add_with_description(...)
  # 受け取った引数をすべてそのままaddメソッドに引き渡す
  answer = add(...)
  "answer is #{answer}"
end

add_with_description(2, 3)
#=> answer is 5

別のメソッドを呼び出すときは丸かっこ(())が必須です。丸かっこがないと開始と終了のない範囲オブジェクト(nil...nil)と見なされます。

# (...)と書かなかった場合は、nil...nilをaddメソッドを渡したことになる(警告も出る)
answer = add ...
#=> warning: ... at EOL, should be parenthesized?

メソッドの仮引数も別メソッドに渡す引数も、いずれも...になっている必要があります。
片方だけが通常の引数だったり、引数の一部だけが...になっていたりすると、構文エラーになります。

def add_with_description(a, b)
  # 仮引数は通常の引数で、別メソッドの呼び出しが...になっていると構文エラー
  answer = add(...)
  "answer is #{answer}"
end
# 引数の一部が通常の引数で、残りが...になっていると構文エラー
def add_with_description(a, ...)
  answer = add(a, ...)
  "answer is #{answer}"
end

セキュリティモデル関連の特殊変数やメソッドのサポートが縮小された

RubyにはCGIプログラミングをサポートするためのセキュリティ機能(セキュリティモデル)が用意されています。(参考
しかし、近年のエコモデルではこの仕組みはあまり役に立たないため、サポートが縮小されつつあります。

特殊変数の$SAFEはRubyの「セーフレベル」を設定するために使われていましたが、Ruby 2.7ではこの変数を変更したり、参照したりすると警告が表示されます。

# 参照すると警告が出る
$SAFE
#=> warning: $SAFE will become a normal global variable in Ruby 3.0
#   0

# 変更しても警告が出る
$SAFE = 1
#=> warning: $SAFE will become a normal global variable in Ruby 3.0

また、taint, untaint, trust, untrust といった、セキュリティモデルに関連するメソッドも無効化されています。(オブジェクトは汚染状態にはならない)

some = "puts '@&%&(#!'"

some.tainted?
#=> false

# Ruby 2.7では呼びだしても何も変化がない
some.taint

some.tainted?
#=> falseのままになる(Ruby 2.6ではtrueになる)

シンタックスハイライトや自動インデントなど、irbが大きく進化した

Ruby 2.7ではirbが大きく進化しました。

進化その1) シンタックスハイライトされる

irb内に打ち込んだコードが自動的にシンタックスハイライトされます。
Screen Shot 2019-12-24 at 5.23.44.png

進化その2) 自動的にインデントしてくれる

irb内に打ち込んだコードが自動的にインデントされます。
6fMleap5ii.gif

進化その3) 上下キーで複数行の入力履歴をまとめて行き来できる

上下キーを押すと複数行の入力履歴をまとめて行き来できます。
rC6CYRF7Tt.gif

ただし、長いメソッドやif文を入力したあとだと、履歴をさかのぼるのがちょっと大変かもしれません。
9risyF6Apm.gif

進化その4) TABキーを押すと入力候補を表示してくれる

メソッドを途中まで入力してTABキーを押すとクラス名やメソッド名、変数名などの入力候補を表示してくれます。
2Q0QKdMev3.gif

候補が1つしかない場合は自動補完してくれます。
3poUVqlEcW.gif

進化その5) TABキーを2回押すとクラスやメソッドのドキュメントを表示してくれる

メソッド名を入力してTABキーを2回押すとクラスやメソッドのドキュメントを表示してくれます。
表示を戻すときはqを押します。
YqJmzQnBb0.gif

従来のirbが好みだという人は、--legacyオプションを付けてirbを起動すると、シンタックスハイライトや複数行の入力履歴等が無効化されます。

$ irb --legacy
irb(main):001:0> 

自動インデントを無効化する場合は、~/.irbrcに以下の設定を追加します。

# ~/.irbrc
IRB.conf[:AUTO_INDENT] = false

入力履歴はデフォルトで1000件保存されます。(~/.irb_historyという履歴ファイルが作成されます)
履歴をもっと増やしたい、または減らしたい、という場合は~/.irbrcで好みの件数を設定できます。(nilを設定すると入力履歴が無効化されます)

# ~/.irbrc
IRB.conf[:SAVE_HISTORY] = 2000

参考: What's new in Interactive Ruby Shell (IRB) with Ruby 2.7 – Saeloun Blog

続いて、クラス単位でRuby 2.7の新機能や変更点を紹介していきます。

Array

&と同じ振る舞いをするintersectionメソッドが追加された

Ruby 2.7では両方の配列に含まれる要素を重複なく返すintersectionメソッドが追加されました。
これは従来の&演算子と同じ挙動になります。

a = [1, 1, 3, 5]
b = [3, 2, 1]

# 2つの配列に共通して含まれる要素は1と3(重複なしで返す)
a.intersection(b)
#=> [1, 3]

# Ruby 2.6以前では&演算子を使う方法しかなかった
a & b
#=> [1, 3]

ちなみにRuby 2.6では|演算子と同じunionメソッドと、-演算子と同じdifferenceメソッドが追加されました。

a = [1, 2, 3]
b = [3, 4, 5]

# a | bと同じ
a.union(b)
#=> [1, 2, 3, 4, 5]
a = [1, 2, 3, 4, 5]
b = [0, 1, 2]

# a - bと同じ
a.difference(b)
#=> [3, 4, 5]

|-に名前付きのメソッドが追加されたのであれば、&にもあった方がいいのでは?ということでintersectionメソッドが追加されたみたいです。

Feature #16155: Add an Array#intersection method - Ruby master - Ruby Issue Tracking System

Array / Range

minmaxメソッドが高速化した

これまでArrayクラスのminmaxメソッドはEnumerableモジュールによって提供されていましたが、Ruby 2.7では高速化のためArrayクラス自身に実装されました。

# Ruby 2.6
[].method(:minmax)
#=> #<Method: Array(Enumerable)#minmax>

# Ruby 2.7
[].method(:minmax)
#=> #<Method: Array#minmax()>

Rangeクラスについても同様にminmaxメソッドがRangeクラス自身に実装されました。

# Ruby 2.6
(1..3).method(:minmax)
#=> #<Method: Range(Enumerable)#minmax>

# Ruby 2.7
(1..3).method(:minmax)
#=> #<Method: Range#minmax()>

Comparable

clampメソッドに範囲オブジェクトを渡せるようになった

Ruby 2.7ではclampメソッドに範囲オブジェクトが渡せるようになりました。

# Ruby 2.6以前
-1.clamp(0, 2) #=> 0
1.clamp(0, 2)  #=> 1
3.clamp(0, 2)  #=> 2

# Ruby 2.7からは範囲オブジェクトも渡せる
-1.clamp(0..2) #=> 0
1.clamp(0..2)  #=> 1
3.clamp(0..2)  #=> 2

ちなみに、clampメソッドはRuby 2.4で追加された比較的新しいメソッドです。

参考 指定された範囲内の値を返すようにするComparable#clamp

Complex(複素数オブジェクト)

<=>メソッドが追加され、条件付きで大小比較ができるようになった

Ruby 2.7では複素数オブジェクトに<=>メソッドが追加され、以下のような比較をしても例外が発生しなくなりました。

# Ruby 2.6
0 <=> 0i
#=> NoMethodError (undefined method `<=>' for (0+0i):Complex)

# Ruby 2.7
0 <=> 0i
#=> 0

ただし、以下の条件を満たさない場合はnilが返ります。

  • 複素数オブジェクトの虚部がゼロである
  • 比較対象のオブジェクトが実数、または虚部がゼロの複素数オブジェクトである
# 虚部がゼロでなければnil
Complex(2, 3) <=> Complex(2, 3) #=> nil
Complex(2, 3) <=> 1             #=> nil

# 比較対象のオブジェクトが実数でないのでnil
Complex(2) <=> :a #=> nil

# 虚部がゼロ、かつ比較対象のオブジェクトが実数なので比較可能
Complex(5) <=> 2  #=> 1

# 虚部がゼロ、かつ比較対象のオブジェクトが虚部ゼロの複素数なので比較可能
Complex(5) <=> Complex(2) #=> 1

Dir

globと[]にヌル文字で区切るパターンを渡すと例外が発生するようになった

Ruby 2.7ではDir.globDir.[]にヌル文字で区切るパターンを渡すと例外が発生するようになりました。

Dir.glob("foo\0bar")
#=> ArgumentError (nul-separated glob pattern is deprecated)

Dir["foo\0bar"]
#=> ArgumentError (nul-separated glob pattern is deprecated)

ちなみにRuby 2.6がリリースされたときには警告が出るようになっていて、段階的にこの機能が削除されたことになります。

# Ruby 2.6
Dir.glob("foo\0bar")
#=> warning: use glob patterns list instead of nul-separated patterns

Feature #14643: Remove problematic separator '\0' of Dir.glob and Dir.[] - Ruby master - Ruby Issue Tracking System

Encoding

CESU-8エンコーディングが追加された

Ruby 2.7ではCESU-8エンコーディング(Wikipedia)が使えるようになりました。

s = "いろは"
encoded = s.encode("CESU-8")
encoded.encoding == Encoding::CESU_8
#=> true

Enumerable

select(filter)とmapを同時に行うfilter_mapメソッドが追加された

Ruby 2.7ではselect(filter)とmapを同時に行うfilter_mapメソッドが追加されました。
このメソッドを使うとブロックの戻り値が真であるものだけがmapの結果として返されます。

numbers = [1, 2, 3, 4, 5]
# filter_mapを利用して偶数の要素だけ値を10倍する(奇数は要素から除外)
numbers.filter_map { |n| n * 10 if n.even? }
#=> [20, 40]

# Ruby 2.6以前だと下のどちらかの書き方になる
numbers.select(&:even?).map { |n| n * 10 }
numbers.map { |n| n * 10 if n.even? }.compact

なお、破壊的なfilter_map!メソッドは用意されていないようです。

numbers.filter_map! { |n| n * 10 if n.even? }
#=> NoMethodError (undefined method `filter_map!' for [1, 2, 3, 4, 5]:Array)

要素ごとの個数をカウントするtallyメソッドが追加された

Ruby 2.7では要素ごとの個数をカウントするtallyメソッドが追加されました。

order = ['ピザ', 'パスタ', 'ピザ', 'ドリア', 'ドリア']
order.tally
#=> {'ピザ' => 2, 'パスタ' => 1, 'ドリア' => 2}

# Ruby 2.6以前で同等のことを実現するコード例
order.each_with_object(Hash.new(0)) { |o, h| h[o] += 1 }
#=> {'ピザ' => 2, 'パスタ' => 1, 'ドリア' => 2}

ちなみに、tallyという名前は線の本数で数を表現する、tally marks画線法)に由来するそうで


Enumerator

自由なデータ変更を伴いつつ、無限のシーケンスを生成できるproduceメソッドが追加された

Ruby 2.7では自由なデータ変更を伴いつつ無限のシーケンスを生成できるproduceメソッドが追加されました。

# 1を順番にインクリメントさせるシーケンスから最初の5つを取り出す
# (第1引数の1はシーケンスの初期値)
Enumerator.produce(1, &:succ).take(5)
#=> [1, 2, 3, 4, 5]

# 0から999までのランダムな数字を5つ作成する(初期値を与えないパターン)
Enumerator.produce { rand(1000) }.take(5)
#=> [110, 725, 554, 755, 861]

こちらの記事にはproduceメソッドを使ってフィボナッチ数列を作成するコード例が載っていました。

# https://blog.saeloun.com/2019/11/27/ruby-2-7-enumerator-produce
Enumerator.produce([0, 1]) { |base_1, base_2|
  [base_2, base_1 + base_2]
}.take(10).map(&:first)
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

lazyなenumeratorを非lazyなenumeratorに変換するeagerメソッドが追加された

Ruby 2.7ではlazyなenumeratorを非lazyなenumeratorに変換するeagerメソッドが追加されました。

# lazyなenumeratorを作成する
lazy = [1, 2, 3].lazy.map { |x| x * 2 }

# 非lazyなenumeratorに変換する
enum = lazy.eager

# Enumeratorのインスタンスになっている
# (eagerを呼ばなければEnumerator::Lazyのインスタンス)
enum.class #=> Enumerator

# 非lazyなので普通に配列で返ってくる
# (lazyのままだと、またEnumerator::Lazyが返るので、to_aする必要がある)
enum.map { |x| x / 2 }
#=> [1, 2, 3]

Feature #15901: Enumerator::Lazy#eager - Ruby master - Ruby Issue Tracking System

Enumerator::Yielderオブジェクトにto_procメソッドが実装された

Enumerator::Yielderオブジェクトがto_procメソッドを実装したため、<<.yieldを使わなくても(&y)のような記法で済むようになりました。

# Ruby 2.6以前
# (ブロック引数のyがEnumerator::Yielderオブジェクト)
enum = Enumerator.new { |y|
  # y << i の代わりに y.yield i と書いても良い
  (1..3).each { |i| y << i }
}
enum.map { |i| i * 10 }
#=> [10, 20, 30]

# Ruby 2.7
enum = Enumerator.new { |y|
  # (&y)でOK
  (1..3).each(&y)
}
enum.map { |i| i * 10 }
#=> [10, 20, 30]

Feature #15618: Implement Enumerator::Yielder#to_proc - Ruby master - Ruby Issue Tracking System

Fiber

resumeすると同時にresumeされたfiber内で例外を発生させるraiseメソッドが追加された

Ruby 2.7はresumeすると同時にresumeされたfiber内で例外を発生させるraiseメソッドが追加されました。

fib = Fiber.new do
  counter = 0
  loop { counter += Fiber.yield }
  counter
end
fib.resume
fib.resume(10)
fib.resume(100)

# raiseメソッドを使ってfiber内でStopIteration例外を発生させ、ループを終了させる
# ループが終了するとcounterの値が返ってくる
fib.raise(StopIteration)
#=> 110

Feature #10344: [PATCH] Implement Fiber#raise - Ruby master - Ruby Issue Tracking System

File

ドットで終わる文字列をextnameメソッドに渡すとドットを返すようになった

Ruby 2.7ではドットで終わる文字列をextnameメソッドに渡すとドットを返すようになりました。

# Ruby 2.6
File.extname('foo.')
#=> ""

# Ruby 2.7
File.extname('foo.')
#=> "."

FrozenError

例外の発生原因となった凍結されたオブジェクトをreceiverメソッドで受け取れるようになった

Ruby 2.7では例外の発生原因となった凍結されたオブジェクトをreceiverメソッドで受け取れるようになりました。

frozen_str = 'abc'.freeze
begin
  frozen_str.upcase!
rescue FrozenError => err
  # receiverメソッドで、例外の発生原因となったオブジェクトを取得できる
  err.receiver
  #=> "abc"

  # 元のオブジェクトと同一のオブジェクトが返るので、equal?で比較するとtrueが返る
  err.receiver.equal?(frozen_str)
  #=> true
end

FrozenErrorオブジェクトをnewする際は、receiver:オプションでreceiverメソッドが返すオブジェクトを指定できます。

frozen_str = 'abc'.freeze
e = FrozenError.new('test', receiver: frozen_str)
e.receiver.equal?(frozen_str)
#=> true

GC

断片化したメモリをデフラグするGC.compactメソッドが導入された

Ruby 2.7では断片化したメモリをデフラグするGC.compactメソッドが導入されました。

# 断片化したメモリをデフラグする
GC.compact

技術的な解説はRuby 2.7.0.preview3のリリースノートより引用します。

一部のマルチスレッドなRubyプログラムを長期間動かし、マーク&スイープ型GCを何度も実行していると、メモリが断片化してメモリ使用量の増大や性能の劣化を招くことが知られています。

Ruby 2.7ではGC.compact というメソッドを導入し、ヒープをコンパクションすることが出来るようになります。ヒープ内の生存しているオブジェクトを他のページに移動し、不要なページを解放できるようになるとともに、ヒープをCoW (Copy on Write) フレンドリーにすることが出来ます。 [Feature #15626]

IO

BOMを見て外部エンコーディングを判別できるset_encoding_by_bomメソッドが導入された

Ruby 2.7ではBOMを見て外部エンコーディングを判別できるset_encoding_by_bomメソッドが導入されました。
ただし、ファイルをバイナリモードで開いていなかったり、すでに外部エンコーディングが設定されていたりした場合はArgumentErrorが発生します。

# BOMが付いているファイルをバイナリモード("rb"の"b")で開く
io = File.open("test/fixtures/bom.txt", "rb")

# デフォルトの外部エンコーディングはASCII_8BIT
io.external_encoding
#=> Encoding::ASCII_8BIT

# encodingを判別する
io.set_encoding_by_bom
#=> Encoding::UTF_8

# 外部エンコーディングがUTF-8に変わる
io.external_encoding
#=> Encoding::UTF_8

以下はBOMが付いていないUTF-8のテキストファイルを開いた場合の挙動です。

# BOMが付いていないファイルをバイナリモードで開く
io = File.open("test/fixtures/no_bom.txt", "rb")

# デフォルトの外部エンコーディングはASCII_8BIT
io.external_encoding
#=> Encoding::ASCII_8BIT

# encodingを判別する(BOMがないのでnilが返る)
io.set_encoding_by_bom
#=> nil

# 外部エンコーディングはASCII_8BITのまま
io.external_encoding
#=> Encoding::ASCII_8BIT

Bug #15210: UTF-8 BOM should be removed from String in internal representation - Ruby master - Ruby Issue Tracking System

Integer

[]メソッドがビットの範囲を指定できるようになった

Ruby 2.7では[]メソッドを使って指定した範囲のビットの値を取り出せるようになりました。

# 0番目のビットから4桁ぶん取得する
0b01001101[0, 4]
#=> 0b1101 (10進数では13)

# 0番目から3番目までのビットを取得する([0, 4]と同じ)
0b01001101[0..3]
#=> 0b1101

# 整数値を指定すると、その位置のビットの値を返す(Ruby 2.6以前からの仕様)
0b01001101[0] #=> 1
0b01001101[1] #=> 0
0b01001101[2] #=> 1
0b01001101[3] #=> 1

Kernel

PathnameメソッドにPathnameオブジェクトを引数として渡すと引数そのものを返すようになった

Ruby 2.7ではKernel#PathnameメソッドにPathnameオブジェクトを引数として渡すと引数そのものを返すようになりました。

pathname = Pathname.new('/tmp')

# Kernel#PathnameにPathnameオブジェクトを渡すと引数自身が返る
Pathname(pathname).equal?(pathname)
#=> true

なお、上のコードでは大文字で始まるPathnameが2回出てきますが、前者がクラス名(定数)で、後者はメソッドである点に注意してください。

# Pathnameはクラス名(定数)
Pathname.new('/tmp')

# Pathnameは(大文字で始まっているが)メソッド
Pathname(pathname)

Method

inspectメソッドがメソッドの引数や定義場所の情報も返すようになった

Ruby 2.7ではMethod#inspectメソッドが、メソッドの引数や定義場所の情報も返すようになりました。

class SampleClass
  def example_for_inspect(a, b=nil, *c, d:, e: nil, **rest, &block)
  end
end

# Ruby 2.6
SampleClass.new.method(:example_for_inspect).inspect
#=> "#<Method: SampleClass#example_for_inspect>"

# Ruby 2.7
SampleClass.new.method(:example_for_inspect).inspect
#=> "#<Method: SampleClass#example_for_inspect(a, b=..., *c, d:, e: ..., **rest, &block) (your path)/sample_class.rb:2>"

Module

定数の定義場所を返すconst_source_locationメソッドが追加された

Ruby 2.7ではModule#const_source_locationメソッドを使って、定数の定義場所を確認できるようになりました。

module ConstantExample
  FOO = 123
end

# const_source_locationメソッドを呼ぶと、定義場所のパスと行番号が返る
ConstantExample.const_source_location(:FOO)
#=> ["(your path)/const_example.rb", 2]

autoload?メソッドがinheritフラグを受け取れるようになった(未執筆)

(僕は使い方がいまいちよくわからなかったので、@tmtmsさんの以下のブログ記事を参照してください?)

nameメソッドが常に凍結された文字列を返すようになった(試験的な変更)

Ruby 2.7ではModule#nameメソッドが常に凍結された文字列を返すようになりました。
ただし、これは試験的な変更です。

name = Time.name
p name #=> "Time"

# Ruby 2.6
name.frozen? #=> false

# Ruby 2.7
name.frozen? #=> true

Module / Proc

ruby2_keywordsメソッドが追加された(未執筆)

(僕は使い方がいまいちよくわからなかったので、@tmtmsさんの以下のブログ記事を参照してください?)

NilClass / TrueClass / FalseClass

to_sメソッドを呼ぶと凍結された文字列を返すようになった(試験的な変更)

Ruby 2.7ではnil/true/falseに対してto_sメソッドを呼ぶと、凍結された文字列を返すようになりました。
ただし、これは試験的な変更です。

s = nil.to_s
p s       #=> ""
# Ruby 2.7(Ruby 2.6ではfalse。以下同様)
s.frozen? #=> true

s = true.to_s
p s       #=> "true"
s.frozen? #=> true

s = false.to_s
p s       #=> "false"
s.frozen? #=> true

ObjectSpace::WeakMap

true/falseやシンボルなど、特別なオブジェクトをキーや値に指定できるようになった

Ruby 2.7ではtrue/falseやシンボルなど、特別なオブジェクトをWeakMapのキーや値に指定できるようになりました。

wm = ObjectSpace::WeakMap.new
x = 'Hello'

wm[true] = x
wm[true] #=> Hello

wm[nil] = x
wm[nil]  #=> Hello

wm[1] = x
wm[1]    #=> Hello

wm[:foo] = x
wm[:foo] #=> Hello

# Ruby 2.6ではエラーになる
wm[true] = x
#=> ArgumentError (cannot define finalizer for TrueClass)

Proc

to_sメソッドの戻り値に@が含まれなくなった

# Ruby 2.6
->{}.to_s 
#=> "#<Proc:0x00007fba7c952db8@(irb):36 (lambda)>"

# Ruby 2.7
->{}.to_s
#=> "#<Proc:0x00007fda4c934c38 (irb):41 (lambda)>"

なお、この変更は@ではなくスペースで区切られていれば、ターミナルでobject_idをダブルクリックしたときにobject_idだけをきれいに選択できるから、という理由で導入されたようです。

参考 Feature #16101: Proc#to_s returns "... file:line" instead of "...@file:line" - Ruby master - Ruby Issue Tracking System

RubyVM

RubyVM.resolve_feature_path$LOAD_PATH.resolve_feature_pathに移動した(未執筆)

(僕は詳しい内容がよくわからなかったので、@tmtmsさんの以下のブログ記事を参照してください?)

String

Unicodeバージョンが12.1.0に、Unicide Emojiバージョンが12.1になった

Ruby 2.7ではUnicodeバージョンが12.1.0に、Unicide Emojiバージョンが12.1になりました。

RbConfig::CONFIG['UNICODE_VERSION']       #=> 12.1.0
RbConfig::CONFIG['UNICODE_EMOJI_VERSION'] #=> 12.1

ちなみにUnicode 12.1では「令和」の合字が追加されています。

12.1 では日本の新年号「令和」の合字一文字のみを加えており、総文字数は 137,929 となりました。

The Unicode Blog: Unicode コンソーシアムは「令和」をサポートする Unicode 12.1 を正式リリースしました

Symbol

start_with?メソッドとend_with?メソッドが追加された

Ruby 2.7ではSymbolクラスにstart_with?メソッドとend_with?メソッドが追加されました。
文字通りレシーバであるシンボルが指定された文字列で始まっていれば(または終わっていれば)trueを、そうでなければfalseを返します。

:foo_bar.start_with?('foo') #=> true
:foo_bar.start_with?('oo')  #=> false

:foo_bar.end_with?('bar')   #=> true
:foo_bar.end_with?('ba')    #=> false

ちなみに引数は文字列で指定します。シンボルで指定すると例外が発生します。

:foo_bar.start_with?(:foo)
#=> TypeError (no implicit conversion of Symbol into String)

Time

ミリ秒以下を切り上げ/切り下げできるceil/floorメソッドが追加された

Ruby 2.7ではTimeクラスにミリ秒以下を切り上げ/切り下げできるceil/floorメソッドが追加されました。

# ミリ秒以下に10桁の値を持つTimeオブジェクトを作成する
t = Time.utc(2010, 3, 30, 5, 43, "25.0123456789".to_r)
t.iso8601(10)
#=> 2010-03-30T05:43:25.0123456789Z

# ミリ秒以下を切り上げる
t.ceil(0).iso8601(10)
#=> 2010-03-30T05:43:26.0000000000Z
t.ceil(5).iso8601(10)
#=> 2010-03-30T05:43:25.0123500000Z

# ミリ秒以下を切り下げる
t.floor(0).iso8601(10)
#=> 2010-03-30T05:43:25.0000000000Z
t.floor(5).iso8601(10)
#=> 2010-03-30T05:43:25.0123400000Z

inspectメソッドがミリ秒の情報も返すようになった

Ruby 2.7ではTime#inspectメソッドがミリ秒の情報も返すようになりました。

t = Time.new(2010, 3, 30, 5, 43, "25.0123456789".to_r, '+09:00')

# Ruby 2.6ではinspectもto_sも返ってくる文字列は同じ
t.to_s
#=> 2010-03-30 05:43:25 +0900
t.inspect
#=> 2010-03-30 05:43:25 +0900

# Ruby 2.7ではinspectメソッドがミリ秒以下の情報も返す
t.inspect
#=> 2010-03-30 05:43:25 123456789/10000000000 +0900

UnboundMethod

bindとcallを同時に行うbind_callメソッドが追加された

Ruby 2.7ではbindとcallを同時に行うUnboundMethod#bind_callメソッドが導入されました。

# https://github.com/ruby/ruby/blob/v2_7_0_rc2/NEWS から引用
class Foo
  def add_1(x)
    x + 1
  end
end
class Bar < Foo
  def add_1(x) # override
    x + 2
  end
end

obj = Bar.new
p obj.add_1(1) #=> 3
p Foo.instance_method(:add_1).bind(obj).call(1) #=> 2
p Foo.instance_method(:add_1).bind_call(obj, 1) #=> 2

標準ライブラリ関連のアップデート

Date

令和の和暦を"R01"のように表示したりパースしたりできるようになった

Ruby 2.7では令和の和暦を"R01"のように表示したりパースしたりできるようになりました。

require 'date'

reiwa_date = Date.new(2019, 5, 1)

reiwa_date.jisx0301
#=> R01.05.01

Date.parse("R01.05.01")
#=> #<Date: 2019-05-01 ((2458605j,0s,0n),+0s,2299161j)>

ERB

ERBインスタンスをマーシャリングしようとすると例外が発生するようになった

Ruby 2.7ではERBインスタンスをマーシャリングしようとすると例外が発生するようになりました。

require 'erb'

str = "hoge"
erb = ERB.new("value = <%= str %>")

# ERBインスタンスをマーシャリングすると例外が発生する
Marshal.dump(erb)
#=> TypeError (singleton class can't be dumped)

open-uri

Kernelのopenメソッドを使うと警告が出るようになった

Ruby 2.7ではopen-uriライブラリが提供しているopenメソッドを使うと警告が出るようになります。

require 'open-uri'

# open-uriが提供しているopenメソッドを使うと警告が出る
open("http://www.ruby-lang.org/")
#=> warning: calling URI.open via Kernel#open is deprecated, call URI.open directly

# 代わりにURI.openメソッドを使う
# (この場合も事前に'open-uri'ライブラリをrequireしておく)
URI.open("http://www.ruby-lang.org/")

なお、この変更はRuby 2.5のリリース時に予告されていました。

URI.open method defined as an alias to open-uri's Kernel.open. open-uri's Kernel.open will be deprecated in future.

https://github.com/ruby/ruby/blob/ruby_2_5/NEWS

Webページを開いた際のデフォルトの文字コードがUTF-8になった

Ruby 2.7ではWebページを開いた際のデフォルトの文字コードがUTF-8になりました。

require 'open-uri'

URI.open("http://www.ruby-lang.org/") do |f|
  puts f.charset #=> utf-8
end

OptionParser

不明なオプションが渡されると"Did you mean?"が表示されるようになった

Ruby 2.7のOptionParserでは不明なオプションが渡されると"Did you mean?"が表示されるようになりました。

option_parser_sample.rb
require 'optparse'

OptionParser.new do |opts|
  opts.on("-f", "--foo", "foo") {|v| }
  opts.on("-b", "--bar", "bar") {|v| }
  opts.on("-c", "--baz", "baz") {|v| }
end.parse!
$ option_parser_sample.rb --fooo
Traceback (most recent call last):
option_parser_sample.rb:7:in `<main>': invalid option: --fooo (OptionParser::InvalidOption)
Did you mean?  foo

Pathname

globメソッドにオプション引数としてbaseが渡せるようになった

Ruby 2.7ではPathname.globメソッドのオプション引数としてbase:が渡せるようになりました。
この引数はDir.globメソッド(Dir.[]メソッド)のbase:オプションに委譲されます。

[PARAM] base:
カレントディレクトリの代わりに相対パスの基準にするベースディレクトリを指定します。指定した場合、結果の頭にはベースディレクトリはつかないので、絶対パスが必要な場合はベースディレクトリを追加する必要があるでしょう。

singleton method Dir.[] (Ruby 2.6.0 リファレンスマニュアル)

require 'pathname'

path = '/tmp'

# /tmpディレクトリ以下のパスを表すPathnameオブジェクトの配列が返る
Pathname.glob("*", File::FNM_DOTMATCH, base: path)
#=> [#<Pathname:.>, #<Pathname:..>, ...]

その他

Ruby 2.7ではこれ以外にもさまざまな変更点がありますが、全部は説明しきれないのでNEWSのページに載っていた内容で、ここまでに説明していない情報を箇条書きにしておきます。

  • RubyGemsのバージョンが3.1.1になった(参考
  • Bundlerのバージョンが2.1.0になった(参考
  • エスケープ文字が含まれる場合のCGI.escapeHTMLメソッドの呼び出しが2倍から5倍速くなった(参考
  • CSVのバージョンが3.1.2になった(参考
  • Object#DelegateClass accepts a block and module_evals it in the context of the returned class, similar to Class.new and Struct.new.
  • Add Net::FTP#features to check available features, and Net::FTP#option to enable/disable each of them. [Feature #15964]
  • Add ipaddr optional parameter to Net::HTTP#start to replace the address for TCP/IP connection [Feature #5180]
  • Net::IMAP / Add Server Name Indication (SNI) support. [Feature #15594]
  • Racc / Merge 1.4.15 from upstream repository and added cli of racc.
  • Reline / New stdlib that is compatible with readline stdlib by pure Ruby and also has a multiline mode.
  • REXMLのバージョンが3.2.3になった(参考
  • RSSのバージョンが0.2.8になった(参考
  • StringScannerのバージョンが1.0.3になった(参考
  • 以下のライブラリがbundled gemでなくなり、明示的なgemのインストールが必要になった
    • CMath (cmath gem)
    • Scanf (scanf gem)
    • Shell (shell gem)
    • Synchronizer (sync gem)
    • ThreadsWait (thwait gem)
    • E2MM (e2mmap gem)
  • 以下のライブラリがdefault gemとしてrubygems.orgに公開された
    • benchmark
    • cgi
    • delegate
    • getoptlong
    • net-pop
    • net-smtp
    • open3
    • pstore
    • singleton
  • 以下のライブラリもdefault gemになった。ただし、rubygems.orgにはまだ公開されていない
    • monitor
    • observer
    • timeout
    • tracer
    • uri
    • yaml
  • Profiler__モジュールとprofile.rbが標準ライブラリから削除された(Ruby 2.0から誰もメンテナンスしていないため)
  • パフォーマンス、もしくは実装上の改善
    • Fiber
    • File.realpath
    • Hash
    • MonitorとMonitorMixinのパフォーマンスが改善された
    • Thread
    • JIT
  • その他の変更
    • RubyのビルドにC99に対応したコンパイラが必要になった
    • IA64アーキテクチャのサポートが削除された
    • 開発リポジトリがSubversionからGitに変更された(ただし、GitHubではなく https://git.ruby-lang.org/ruby.git でホストされている)
    • RUBY_REVISIONが数値ではなく文字列になった(リポジトリがSubversionからGitに変わった影響)
    • RUBY_DESCRIPTIONにSubversionのrevisionではなく、Gitのrevisionが含まれるようになった
    • Support built-in methods in Ruby with _builtin syntax.

まとめ

というわけで、この記事ではRuby 2.7で導入されたさまざまな新機能や変更点を説明しました。

いやあ、今年は例年以上に変更点が多いですね!!
「Rubyは死んだ」とか言ってるのはいったい誰なんでしょうか?

個人的にはEnumerable#tallyEnumerable#filter_mapの使用頻度が高くなりそうな予感がします。
Rubyって配列やハッシュ周りのメソッドがめちゃくちゃ豊富なのが好きです、僕は。

irbの進化もすさまじくて、簡単な動作確認がすこぶる効率アップしそうですね。

そして、今年も楽しいクリスマスプレゼントを届けてくれたMatzさんやコミッタのみなさんに感謝したいと思います。どうもありがとうございました!
みなさんもぜひRuby 2.7の新機能を試してみてください?

あわせて読みたい

本記事の中でもいくつか紹介しましたが、@tmtmsさんも毎年アドベントカレンダーでRubyの新機能を解説されています。
僕が説明を端折った機能も丁寧に解説されているので、こちらも一緒にチェックすることをお勧めします。

Ruby 2.7 Advent Calendar 2019 - Qiita

Ruby 2.3〜2.6の新機能は以下の記事にまとめてあります。
こちらもあわせてどうぞ。

PR: 本記事を読んでもよくわからなかったRuby初心者の方へ

「本文に一通り目を通してみたけど、なんかよくわからない用語がたくさん出てきて、イマイチちゃんと理解できなかった?」というRuby初心者の方は、拙著「プロを目指す人のためのRuby入門」(通称チェリー本)を読んでみてください。
本書の内容を一通り理解すれば、この記事の内容も問題なく読みこなせるはずです!

プロを目指す人のためのRuby入門|技術評論社
9784774193977.jpg

ちなみに本書の対象バージョンはRuby 2.4.1ですが、Ruby 2.5以降で発生する記述内容との差異は、それぞれ以下の記事にまとめてあります。なので、多少バージョンが古くても安心して読んでいただけます?

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

attr_accessorについて

はじめに

attr_accessorについて厳密に理解していないとこがあったので整理してみます

attr_accessorとはなんぞや

sample.rb
class Book
  def title
    @title
  end
  def title=(val)
    @title = val
  end

しっくり来ない

attr_accessorはattr_readerとattr_writerを合わせたものということは分かってる人は多いと思います。
なので、とりあえずattr_readerとattr_writerがそれぞれどのような役割があるのか見てみたいと思います。

超初学者の気持ちになって順をおって考えてみる

atter_reader

まずはBookクラスを用意

qiita.rb
 class Cafe

 end

インスタンスを生成

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")

「新たに作ったインスタンス変数@nameの名前を出力したいな。でもインスタンス変数を外部から扱うことことは出来ないな。」
当然インスタンス変数は外部から参照出来ないので以下のようにしてもエラーが出る
(こんなことやるやつ居ないと思いますが。)

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.@name 

ファイルを実行

#エラー
  syntax error, unexpected tIVAR, expecting '('
 puts cafe.@name
           ^~~~~

@nameを識別できません

と表示される

「なら、nameというインスタンスメソッドを新たに定義しよう。」

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end

    def name
        @name
    end
     #インスタンスメソッドnameを定義
 end

 cafe = Cafe.new("Star Backs")

nameメソッドを定義できたら、nameメソッドを使ってインスタンス変数を出力する

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end

    def name
        @name
    end

 end

 cafe = Cafe.new("Star Backs")

 puts cafe.name
 #nameメソッド

実行すると、ちゃんと出力される

$ ruby qiita.rb
Star Backs

つまり、インスタンス変数に外部から直接的に参照出来ないならば、インスタンスメソッド経由で間接的にインスタンス変数を参照しようという訳だ

でも一々インスタンスメソッド定義するのは面倒臭い

そこで登場するのがattr_reader

先ほど定義したnameメソッドは以下のように置き換えることができます

qiita.rb
 class Cafe
    attr_reader :name
    #atter_readerに置き換える

    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name

これは楽ですね

atter_reader

今度はカフェの名前をTully'sに変更したいとします。
つまり、インスタンス変数@nameをStar BacksからTully'sに変更します。

qiita.rb
 class Cafe
    attr_reader :name

    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
#Tully'sをインスタンス変数に再代入
 cafe.name = "Tully's"

これを実行しても

undefined method `name' for #<Cafe:0x0000000001b6dae0 @name="Star Backs"> (NoMethodError)

nameメソッドはカフェクラスに定義されていません。

と表示されます。
インスタンス変数の値の変更はクラス内でしか出来ないので当然ですね。

ならば、先ほど同様にインスタンスメソッド経由で変数に参照できるようにします。

qiita.rb
 class Cafe
    attr_reader :title

    def initialize(name)
        @name = name
    end
    def name=(val)
        @name=(val)
    end
    #name=メソッドを定義
    #引数で値を渡せるようにする 今回の場合は"Tully's"
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = "Tully's"

qiita.rb
 class Cafe
    attr_reader :name

    def initialize(name)
        @name = name
    end
    def name=(val)
        @name=(val)
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = "Tully's"
 puts cafe.name
 #代入し直したインスタンス変数nameを出力

実行してみる

 $ ruby qiita.rb
  Star Backs
  Tully's

大丈夫そうです
でも、これもいちいち定義するの面倒臭い

そこで登場するのがattr_writer

先ほど定義したname=メソッドは以下のように置き換えることができます

qiita.rb
class Cafe
    attr_reader :name
    attr_writer :name
    #attr_writerに置き換える
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = ("Tully's")
 puts cafe.name

さらにattr_readerとattr_writerをまとめて1行にまとめられます。

qiita.rb
class Cafe
    attr_accessor :name
    #attr_accessorに置き換える
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = ("Tully's")
 puts cafe.name

まとめ

インスタンス変数を外部から参照することは出来ません
従って、外部から間接的に参照するためにメソッドを用意する必要があります
そのメソッドを1行で簡略化したものがattr_readerattr_writerです

さらにそのattr_readerとrattr_writerを1行に簡略化したものがattr_accessorということですね

 

もし間違っていることがあればご享受頂けると幸いです。

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

attr_accessorを順を追って理解してみた

はじめに

去年attr_accessorについてまとめた物がqiitaの下書きに眠っていたので投稿しておきます

attr_accessorとはなんぞや

attr_accessorはattr_readerとattr_writerを組み合わせたやつです
クラス外からインスタンス変数に参照したり変更する時に使われます

attr_reader

huga.rb
class Huga
    def hoge
        @hoge
    end
end

attr_writer

huga.rb
class Huga
    def hoge=(val)
        @hoge=(val)
    end
end

attr_accessor

huga.rb
class Huga
    def hoge
        @hoge
    end
    def hoge=(val)
        @hoge=(val)
    end
end

いきなりコードだけ見てもしっくりこないですねattr_readerattr_accessorについて順に考えてみます

順を追って考えてみる

atter_reader

まずはCafeクラスを用意

qiita.rb
 class Cafe

 end

インスタンスを生成

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")

「新たに作ったインスタンス変数@nameの名前を出力したいな。でもインスタンス変数を外部から扱うことことは出来ないな。」
当然インスタンス変数は外部から参照出来ないので以下のようにしてもエラーが出る
(こんなことやるやつ居ないと思いますが。)

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.@name 

ファイルを実行

#エラー
  syntax error, unexpected tIVAR, expecting '('
 puts cafe.@name
           ^~~~~

@nameを識別できません

と表示される

「なら、nameというインスタンスメソッドを新たに定義しよう。」

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end

    def name
        @name
    end
     #インスタンスメソッドnameを定義
 end

 cafe = Cafe.new("Star Backs")

nameメソッドを定義できたら、nameメソッドを使ってインスタンス変数を出力する

qiita.rb
 class Cafe
    def initialize(name)
        @name = name
    end

    def name
        @name
    end

 end

 cafe = Cafe.new("Star Backs")

 puts cafe.name
 #nameメソッド

実行すると、ちゃんと出力される

$ ruby qiita.rb
Star Backs

つまり、インスタンス変数に外部から直接的に参照出来ないならば、インスタンスメソッド経由で間接的にインスタンス変数を参照しようという訳だ

でも一々インスタンスメソッド定義するのは面倒臭い

そこで登場するのがattr_reader

先ほど定義したnameメソッドは以下のように置き換えることができます

qiita.rb
 class Cafe
    attr_reader :name
    #atter_readerに置き換える

    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name

これは楽ですね

atter_reader

今度はカフェの名前をTully'sに変更したいとします。
つまり、インスタンス変数@nameをStar BacksからTully'sに変更します。

qiita.rb
 class Cafe
    attr_reader :name

    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
#Tully'sをインスタンス変数に再代入
 cafe.name = "Tully's"

これを実行しても

undefined method `name' for #<Cafe:0x0000000001b6dae0 @name="Star Backs"> (NoMethodError)

nameメソッドはカフェクラスに定義されていません。

と表示されます。
インスタンス変数の値の変更はクラス内でしか出来ないので当然ですね。

ならば、先ほど同様にインスタンスメソッド経由で変数に参照できるようにします。

qiita.rb
 class Cafe
    attr_reader :title

    def initialize(name)
        @name = name
    end
    def name=(val)
        @name=(val)
    end
    #name=メソッドを定義
    #引数で値を渡せるようにする 今回の場合は"Tully's"
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = "Tully's"

qiita.rb
 class Cafe
    attr_reader :name

    def initialize(name)
        @name = name
    end
    def name=(val)
        @name=(val)
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = "Tully's"
 puts cafe.name
 #代入し直したインスタンス変数nameを出力

実行してみる

 $ ruby qiita.rb
  Star Backs
  Tully's

大丈夫そうです
でも、これもいちいち定義するの面倒臭い

そこで登場するのがattr_writer

先ほど定義したname=メソッドは以下のように置き換えることができます

qiita.rb
class Cafe
    attr_reader :name
    attr_writer :name
    #attr_writerに置き換える
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = ("Tully's")
 puts cafe.name

さらにattr_readerとattr_writerをまとめて1行にまとめられます。

qiita.rb
class Cafe
    attr_accessor :name
    #attr_accessorに置き換える
    def initialize(name)
        @name = name
    end
 end

 cafe = Cafe.new("Star Backs")
 puts cafe.name
 cafe.name = ("Tully's")
 puts cafe.name

まとめ

インスタンス変数を外部から参照することは出来ません
従って、外部から間接的に参照するためにメソッドを用意する必要があります
そのメソッドを1行で簡略化したものがattr_readerattr_writerです

さらにそのattr_readerとrattr_writerを1行に簡略化したものがattr_accessorということですね

 

もし間違っていることがあればご享受頂けると幸いです。

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

Railsにマークダウン記法を取り入れる方法

結論

1,2,3を導入すればいい

1. gem 'redcarpet'

2.

module MarkdownHelper
  def markdown(text)
    options = {
      filter_html:     true,
      hard_wrap:       true,
      space_after_headers: true,
      with_toc_data: true
    }

    extensions = {
      autolink:           true,
      no_intra_emphasis:  true,
      fenced_code_blocks: true,
      tables:             true
    }

    renderer = Redcarpet::Render::HTML.new(options)
    markdown = Redcarpet::Markdown.new(renderer, extensions)
    markdown.render(text).html_safe
  end

end

3 .

<%= markdown(@article.body).html_safe %>

参考
https://musicamusik.hatenablog.com/entry/2018/06/09/181439

エラーが出た場合

gemlockを確認するとgem 'redcarpet'がバージョン指定されている可能性があります。自分の場合、gemlockのバージョン指定を削除してbundle updateすることで解決しました。

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

Rails6 each文にてeachがメソッドとして扱われて困った話

目的

  • each文を使用してカラム内容を順に出力したい時にeachがメソッドとして扱われてしまって困ったときの解決法をまとめる

結論

  • each文で繰り返し処理の対象の変数に格納されている値を配列状態で格納する。

エラーの内容とエラー時のコード

  • 当該ページは@posts.contentに格納されている内容をeach文で次々渡し、表示する処理がある。
  • 当該ページへアクセスしたところ下記のエラーが発生した。

    undefined method `each' for #<Post:0x00007fac1ebd28e8>
    
  • エラーに関係するルーティングファイルの内容を記載する。

    get "posts/index/:id" => "posts#index"
    
  • エラーに関係するpostsコントローラファイルの内容を記載する。

    def index
      @posts = Post.find_by(id: params[:id])
    end
    
  • エラーに関係するindexビューファイルの内容を記載する。

    <% @posts.each do |post| %>
      <%= post.content %>
      <%= link_to("詳細", "/posts/#{post.id}") %>
    <% end %>
    

解決方法

  • 修正を行なったコードのみ下記に記載する。

    def index
      @posts = Post.where(id: params[:id])
    end
    
  • find_byメソッドは一致した最初のものを一つだけ返す。

  • whereメソッドは一致した全てのものを返す。

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

form_tag,form_withと空のnewアクション

はじめに

今スクールでpictweetという、写真とツイートを一緒に投稿できる、簡単な練習用のwebサービスをつくっているのですが、ふと気になったことがあったので記事にしてみました。

form_tag,form_withとnewアクション

MVCモデル学習し、ツイートが投稿されたときの流れも理解していたつもりでした。
投稿されたデータがデータベースに保存されるためにはnewアクションでインスタンス変数を定義しなければいけないと思っていたのですが、、、
サンプルコードを見ると

qiita.rb
def new
end

え???newアクションの中身なにもないやん!(◎◎;)
でもよく似たwebサービスのサンプルコードではpostsコントローラに「@post = Post.new」としっかりインスタンス変数が定義されていました。
何が違うの?
と思って数時間かけて調べた結果,HTMLでform
tagを使用していた場合は、newアクションでインスタンス変数は定義しなくていいみたいです。
逆にform_withを使用する場合は@モデル名で定義しなければいけないみたいです。

もう少し調べるとform_forというのも出てきて、そもそもform_forとform_tagは投稿されたデータをデータベースに保存するかしないかで使い分けるんですね。
ただ新しく作られたform_withは両方の用途で使用し、またセキュリティ面でも有能のようです。

結果、これからは投稿フォーム系にはform_withを使うのがベストというのが今回の学びです。

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

Rustにチャレンジ 2019/12/4 adventcalender

今日は、AdventCalenderの4日目になります

Rust?

最近、SNSで話題になっているなーと思いつつ、せっかくなので空き時間で
チャレンジしてみました( ゚Д゚)

使いやすい?

学習して合計5日ぐらいなので「俺たちはわからない、感覚で書いている」という状態です。

環境について

RustのPlayGround ( https://play.rust-lang.org/ ) を使いました

今回作ったときの手順

1.過去に作っていたプログラミング言語で簡単なプログラムを用意する
お仕事のソースコードとかは持ってこれないので、ある程度書きなれている言語で
お題を考えながら用意してみる

僕は、Ruby(普段PHPやPythonのLL系が多いので)で以下のようなコードを用意しました

# Your code here!

# XR簡易ゲームのコンソールイメージで3択ゲームとして実装


# じゃんけんなので、0=グー、1=ちょき、2=パー

srand(Time.now.to_i)
player_value = rand(3)
# issue 正式版は、標準入力から取得をする
# ローカルだとタイムアウトするので、sleepはコメントアウトする

puts "じゃんけんを開始するよ"

# 入力の代わりに3秒待たせる
#sleep(3)

puts "それじゃあ、いくよ"

#sleep(1)

puts "じゃーんけん"

# 5秒ぐらい待っている
#sleep(5)


puts "ぽんっ"

# お互いに手を繰り出すモーションの待機時間
#select(2)



# 判定をする
def judge(select_value)
  # CPU側のじゃんけんをランダムで実行をする
  # issue 倍率によって、じゃんけんで使う手立ての確率を変更する
  srand(Time.now.to_i)
  cpu_value = rand(3)
  result_hash = Array.new(3, Array.new(3, ""))
  # issue 定数の初期化で一括記載
  result_hash[0][0] = "あいこだよ"
  result_hash[0][1] = "勝ちだよ"
  result_hash[0][2] = "負けだよ"
  result_hash[1][0] = "負けだよ"
  result_hash[1][1] = "あいこだよ"
  result_hash[1][2] = "勝ちだよ"
  result_hash[2][0] = "勝ちだよ"
  result_hash[2][1] = "負けだよ"
  result_hash[2][2] = "あいこだよ"
  hands = Array.new(3, "")
  hands[0] = "ぐー"
  hands[1] = "ちょき"
  hands[2] = "ぱー"
  puts "プレイヤー:%s CPU : %s 結果は %s" % [ hands[select_value] , hands[cpu_value] , result_hash[select_value][cpu_value] ]
  puts select_value
  puts cpu_value
end


# じゃんけんで入力のフェイクとしてスリープをする
puts judge(player_value)

Rustに移植してみたバージョンです

extern crate rand;
use rand::Rng;
use std::process::Command;
use std::string;
use std::vec;


fn main() {
//    let mut rng = rand::thread_rng();
//    let i: i32 = rng.gen();

    game_opening();

    // 2秒待機

    // じゃんけん判定処理を呼び出す
    judge(0);


}


fn waitfor_value(_w: string::String) -> () {
  let mut child = Command::new("sleep").arg(_w).spawn().unwrap();
  let _result = child.wait().unwrap();
}


// ゲームオープニング処理
fn game_opening() {
    println!("じゃんけんを開始するよ");

    // 3秒
    let before_string = "3";
    waitfor_value(before_string.to_string());

    println!("それじゃあ、行くよ");

    // 1秒
    let start_string = "1";
    waitfor_value(start_string.to_string());


    println!("じゃーんけん");

    // 5秒
    let action_string = "5";
    waitfor_value(action_string.to_string());

    println!("ぽんっ");

}

// じゃんけん判定処理
fn judge(_s: i32) -> () {

  // CPU側のじゃんけんをランダムで実行をする

  // じゃんけんで、0~2を指定
  let num = rand::thread_rng().gen_range(0, 2);

  // CPU手数
  let cpu_num = rand::thread_rng().gen_range(0, 2);

  // じゃんけん判定用配列を用意
  let display_name = vec!["グー", "チョキ", "パー"];
  let display;
  // グーの場合
  // issue 配列周りをplay-ground rustで書けるようにして、きれいにする
  if num == 0 {
    if cpu_num == 0 {
        display = "あいこ";
    } else if cpu_num == 1 {
        display = "かち";
    } else {
        display = "まけ";
    }
  } else if num == 1 {
    if cpu_num == 0 {
        display = "まけ";
    } else if cpu_num == 1 {
        display = "かち";
    } else {
        display = "あいこ";
    }
  } else {
    if cpu_num == 0 {
        display = "かち";
    } else if cpu_num == 1 {
        display = "まけ";
    } else {
        display = "あいこ";
    }
  }

  // じゃんけんの結果を表示する
  //  puts "プレイヤー:%s CPU : %s 結果は %s" % [ hands[select_value] , hands[cpu_value] , result_hash[select_value][cpu_value] ]
  println!("プレイヤー: {} CPU : {} 結果は {} だよ", display_name[num].to_string(),  display_name[num].to_string(), display.to_string());
  //  puts select_value
  //  puts cpu_value


}

改めて自分自身での移植をやってみてどうだったの

1.お題があると、プログラム言語の習熟に集中しやすい

言語の仕様を把握していない段階なので、サンプルがない状態でうーんと考えるよりは”これを移植しよう”って考えると言語学習に集中できました。

2.移植元があると、参考にしやすい

これは、他のアプリでもいいのですが参考になるものがあると挙動が違うな等
の参考になりました

  1. ちょっと転職とか、お話がしやすそう 移植業務が多かった方向けですが、"双方の言語が読める"と、"移植元と移植先の動きを確認しながら差異がある場合は、相談したり提案してくれる"という指標でポートフォリオに入れてもいいかなと思いました。

*コード修正やリプレースが多かったので

実行結果

youtubeのURLだけちらっと公開

この記事のissue

フローチャート追加する予定です
*ほかの日が終わったら、

以上、次回はyumemi_vueの女の子を用意して後半レポートを書くよ( ゚Д゚)

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

Rubyであたまの体操

あたまの体操してますか?

  • 以下は、 プログラマ脳を鍛える数学パズルを写経したときのメモです。回答例はRubyで記述したものです。もっとRubyのメソッドを活用すればきれいに書けるZE☆などあったら、コメントください。

  • Q1〜Q3まであります。前提条件の後に問題が表示されます。回答例を見る前にコーディングしてみると、あたまの体操になります。(たぶん)

Q1.カードを裏返したい

【前提】
1〜100までの番号が書かれた100枚のカードが1から順番に並んでいます。最初、すべてのカードは裏返しの状態になっています。ある人が2番目のカードから、1枚おきにカードを裏返していきます(2,4,6...100のカードが表を向く)。次に3番めのカードから2枚お気にカードを裏返していく。(表向きのカードは裏返され、裏向きのカードは表を向く)。このようにn番目のカードからn-1枚おきにカードを裏返す操作を、どのカードの向きも変わらなくなるまで続けたとします。

【問題】
カードの向きが変わらなくなったとき、裏向きになっているカードの番号をすべて求めてください。

【回答例】

# 100枚のカードを初期化
N = 100
cards = Array.new(N, false)

# 2〜Nまで裏返す
(2..N).each do |i|
  j= i -1
  while (j < cards.size ) do
    cards[j] = !cards[j]
    j += i
  end
end

# 最後に裏向きのカードを出力する
N.times do |i|
  puts i + 1 if !cards[i]
end

【考え方】
左から順番に処理していけばOK。
表向きを「true」, 裏向きを「false」とすることで、反転を表現できる。

Q2.棒を切り分けたい

【前提】
長さn[cm]の一本の棒を、1cm単位に切り分ける。一本の棒を一度に切ることができるのは、1人だけ。例えば、切り分けられた棒が3本あるとき、同時に3人で切ることができる。
最大m人の人間がいるとき、最短で何回切り分けられるかを求めてください。

【問題】
n=100, m=8のときの回数を求めよ。

  # currentは現在の棒の数
def cutbar(m, n , current)
  # 切り終えたとき
  if current >= n then
    0
  # 切る人より、現在の棒の数が少ない時、切れる
  elsif current < m then
  # 切れたら現在の2倍になる
    1 + cutbar(m, n, current * 2)
  else
  # 切る人の数だけ増加
    1 + cutbar(m, n, current + m)
  end
end

puts cutbar(8, 100, 1)

【考え方】
一本の棒を切れるのは1人だけ。棒を切れるときの条件を整理する。
(問題を逆に読み、1cmの棒をm人で繋いで、n[cm]の棒を作ることとも読み替えられる→別解を考えてみよう!)

Q3.どうしても現金で払いたい

【前提】
バスに設置されているような、両替機を想定してください。この機械では、10円、50円、100円、500円の組み合わせで両替することができる。両替する際に、使われない硬貨があっても構いませんが、大量の小銭は持て余すので、最大で15枚となるように両替します。例えば、「50円玉、20枚」という両替はNG。

【問題】
1,000円札を入れた時の、硬貨の組み合わせは何通りになるか?

  • 回答例[強引ver.]
count = 0
(0..2).each do |y500|
  (0..10).each do |y100|
    (0..15).each do |y50|
      (0..15).each do |y10|
        if y500 + y100 + y50 + y10 <= 15 then
          if y500 * 500 + y100 * 100 + y50 * 50 + y10 * 10 == 1000 then
            count +=1
          end
        end
      end
    end
  end
end
puts count
  • 回答例[メソッド活用ver.]
coins = [10, 50, 100, 500]
count = 0
(2..15).each do |i|
  coins.repeated_combination(i).each do |coin_set|
    count +=1 if coin_set.inject(:+) == 1000
  end
end
puts count

おわりに

いかがでしたか?
今回は、ちょっとあたまの体操がしたいなという時に解いてみたいpuzzleをご紹介しました。
ぜひこの機会にあたまをやわらかくしてみてくださいね。(こちらの記事の情報は記事更新時点のものです。事前に最新のあたまの体操情報をご確認ください)

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

Rails の scope 便利手帳

Ruby on Rails で、使いやすいScopeをまとめました。
Model の設計によって差異があるものの、視点は使えるのでは?と思います。

Scopeとは

Model に scope を記載することで、Controllerなどから、複雑なWhere条件を書かずにデータを取りだせるようになります。

app/model/article.rb
class Article < ApplicationRecord
  scope :newer, ->{order("updated_at DESC") }
end

これによって、Controllerなどで

app/controllers/articles_controller.rb
@articles = Article.newer

で、更新日が新しい順に取得することができます。

app/controllers/article.rb
@articles = Article.newer.limit(5)

とすると、更新日が新しい順に5件を取得できます。

公開されているか

  scope :published, -> { where(published: true) }

ある時間より前か

  scope :created_before, ->(dt) { where("created_at < ?", dt) }

呼び出し元では、引数に時間をいれる

@articles = Article.created_before(Time.now)

終了状態であるか

終了日の有無で判定する場合

  scope :finished, ->{where("finished_at IS NOT NULL") }

その他追記予定

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

【チーム開発】【GitHub】プログラミング未経験の営業が、GitHubでチーム開発を経験してみたお話

~はじめに~

この記事なに?

社会人で営業の経験しかしてこなかった26歳が、プログラミング独学で勉強して早半年。
MENTAというサービスで現役Railsエンジニアの方に教えて頂き、ちょっとだけチーム開発の経験をさせて頂きました。
『アウトプットが大切』ということを周りから聞いてますので頑張って書いてます。
~未経験の方に向けてチーム開発の大まかな流れが分かっていただければと思います~


自分のレベル感

  • 営業歴8年…お話は得意です。特にテキト〜な話は大好物。
  • Railsチュートリアル2周ほど
  • GitとGithub使って2ヶ月ほど
  • 基本情報技術者試験ぎりぎり合格。。。
  • チーム開発は、もちろん未経験です。

で、なにするの?

https://github.com/Matsushin/qiitan
こちらQiitaを模倣したサイト『Qiitan』
過去MENTA生徒の方々と、Railsエンジニアの先生がコツコツ作ってきた汗と涙の結晶!

こちらのサイトへ『追加機能を実装』していきます!
~現場によってやり方は様々ですので今回はあくまで一例になります~

0: 前提条件

gitのインストールとgithubへのアカウント登録は済んでいること。

1: GithubからCloneして、ローカルリポジトリを作成

1112hiroki_action_text_sample.png
1:上記画像参照頂き、緑のボタンを押下
2:URLをそのまま全てコピー(一番右にあるボタンでもコピー可能)
3:ターミナルでコピペして下記を実行 ↓

$ git clone https://github.com/xxx/xxx.git

2: 作業用のブランチを作成する

そのまま作業をするとmasterブランチに直接コードを書いてしまう為、自分の実装内容にあったブランチ名をつけて作成を行う。

$ git checkout -b [ブランチ名]

  checkout → ブランチを移動する時に使う。
  -b     → checkoutに-b (-branchの略)をつけることで、ブランチの作成+移動を行う。

3:Githubで『実装進捗の見える化』

Githubのの進捗管理として[Project]を設定でき、その中で自分の割り当てられたタスクをドラックして指定の位置へ。
この様にすることで『誰が今なんの機能を実装しているか』を把握することが出来ます。

Projects_·_Matsushin_qiitan.png
1-2.png

4: 作業用のブランチで実装スタート

先程作成した作業用ブランチで機能実装開始!

●実装に一区切りがついたら → addしてcommitをしよう!

$ git add .
 → 作業したファイルを全てステージングに乗せる
$ git commit -m [メッセージの入力]
 → 上記の[]内に、終了したタスクのコメントを簡単に入力

●与えられた機能の実装が全て終了したら → pushをしてGithubに上げよう!

$ git push origin [ブランチ名]
 → Github上に自分のブランチをpushで上げる事ができる。

※今回の『Qiitan』では[CircleCi]を使用してますので、GithubにPushしたらCircleCiのテストが作動します。
URLにアクセスして、テストが通ることを確認してみましょう↓↓↓
7.png

5: Githubにpushをしたら、プルリクエストを

8.png
9.png

6: プルリクエスト後、[Project]内のタスク移動を忘れずに!

[Project]を開き、再度タスクをドラックして[Review]へ
あとは、リードプログラマのコードレビューを待ちます。
2-2.png

7: コードレビューをもらったら一つずつ修正を行っていく

image.png

~コードを全て修正後、再度push⇔コードレビューを繰り返します~

4-2.png


こんな感じです

8: コードレビューが全て完了したら、masterにマージする

image.png

9: 最後に忘れずに、[Project]内のタスクを移動して終了

6-2.png

こうすることで自分が実装した機能が、本流へ追加されます!
お疲れさまでした!

〜最後に:チーム開発体験を終えて〜

チーム開発の大枠の流れを把握することが出来ました。
実際に既存のコードをさわる際に、既存コードの流用や真似をして記載するようにしていたが、
周りのコードに影響が出ないか。また必要であれば既存のコードへの加筆をしてもいいか、判断に迷いました。

現在は個人開発しかしておりませんので、ブランチ名もクラス名もテキトーに付けてしまっている部分もありますが、
チーム開発ではそれは出来ません。

日頃からそちらも意識して開発して参りますm(_ _)m

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

明白かつ現在のSlack:Slackの各種イベントを監視するbotを気軽に作れるようにした

この記事は『Slack Advent Calendar 2019』24日目の記事です。

:rocket: 3行でおk

  • Slack上でのチャンネル作成などのイベントを検知するbotが欲しい
  • 手軽にRubyでbotが作れる mobb というライブラリを使いたい
  • mobb上で簡単に作れるreppハンドラをつくったよ

:muscle: モチベーション

※このセクションはポエムなので、興味がない方とQiitaの運営の方は読み飛ばしてください

Slackで何が起こっているか、知ってますか?

Slackはとても便利な ゲームプラットフォーム チャットツールです。
複数拠点をまたいだやり取りや、リモートワークなどを支える
重要なコミュニケーションツールとしての地位を確立しつつあります。

一方、会社や学校、大規模コミュニティなど多くの人が所属するチームで使うようになると
全体を見渡す、どこで何が起きているのかを認識することは難しくなります。

ですが、Slackは多くのAPIを提供しています。
特に Real Time Messaging APIEvents API を利用することで、
Slack上で起きたイベント(メンバーの追加、チャンネルの追加など)を認識することができます。
bot.jpg
※botがReal Time Messaging APIを受け取り、その内容を元に人間にわかりやすく発言している図
新しいメンバー・チャンネル・絵文字の追加や削除などが通知されます

ただし作るのは微妙に面倒くさい

SlackのAPIは程よく難解です[要出典]
Events APIを利用するためにはHTTPSのエンドポイントを用意してメッセージを受け取る必要があります。
ちょっと10分くらいいじってみよ~という気持ちで触るにはハードルが高いですよね……。

最も手軽なのは Bot integration での Real Time Messaging API の利用ですが、
Slack初心者が挑むとAPIレスポンスの反撃を受けることがあります。

sinatraライクなmobbを利用したい

Rubyでしっかりとしたbotを作るのであれば Ruboty という選択肢があります。
すでに様々なアダプタが提供されているため、多くのチャットツールでも利用できます。

一方、僕は別にお仕事でbotを作るわけではなく、
サクっと書いてサクっと動かしたい気持ちが強いです。

そこで Mobb を使います。
Mobb は Sinatra にインスパイアされて作られたbotフレームワークで、ほぼ Sinatra のような書き味でbotを作ることができます。

require 'mobb'
set :service, 'slack'

on 'ping' do
  'pong !'
end

5秒で書けそう!

:robot: MobbでSlack監視botをつくる

Reppハンドラ?

Mobb で作られたbotの通信処理は Repp の中に定義されています。
Repp はいわゆる Rack のbotフレームワーク版のようなものとのことです。

Reppは標準でSlackハンドラを提供しているのですが、
あくまでチャット用途のものであり、通常のメッセージしか対象ではありません。

ところで、実は Mobb/Repp の作者の方が既にSlack監視botを作成して公開しています。

この中身を見てみると、このbot専用にイベントだけを通知するSlackハンドラを定義して、
それを元にメッセージを送信しているようです。

これをそのままパクるのもありですが、どうせなら普通の会話もイベント監視もできる
便利でカワイイbotを作れたら嬉しいので、今回はイベントにも対応したSlackハンドラを作ってみました!

repp-heartful_slack をつくりました

今回つくった、ReppのSlackハンドラです。
Repp標準のSlackハンドラとの違いは、通常メッセージだけでなくイベント通知も取得できるようになっています。

このハンドラを利用すると、以下のような形で書くことができます。

require 'mobb'
require 'repp/heartful_slack'

set :service, 'heartful_slack'

# 通常メッセージだけを絞り込むフィルタ
set :on_message do |_|
  condition { @env.is_a?(::Repp::HeartfulSlack::MessageReceive) }
end

# イベントメッセージだけを絞り込むフィルタ
set :on_event do |_|
  condition { @env.is_a?(::Repp::HeartfulSlack::EventReceive) }
end

# 通知先の設定
# イベントは発言元のチャンネルがないので、これでチャンネル名を設定します
set :to_notify do |_|
  dest_condition do |res|
    res.last[:channel] = ENV['NOTIFY_CHANNEL'] # ex. Cxxxxxxx
  end
end

# 通常メッセージ
on 'ping', on_message: true do
  'pong'
end

# イベント通知(チャンネルの追加)
# 参考: https://api.slack.com/events/channel_created
on 'channel_created', on_event: true, to_notify: true do
  "created -> <##{@env.raw.channel.id}>" # 「created -> #hoge」のように投稿される
end

これで普通のbotとしての振る舞いもできるし、
イベント通知もできるbotが簡単に書けるようになります!1ハートフルですね!

なお、残念なことにまだ全イベントには対応できていません。
イベントの種類が膨大なのと、そのイベントをどうやって使うかが想像しきれなかったものは省いています。

もし、このイベントにこういう形で対応したら便利!というのがあれば
PullRequestもらえると嬉しいです。

現時点でできることは以下のサンプルコードに書いてあります。

:pencil2: 最後に

こういったイベント監視botは便利で愛されますが、
一部の人からは思いっきり嫌われます :dark_sunglasses:
大きい組織に導入する場合は、強い心を持って導入するか、
自分だけが見られるチャンネルにこっそり導入しましょう。

用法、容量を守って楽しくハッピーSlacking!

:book: 参考文献


  1. メッセージとイベントが混ざって飛んでくるせいで、フィルタを毎回書かないといけなくなってることから目を背けながら 

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