Rubyのsendとinjectを使って短くスッキリしたコードを書く

こんにちは!kossyです!



1ヶ月ほど更新できていませんでした。。。
大きめの機能追加に目処がついたため、少し余裕ができました。
これから可能な限り、ブログを更新したいと思います。




さて、今回はRubyのsendメソッドとinjectメソッドを使って、短いコードを書く方法をブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




sendメソッドとinjectメソッド

まず、両メソッドについて説明します。

と言っても、公式リファレンスのリンクを貼るだけですが、、、

sendメソッド
https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

オブジェクトのメソッド name を args を引数にして呼び出し、メソッドの実行結果を返します。

ブロック付きで呼ばれたときはブロックもそのまま引き渡します。

send が再定義された場合に備えて別名 __send__ も用意されており、ライブラリではこちらを使うべきです。また __send__ は再定義すべきではありません。

send, __send__ は、メソッドの呼び出し制限にかかわらず任意のメソッドを呼び出せます。 クラス/メソッドの定義/呼び出し制限 も参照してください。

引用: https://docs.ruby-lang.org/ja/latest/method/Object/i/__send__.html

メソッドを動的に実行したい時なんかに使ったりしますね。
「黒魔術」と揶揄されたりもしてます。https://qiita.com/Yuki_Nakagami/items/21319c10e833cd0efc12

今回のブログでは、ゴリゴリ使います。


injectメソッド
https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

リストのたたみこみ演算を行います。

最初に初期値 init と self の最初の要素を引数にブロックを実行します。 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。

要素が存在しない場合は init を返します。

初期値 init を省略した場合は、最初に先頭の要素と 2 番目の要素をブロックに渡します。また要素が 1 つしかなければブロックを実行せずに最初の要素を返します。要素がなければブロックを実行せずに nil を返します。

引用: https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/inject.html

金額の合計値とかを出したい時によく使ったりします。
当たり前ですが、配列やハッシュをカスタマイズして新しい値を返したい時がユースケースとしては一番多いですね。


前提

家計簿アプリ的な物を例にしてみます。

・Accountsテーブル(家計簿そのもの)
・コストごとにテーブルが分かれているとします。(今回は「食費」「光熱費」「交際費」のみとします。)



食費モデル(MealCost)
光熱費モデル(UtilityCost)
交際費モデル(EntertainmentCost)

は、

家計簿モデル(Account)に属し(belongs_to)、

家計簿モデル(Account)はそれぞれのコストをhas_manyする関連があるとします。

それぞれのコストは、dateとpriceとnameをattrとし持っているとします。

前提は以上です。


メソッド定義

その月のコスト全てを合計した総額を返すようなメソッドを定義してみます。

普通に考えると、

class Account
  def total_price(target_month)
    meal_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, meal| sum += meal.price } + utility_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, utility| sum += utility.price } + entertainment_costs.where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, entertainment| sum += entertainment.price }
  end
end

みたいなメソッドを定義することになるかなぁ、、、(異論は認めます)


でも、コストは上記の3種類以外にもありますし、コスト項目が増えると見通しも悪くなります。

そこで、sendを駆使してみます。

class Account
  ITEM_COSTS = [
    'meal',
    'utility',
    'entertainment'
  ]

  def total_price(target_month)
    ITEM_COSTS.inject(0) do |total, item|
      total += self.send("#{item}_costs").where('? <= date && date < ?', target_month.to_time.beginning_of_month, target_month.to_time.end_of_month).inject(0){|sum, _item| sum += _item.price }
    end
  end
end

中身がコスト名の定数ITEM_COSTSを定義し、total_priceメソッド内でsendメソッドとinjectメソッドを使って合計額を算出する形に切り替えました。

これなら、コスト項目が増えてもITEM_COSTSに足すだけでいいので、変更にも強いかなと思います。

Railsのenumで定義した値がenum内の値かどうかを検証する

こんにちは!kossyです!




さて、今回はRailsにおいて、enumで定義した値がenum内の値かどうかを検証する方法について、
ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3



サンプル


例えば下記の実装があったとします。

class Article
  enum status: [:draft, :published, :dropped]
end

このような実装をしていた時に、
「ブラウザから送られてきたパラメータがenumで定義した値のいずれかでなければ、エラーを返す」
という実装を行いたいとします。

普通に実装すると

raise StandardError unless [:draft, :published, :dropped].include?(params[:status])

みたいになるかと思うのですが、

Enumで定義した値は、
Article.statuses(モデル名.enumで定義したカラムの複数形)とすると、

{ "draft": 0, "published": 1, "dropped": 2 }

という返り値を得ることができるので、keysメソッドと合わせて、

raise StandardError unless Article.statuses.keys.include?(params[:status])

と書くことができます。
Railsっぽくていいですね!

RailsでObject.has_many.has_manyという関連が組んである場合のデータ取得

こんにちは!kossyです!



さて、今回は、Object.has_many.has_manyという関連が組んである場合のデータ取得の方法を
ブログに残したいと思います。



環境
Rails 5.2.3
Ruby 2.5.1
MacOS Mojave



mapやflattenを使う!



「学生は講義をいっぱい持っていて、講義は科目をいっぱい持っている」とします。

Student has_many rectures
recture has_many subjects

ここで、
「その学生が持つ講義の科目を全て取得したい」とします。

その場合、

$ student = Student.first

$ student.rectures.map{|rec| rec.subjects }.flatten

こうすることで実現可能です。

要件が変わって、
「その学生が持つ講義の科目名を全て取得したい」となっても、

$ student = Student.first

$ student.rectures.map{|rec| rec.subjects }.flatten.pluck(:name)

とすることで対応可能です。

Rubyでオブジェクトをハッシュ化して特定のkeyを取り除いて新たなハッシュを返す

こんにちは!kossyです!



さて、今回は、Rubyでオブジェクトをハッシュ化して特定のkeyを取り除いて新たなハッシュを返す方法について、
ブログに残してみたいと思います。







下記のようなオブジェクトがあるとします。

=> #<Article:0x00007fe76b8d89c0
 id: 1,
 title: "練習試合の結果0",
 body:
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 released_at: Sat, 22 Jun 2019 12:02:35 JST +09:00,
 expired_at: Fri, 28 Jun 2019 12:02:35 JST +09:00,
 member_only: true,
 created_at: Sun, 30 Jun 2019 12:02:35 JST +09:00,
 updated_at: Sun, 30 Jun 2019 12:02:35 JST +09:00>

このオブジェクトをハッシュ化したい場合、
attriburesメソッドを使うと、ハッシュ化できます。

rails/attribute_methods.rb at master · rails/rails · GitHub

pry(main)> article.attributes
=> {"id"=>1,
 "title"=>"練習試合の結果0",
 "body"=>
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 "released_at"=>Sat, 22 Jun 2019 12:02:35 JST +09:00,
 "expired_at"=>Fri, 28 Jun 2019 12:02:35 JST +09:00,
 "member_only"=>true,
 "created_at"=>Sun, 30 Jun 2019 12:02:35 JST +09:00,
 "updated_at"=>Sun, 30 Jun 2019 12:02:35 JST +09:00}


$ article.attributes.class
=> Hash

ハッシュ化したあと、特定のkeyを取り除いて新たなハッシュを作成したい場合は、
reject(またはdelete_if)を使います。

$ article.attributes.reject{|key| ['id', 'body', 'released_at', 'expired_at', 'created_at', 'updated_at'].include?(key) }
=> {"title"=>"練習試合の結果0", "member_only"=>true}


特定のkeyを取り除いて、新たなハッシュを返すことができました。

rejectにブロック引数を複数渡すこともできます。
例えば、特定のkeyを取り除きつつ、valuenilの値を取り除きたいとします。

=> #<Article:0x00007fe76b8d89c0
 id: 1,
 title: "練習試合の結果0",
 body:
  "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。",
 released_at: Sat, 22 Jun 2019 12:02:35 JST +09:00,
 expired_at: nil,
 member_only: true,
 created_at: Sun, 30 Jun 2019 12:02:35 JST +09:00,
 updated_at: Sun, 30 Jun 2019 12:02:35 JST +09:00>

$ article.attributes.reject{|k, v| ['id', 'body', 'released_at', 'created_at', 'updated_at'].include?(k) || v.nil?}

include?やnil?メソッドを駆使して、目的を達成することができました。

Rubyってほんと書きやすいですね。

FactoryBotで多対多の関連を組んでいる時のモックデータを作成する

こんにちは!kossyです!



さて、今回はFactoryBotで多対多の関連を組んでいる時のモックデータを作成するやり方について、ブログに残してみたいと思います。



例えば、

  • 学生(students)は授業(rectures)を複数持っている
  • 授業は学生を複数持っている

という関連があるとしましょう。

この場合、

# FactoryBotの省略構文を用いています。

student = create  :student
recture = create :recture
create :student_recture, student: student, recture: recture

上記のような定義方法になるかと思いますが、

student = create  :student
recture = create :recture, students: [student]

関連先のオブジェクトを配列で指定することによって、
中間テーブルのレコードを同時に作成することができます。


勉強になりました。

RubyのTimeクラスのメソッド「strptime」の使い方

こんにちは!kossyです!



さて、今回はRubyのTimeクラスのメソッドである「strptime」の使い方をブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave



2020-03-28T21:05:06+09:00 みたいな表示をparseする

ログの時刻表示が2020-03-28T21:05:06+09:00のようになるパターンがあるかと思いますが、
ニーズによっては、2020-03-28 21:05:06 + 09:00 のように表示したいということもあるかと思います。

そこで、Time#strptimeの出番です。

$ Time.strptime('2020-03-28T21:05:06+09:00',  '%Y-%m-%dT%H:%M:%S%z')

=>  '2020-03-28 21:05:06 + 09:00'

このように、第一引数に渡した時刻を、第二引数で指定したフォーマット通りに変換してくれます。



勉強になりました。

RequestSpecを書く際にiso8601メソッドを使ってcreated_atのテストを通す

こんにちは!kossyです!




さて、今回はAPIモードで実装されたRailsのRequestSpecを書く際に、
iso8601メソッドを使ってcreated_atのテストを通す方法について、ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 6.0.2.1
MacOS Mojave




普通に比較すると通らない

require 'rails_helper'

RSpec.describe 'Pokemon API', type: :request do
  describe 'index' do
    it '' do
      create_list(:pokemon, 50)
      pokemon = Pokemon.first

      get '/pokemons'

      expect(response.status).to eq 200
      json = JSON.parse(response.body)
      pokemon_json = json[0]
      expect(pokemon_json['created_at']).to eq pokemon.created_at
    end
  end
end

上記のようなテストがあったとして、

expect(pokemon_json['created_at']).to eq pokemon.created_at

この書き方をすると、

expected: 2020-03-18 21:59:31.000000000 +0900
            got: "2020-03-18T21:59:31.000+09:00"

日付の中身がちゃうやん!と怒られてしまいます。

そこで登場するのが、DateTimeクラスのインスタンスメソッドであるiso8601です。
iso8601 (DateTime) - APIdock

ISO8601についてはこちらが詳しかったです。
ISO 8601 - Wikipedia


このメソッドを、

expect(pokemon_json['created_at']).to eq pokemon.created_at.iso8601(3)

このようにcreated_atに適用します。

すると、

should eq "2020-03-18T22:04:09.000+09:00"

このように、テストをパスすることができます。




勉強になりました。

STIを使っている時のFactoryBotでのモックデータの作成

こんにちは!kossyです!




さて、今回はSTI(SingleTableInheritance)を使ってクラス定義をしているクラスの
FactoryBotでのモックデータの作成の仕方について、ブログに残してみたいと思います。




環境
Ruby 2.5.1
Rails 5.2.3
MacOS Mojave




なお、STIが何かについての説明は割愛します。
みんなRailsのSTIを誤解してないか!? - Qiita
[Rails] STI(単一テーブル継承)とメタプログラミングでDRY - Qiita

STIについて詳しく知りたい方は上記の記事をご覧になってみてください。




initialize_withメソッドを使う

例えば、STIで、
親 Commentクラス
子 ArticleComment, EntryCommentクラス

が定義されているとします。

この場合、

FactoryBot.define do
  factory :comment do
    body { Faker::Book.title }
  end

  trait :article do
    type 'ArticleComment'
  end
end

のようにtraitを使って定義することで、

create :comment, :article

とすることで、typeにarticleが指定されたCommentインスタンスを作成することができますが、
クラスはArticleCommentクラスではなく、Commentクラスになっています。

この場合、ArticleCommentに定義されたインスタンスメソッドのテストを行うことができません。

なので、

FactoryBot.define do
  factory :comment do
    body { Faker::Book.title }
  end

  trait :article do
    type 'ArticleComment'
      initialize_with do
        klass = type.constantize
        klass.new(attributes)
      end
  end
end

このようにinitialize_with メソッドを使うことで、
ArticleCommentクラスのインスタンスを生成することができます。



参考にさせていただいた記事
STIを利用している場合のFactoryBotのfactory定義について - indilog

Rspecで画像アップロードのテストデータを準備する

こんにちは!kossyです!




さて、今回はRspecで画像アップロードのテストデータを準備する方法について、
ブログに残してみたいと思います。




環境
Ruby 2.5.1
Raila 5.2.3
MacOS Mojave



Rack::Test::UploadedFileクラスを使う

Wraps a Tempfile with a content type. Including one or more UploadedFile's in the params causes Rack::Test to build and issue a multipart request.

出典: www.rubydoc.info

Tempfileをcontent_typeでラップしてくれるクラスです。


これを使って、画像アップロードのためのモックデータを準備できます。



使い方は簡単で、

params = {
  image: Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/profile.png'), 'image/png')
}

みたいにパラメータとして定義したり、

FactoryBot.define do
  factory :article do
    title { Faker::Book.title }
    body { Faker::Lorem.sentences }
    released_at { (Date.today - 1) }
    expired_at { (Date.today + 1) }
    member_only { false }
    thumbnail {  Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec/fixtures/profile.png'), 'image/png') }
  end
end


みたいにFactoryBotのモックデータとしても定義できます。

第一引数で画像のパスを指定し、第二引数でコンテンツのタイプを指定します。
ちなみに第二引数に何も指定しないと、コンテンツタイプがtext/plainになるので、コンテンツタイプでバリデーションを
かけている場合は気をつけましょう。(私は30分ハマりました)



参考にさせていただいた記事

https://github.com/rack-test/rack-test/blob/master/lib/rack/test/uploaded_file.rb
https://www.rubydoc.info/github/brynary/rack-test/Rack/Test/UploadedFile
https://qiita.com/selmertsx/items/2beb0d7ec0774cbbf050
https://qiita.com/tatsuya1156/items/f03c53917d72cdca053a
http://sissoko.hatenablog.com/entry/2016/07/08/131344

Reactの学習に使えそうなサイトまとめ

こんにちは!kossyです!




さて、今回はReact初心者の学習に使えそうなサイトをまとめてみたいと思います。



1. Deep Dive Into Modern Web Development

fullstackopen.com

Reactだけでなく、周辺ライブラリや、GraphQLとの連携までカバーしています。

初心者向けというよりは中級者向けの内容かもしれません。

2. React/ReduxでGoogleカレンダー風カレンダーアプリケーションを作ろう

www.techpit.jp


こちらも中級者向けかもしれません 笑
ディレクトリやコンポーネンt設計までカバーした本格的な内容になっています。

3. 【React × Ruby】サーバーレスでゴルフ場検索サービスを作ってみよう!

www.techpit.jp

こちらはAPI側の実装も網羅した教材になっています。
現場ではReactだけでアプリケーションを構築することはおそらく稀で、
APIを作成するのも同時に行うのが当たり前かと思われます。

上記の教材はサーバーレス構成でモダンなアプリケーションを作成できる教材になっているので、
ある程度Reactに慣れて、バックエンドにも手を出してみたい方におすすめです。

4. Progate React コース

prog-8.com

ベタですが、全く何もわからない状態からの学習にはProgateの教材が一番適していると思います。
わかりやすいスライドが常に確認でき、環境構築も必要ないので、
学習の入り口としては最適かと思います。

番外編 TypeScript Deep Dive

typescript-jp.gitbook.io

ReactはJavaScriptでもTypeScriptでもアプリケーションを作成することができますが、
近年はTypeScriptで開発するのが主流になりつつあるようです。

上記の記事は体系的にTypeScriptについて学習できる内容になっており、おすすめです。