Madogiwa Blog

主に技術系の学習メモに使っていきます。

Markdown形式のテキストをRSpec形式に変換するGemを作りました💎

画面仕様書等、Markdownで整理していたものからRSpecを書き直すのが意外と手間と思うことがあったのでMarkdownで書いたものをRSpec形式のテキストに変換するGemを作りました。

github.com

使い方

使い方は、

$ gem install markdown_to_rspec
# CLI
$ markdown_to_rspec -f `MARKDOWN_FILE_PATH`
#=> return A string in RSpec format
$ markdown_to_rspec -t `MARKDOWN__TEXT`
#=> return A string in RSpec format

上記ようにgemをinstall後に引数に以下の値を渡すことでRSpec形式の文字列に変換します。

例えばこのような画面仕様書的なマークダウンを変換すると、

# Details.
A screen to check something
ref: https://example/com/tickets/1

## Initial Display.

### When a record exists.
* The title must be displayed.
* The text must be displayed.

### When the record does not exist.
* The title should not be displayed.
* The text should not be displayed.

### Other cases.
* 500 pages to be displayed.

# Index.
A screen to check something

## Initial Display.
* The items must be displayed.

以下のようなRSpec形式の文字列を取得出来ます✨

RSpec.describe 'Details.' do
  # A screen to check something
  # ref: https://example/com/tickets/1
  describe 'Initial Display.' do
    context 'When a record exists.' do
      it 'The title must be displayed.' do
      end
      it 'The text must be displayed.' do
      end
    end
    context 'When the record does not exist.' do
      it 'The title should not be displayed.' do
      end
      it 'The text should not be displayed.' do
      end
    end
    context 'Other cases.' do
      it '500 pages to be displayed.' do
      end
    end
  end
end
RSpec.describe 'Index.' do
  # A screen to check something
  describe 'Initial Display.' do
    it 'The items must be displayed.' do
    end
  end
end

※インデントの数やMarkdown#のどのレベルをdescribe or contextにする等は一旦固定値になっているので調整出来ません🙏💦

個人的な技術Topic

作った上で個人的な技術Topicをちょっと書いときます。

RDoc::Markdownを使ってマークダウンから中間オブジェクトを生成

MarkdownからRSpecに変換する際になんかしらの中間的なオブジェクトを生成する必要がありそうだなぁと思い、 なんかいい感じのgemを探していたのですが見つからず。。。

そういればRDocってMarkdown形式でかけるなと思い標準ライブラリを探していたら、 RDoc::Markdownがまさにな感じだったのでそれを使うことにしました。

docs.ruby-lang.org

下記のような感じでMarkdown形式の文字列をRDoc::Markdown.parseに渡してあげると、 RDoc::Markup::DocumentというMarkdownのアイテムの関係性を保持した中間のオブジェクトを返却してくれます。

require 'rdoc/markdown'

markdown = <<~MARKDOWN
# title
## subtitle
* item 1
* item 2
MARKDOWN

RDoc::Markdown.parse(markdown)
#=> #<RDoc::Markup::Document:0x00007f92f0b2e398 @parts=[#<struct RDoc::Markup::Heading level=1, text="title">, #<struct RDoc::Markup::Heading level=2, text="subtitle">, #<RDoc::Markup::List:0x00007f92f11dd928 @type=:BULLET, @items=[#<RDoc::Markup::ListItem:0x00007f92f0b65f78 @label=nil, @parts=[#<RDoc::Markup::Paragraph:0x00007f92f11e3828 @parts=["item 1"]>]>, #<RDoc::Markup::ListItem:0x00007f92f11e8fa8 @label=nil, @parts=[#<RDoc::Markup::Paragraph:0x00007f92f0b54b38 @parts=["item 2"]>]>]>], @file=nil, @omit_headings_from_table_of_contents_below=nil>

このオブジェクトをRSpecに変換するコードを今回は実装しています。

この辺のRDoc::Markup::Formatterを継承したクラスを作ったほうがいい感じに出来たのかも・・・? ※今回は使い方を学習するのが結構難しそうで自前で実装してしまった。。。

docs.ruby-lang.org

OptionParserを使ってCLIのインタフェースを定義

CLI部分のインタフェース部分を標準ライブラリのOptionParserを使って実装してます。

docs.ruby-lang.org

OptionParserを使うと下記のような感じで実行時に渡されたオプションに対して何を実行するのかを定義出来ます。

option.onで指定されたオプションと実行する処理を紐付けして、option.parse!(ARGV)で、 実行時引数を元に実行する処理を判定して引数をよしなに渡してくれるようです✨(便利)

require 'optparse'
require 'markdown_to_rspec/rake_tasks'

option = OptionParser.new
option.on('-f', '--file FILE_PATH', 'Set the target Markdown file path.') do |v|
  Rake::Task['markdown_to_rspec:to_rspec_from_file'].execute(file_path: v)
end
option.on('-v', '--version', 'Show gem version.') do
  Rake::Task['markdown_to_rspec:version'].execute
end
option.parse!(ARGV)

上記のような設定だと以下のような動きになります。

  • -f--formatの時に引数で渡された値を元にRake::Task['markdown_to_rspec:to_rspec_from_file']を実行 例) $ markdown_to_rspec -f sample.md
  • -v--versionの時にRake::Task['markdown_to_rspec:version']を実行 例) $ markdown_to_rspec -v

あとは--helpも自動で実装してくれます✨

$ markdown_to_rspec -h
Usage: markdown_to_rspec [options]
    -f, --file FILE_PATH             Set the target Markdown file path.
    -v, --version                    Show gem version.

おわりに

マークダウン形式で書かれた画面仕様書からRSpecのE2Eテスト等を作るときに、 抜け漏れ等を防げて便利な気がするので、せっかく作ったので使っていこうと思います💪

色々調べるとRubyの標準ライブラリ、便利ですね✨

Ruby: オレオレフレームワーク`makanai`を`v0.1.6`にアップデートしました🥳

先日、ほぼピュアなRubyで書いているオレオレフレームワークmakanaiv0.1.6にバージョンアップしました🥳

github.com

アップデートの主な内容は下記の通りです。

✨ enabled to switch template engine Haml and ERB.

github.com

テンプレートエンジンが今まではERB固定だったのですが、Hamlも使えるようになりました⚙️

Makanai::Settings.template_engineの値か、renderの第2引数で使用するテンプレートエンジンが指定可能になりました✨(:haml or :erb)

require 'makanai/main'

# setting template engine(default: :erb).
Makanai::Settings.template_engine = :haml

router.get '/index' do
  @title = 'Makanai title'
  @body = 'Makanai body'
  render :index # render default template engine.
end

router.get '/index' do
  @title = 'Makanai title'
  @body = 'Makanai body'
  render :index, :haml # render specified template engine.
end

⚡ remove sqlite3 from runtime_dependency.

github.com

今までsqlite3runtime_dependencyとなっていましたが、静的サイト等特にDBを使わない場合は必要ないのでruntime_dependencyから外しました✂️

makanai initでGemfileにsqlite3が追記された状態でapplication用のディレクトリ構成が作成されるようになっています📦

それでは👋

Ruby on Rails: リクエストのformatによって実行するControllerを分岐させるroutesの設定方法MEMO

Ruby on Railsでエンドポイントを変えずにformatで実行するControllerを分ける方法でちょっと悩んだので解決方法をメモしておきます。

やりたかったこと

個人のサービスでRSS用のXMLを表示するのと普通にHTMLを返却する処理を下記のようにrespond_toで分岐するようにしていたのですが、

# routes
Rails.application.routes.draw do
  resources :posts
end

# controller
class PostsController < ApplicationController
  def index
    @posts = Post.all
    respond_to do |format|
      format.html
      format.rss {
        # RSS特有の処理
        render layout: false 
      }
    end
  end
end

RSS用にに表示する内容とHTMLで画面に表示する内容に差が出てきてContollerを分けてくなってきたのですが、RSS用のエンドポイントは外部に公開していて変更するのは影響が大きいので、routes.rbformatで実行するContorollerを分岐出来ないかと悩んでしました🤔

解決方法

routesではconstraints内でlamdaを使って条件に合致するかどうかをチェックすることができるようで、これを使って解決しました💡

次のようにlambdaを使うことができます。get 'foo', constraints: lambda { |req| req.format == :json } このルーティング指定は明示的なJSONリクエストにのみ一致します。 Rails のルーティング - Railsガイド

修正したコードが下記です。constraints: lambda { |req| req.format == :rss }formatrssのときのみにrss/posts#indexが実行するようにしてあげて、Rss::PostsControllerを追加しています。respond_toの分岐もなくなってスッキリしました👏

Rails.application.routes.draw do
  resources :posts, constraints: { format: :html } # `.rss`に反応しないようにformatをhtmlのみに制限
  get '/posts', to: 'rss/posts#index', constraints: lambda { |req| req.format == :rss } # lambdaを使って.rssのみに制限
end

# controller
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end
end

# controller(rss)
class Rss::PostsController < ApplicationController
  def index
    @posts = Post.all
    # Rss特有の処理
    render layout: false
  end
end

constraints: { format: :rss }では制限出来ないので注意してください。Railsガイドによると下記とのことです。

get 'foo'、constraints: { format: 'json' }はGET /fooと一致します。これはデフォルトでformatがオプションであるためです。 Rails のルーティング - Railsガイド

おわりに

constraints: lambda { }の形で実行するControllerを判定するの、何も考えずに使用すると煩雑になって大変そうですが、リクエストを元に自由に実行するContollerを決めれて便利そうですね👀

参考

railsguides.jp

SimpleCovとCircleCIのArtifactsを使ってカバレッジ測定結果を確認するMEMO

rubyを使ったアプリケーションのテストコードのカバレッジ測定にはSimpleCovを使うことが多いと思いますが、CircleCIのArtifactsを使うと簡単にCircleCIの管理画面上でSimpleCovの結果を確認できたので、そのへんの手順をメモしておきます📝

github.com

Codecovを使った方がPRにコメントしれくれたりと色々便利な面もあるかもですが、お手軽にSimpleCovを使ってプロジェクトのカバレッジを測定できるようになるのはいいのかなと✨

SimpleCovでカバレッジを測定する

今回はRailsRspecの環境で有効化する手順を記載します。

まずはGemfilegroup :testsimplecovを追記してbundle installします。

group :test do
  gem 'simplecov', require: false
end

そしてspec_helperを下記のような記述を追加します。※SimpleCovの設定周りは公式のドキュメントを参考に任意の値に変更してください。

SimpleCov.start do # カバレッジ測定を有効化
  enable_coverage :branch # Branch coverageを有効化
  add_filter '/spec/' # spec配下は測定対象外
  add_filter do |source_file|
    source_file.lines.count < 5 # 5行未満のファイルは対象外
  end

  # model、controllerのファイルはgroupとして見やすくする
  add_group 'Models', 'app/models' く
  add_group 'Controllers', 'app/controllers'
end

この状態でbundle exec rspecでテストを実行するとcoverage配下に結果が格納されるようになります。

$ cd coverage
$ tree
.
|____index.html
|____.last_run.json
|____.resultset.json
|____assets
| |____0.12.2
| | |____images
| | | |____ui-icons_cd0a0a_256x240.png
| | | |____ui-icons_888888_256x240.png
| | | |____ui-bg_glass_75_dadada_1x400.png
| | | |____ui-icons_2e83ff_256x240.png
| | | |____ui-bg_flat_75_ffffff_40x100.png
| | | |____ui-bg_glass_75_e6e6e6_1x400.png
| | | |____ui-bg_glass_65_ffffff_1x400.png
| | | |____ui-bg_glass_95_fef1ec_1x400.png
| | | |____ui-icons_222222_256x240.png
| | | |____ui-bg_highlight-soft_75_cccccc_1x100.png
| | | |____ui-bg_glass_55_fbf9ee_1x400.png
| | | |____ui-icons_454545_256x240.png
| | | |____ui-bg_flat_0_aaaaaa_40x100.png
| | |____application.css
| | |____loading.gif
| | |____colorbox
| | | |____border.png
| | | |____loading.gif
| | | |____controls.png
| | | |____loading_background.png
| | |____favicon_red.png
| | |____favicon_green.png
| | |____favicon_yellow.png
| | |____DataTables-1.10.20
| | | |____images
| | | | |____sort_asc_disabled.png
| | | | |____sort_both.png
| | | | |____sort_desc_disabled.png
| | | | |____sort_desc.png
| | | | |____sort_asc.png
| | |____magnify.png
| | |____application.js
|____.resultset.json.lock

index.htmlをブラウザで開くとカバレッジの測定結果を確認できます✨

f:id:madogiwa0124:20200725155343p:plain

CircleCIのArtifactsでSimpleCovの結果を格納する

Artifactsとは

アーティファクトには、ジョブが完了した後もデータが維持され、ビルドプロセス出力の長期ストレージとして使用できます。 アーティファクトAmazon S3 に保存され、プライベートプロジェクト用の CircleCI アカウントを使用して保護されます。 https://circleci.com/docs/ja/2.0/artifacts/

Job実行時に生成されたファイル等をCircleCIが管理するS3上にアップロードする機能のようです👀 

publicリポジトリの場合はArtifactsで公開したファイルが未ログインでも見れる状態になっているようなので注意してください。

discuss.circleci.com

👇詳しくはこちら

circleci.com

artifactsを使って測定結果をアップロードして確認する

CircleCIのconfigファイルでstore_artifactsを使 うことでアップロードすることができます。

設定ファイルは省略していますが下記のような感じでrspecを実行したあとにcommandsで定義したstore_coverage_atifactsを実行してcovarage配下のファイルをArtifactsにアップロードしています。

commands:
  store_coverage_atifacts:
    steps:
      - store_artifacts:
          path: coverage


jobs:
  ruby_test:
    steps:
      - run:
          name: run tests
          command: bundle exec rspec spec/
      - store_coverage_atifacts

これでCircleCIの画面上からSimpleCovで測定してカバレッジを参照できるようになりました🎉

参考資料

qiita.com

Ruby on Rails: cssの管理をAssets PipelineからWebpakerに移行するときのメモ

個人のアプリケーションを今まではデフォルトのアセットまわり(CSS等)をSprocketsJavaScriptWebpackerでビルドするような形にしてたのですが、 今回はstylesheetをWebpackerで管理するようにしたので、そのへんの手順をメモしておきます📝

対応したアプリケーションのRailsのバージョンは6.0.3.2、Webpackerのバージョンは5.1.1です。

.cssファイルをWebpackerで管理する

今回は.css(.scss)ファイルをWebpackerで管理するための手順をまとめていきます。

assets配下の.cssファイルをjavascript配下に移動

まずはSproketsで管理しているapp/assets配下の.cssファイルをWebpackerで管理するためにapp/javascript配下に移動します。 ※webpacker.ymlでentryまわりの変更している場合は、その設定に従って移動先を指定してください。

今回はWebpackerの公式のドキュメントがapp/javascript/stylesheets配下においてそうだったので、そこに移動するようにしました🙋

# https://github.com/rails/webpacker/blob/master/docs/css.md
app/  
  javascript/  
    stylesheets/  
      application.scss  
      posts.scss  
      comments.scss

entryの.js.cssファイルをimportしてWebapckerのビルド対象にする

Webpacker(Webpack)はentryでimportされたファイルをビルド対象とするので必要な.cssファイルを entryの.js(デフォルトだとapp/javascript/packs配下のjs)でimportしてあげます。 ※Webpackerはデフォルトでsass-loadercss-loader等がinstallされて設定もよしなにしてくれるようです。

// app/javascript/application.js
import '../stylesheets/application.scss'

Webpacker v5からはpacks配下にentryのjsと同名のcssを配置すると自動でimportしてくれるようになったようです、便利✨

By Webpacker convention (as of Webpacker v5), this will bundle application.js and application.scss as part of the same entry point (also described as a multi-file entry point in the webpack docs). https://github.com/rails/webpacker/blob/master/docs/css.md#importing-css-as-a-multi-file-pack-webpacker-v5

これでbin/webpack時にimportしたcssファイルがビルドされて読み込まれるようになりました🙌

しかしデフォルトだとhead内に直接ビルドされたstyleが記述されてしまいます。 styleが比較的軽量な場合は問題ないのですが、S3から.cssファイルを配信するため別ファイルで出力したいケースは多いかと思います。

.cssファイルを個別のファイルとしてビルドするようにする

Webpackerにはmini-css-extract-pluginを使用してimportしたファイルを個別ファイルで出力する機能が提供されています。

使用するためにはwebapcker.ymlextract_csstrueに設定することで.cssファイルを個別のファイルとして出力することができます。

default: &default
  # Extract and emit a css file
  extract_css: true

development: &default
  # Extract and emit a css file
  extract_css: true

// その他環境でも必要に応じて同様に設定

Viewでビルドされた.cssファイルを読み込む

個別に出力された.cssファイルはjavascript_pack_tagと同様にView側の読み込む必要があります。

cssの場合はstylesheet_pack_tagを使って読み込みを行います。

<%= stylesheet_pack_tag 'application' %>

これで.cssファイルをWebapckerでビルドして個別ファイルに出力し、それをView側で読み込むことができるようになりました🎉

おまけ:eslintの対象からCSSまわりを除外する

package.jsonのlintまわりの設定を下記のような形にしていたのですが、javascript配下のcssがeslintの対象となってしまい。。。🤔

  "scripts": {
    "lint": "eslint app/javascript/**/* --ext .vue,.js,.ts",
    "lint-fix": "eslint app/javascript/**/* --ext .vue,.js,.ts --fix",

色々調べてみたところ--extオプションがファイル指定の場合に効かないようで、、、eslintignoreで指定するようにしたのですが、

app/javascript/stylesheets

Note: --ext is only used when the arguments are directories. If you use glob patterns or file names, then --ext is ignored. https://eslint.org/docs/user-guide/command-line-interface#ext

これでは、eslintがeslintignoreと実行時の指定のどちらを優先すればいいかわからず、warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override.の警告が発生してしまいます😢

If you pass a specific file to ESLint, then you will see a warning indicating that the file was skipped. https://eslint.org/docs/user-guide/configuring#ignored-file-warnings

最終的には下記のようなディレクトリ指定にして--extオプションが効くようにして対応しました🙇‍♂️

  "scripts": {
    "lint": "eslint app/javascript --ext .vue,.js,.ts",
    "lint-fix": "eslint app/javascript --ext .vue,.js,.ts --fix",

おまけ: Sproketsの無効化

アセットまわりもWebpackerでビルドするようにするとSproketsはもう必要ないので無効化してあげると依存gemも減らせていい感じです。

※ちなみに新規にアプリケーションを作る場合は--skip-sproketsを指定してrails newすればOKなので楽です👌

Sproketsのrequireをやめる

application.rbrequire 'rails/all'をしているとsprockets/railtierequireされてしまうので、

 %w(
   #...
   sprockets/railtie
 ).each do |railtie|
   begin
     require railtie
# https://github.com/rails/rails/blob/6-0-stable/railties/lib/rails/all.rb#L18

sprockets/railtieを除いて個別にrequireするようにします。

require 'active_record/railtie'
require 'active_storage/engine'
require 'action_controller/railtie'
require 'action_view/railtie'
require 'action_mailer/railtie'
require 'active_job/railtie'
require 'action_cable/engine'
require 'action_mailbox/engine'
require 'action_text/engine'
require 'rails/test_unit/railtie'

config系のファイルからassetsまわりの設定値を削除する

その後config/enviroments配下の各ファイルからconfig.assets関係の設定を削除していきます。

各設定値の詳細はこちら

railsguides.jp

(config.assets.enabledfalseにしといた方がいいのかな?🤔)

私の個人アプリだとこの辺の設定を削除しました。

# config/environments/development.rb
config.assets.debug = true # 削除
config.assets.quiet = true # 削除

# config/environments/production.rb
config.assets.compile = false # 削除

そして、config/initializers/assets.rbも削除します。

sass-railsをGemfileから削除する

最後にGemfileからsass-railsを削除してbundle installします。※uglifier等も残っていたら削除します。

gem 'sass-rails', '>= 6' # 削除

これでSproketsも無効化できました🎉

おわりに

今回はstyleまわりを例にAssets PipelineからWebpakerに移行する手順をちょっと整理してみました。(今回行ったのは.cssだけだったのでシンプルでしたが、実際の案件ではこんなにすんなりはいかなさそう。。。)

Webpack移行の前段としてSproketsとの併用をやめて、Webpacker単体の環境に移行しておくと移行対象が減るので、いいかもですね👀

参考資料

webpacker/css.md at master · rails/webpacker · GitHub

stackoverflow.com

RuboCopに自分で定義した新しいルールを追加する方法のMEMO🤖

RuboCopは、Rubocop::Cop::Copを継承することで独自のルールを追加することできるようで、 レビューでよくコメントもらう内容等、RuboCopで検知できるようになったら便利だなと思い、 ルールを作成してみたので、そのへんの手順等をMEMOしておきます✍

公式の開発用のDocはこちら📝

docs.rubocop.org

今回作ったもの

今回は下記のようなことを検証するルールを作ってみました🤖

SQLの実行回数削減のため、pluckを使っていたらselectを使うことをサジェストする

# bad
Foo.where(id: Bar.pluck(:foo_id))
# good
Foo.where(id: Bar.select(:foo_id))

where内でLIKEを使っていたらsanitized_sql_likeを使うことをサジェストする

# bad
Foo.where('title LIKE ?', "%#{title}%")
# good
Foo.where('title LIKE ?', "%#{sanitized_sql_like(title)}%")

作ったものは下記のリポジトリにアップしています。

github.com

新しいルールを作るのに必要なこと

新しいルール作ってrubocop実行時に静的解析を行うようにするには下記が必要なようです👀

  • RuboCop::Cop::Copを継承したClassを作る
  • RuboCop::Cop::Copを継承したClassのテストを書く
  • 作成したClassを.rubocop.ymlrequireする

RuboCop::Cop::Copを継承したClassを作る

今回、私が実際に作ったClassは下記のような形です。

下記は、SQLの実行回数削減のため、pluckを使っていたらselectを使うことをサジェストするルールです。

require 'rubocop'

module CustomCops
  # @example
  #   # bad
  #   Foo.where(id: Bar.pluck(:foo_id))
  #
  #   # good
  #   Foo.where(id: Bar.select(:foo_id))
  class UseSelectInWhere < RuboCop::Cop::Cop
    MSG = 'Use select instead of pluck in where to use subqueries to reduce SQL executions.'

    def_node_matcher :where_in_pluck_candidate?, <<~PATTERN
      (send _ :where (:hash (:pair (:sym _) (send _ :pluck (:sym _)) ...)))
    PATTERN

    def on_send(node)
      where_in_pluck_candidate?(node) do
        add_offense(node)
      end
    end

    def autocorrect(node)
      # TODO
    end
  end
end

ちょっと解説すると

  • MSGに設定された値が違反時にメッセージとして表示されるようです。
  • def_node_matcherwhere{ sym: 任意のオブジェクト.pluck}のような形で引数に渡しているか解析するmather where_in_pluck_candidate?を定義しています。※このmatherは抜け漏れがあるかも。。。
  • where_in_pluck_candidate?に合致するようなものがあったらadd_offenseで警告を出しています。
  • 今回はTODOにしてしまっていますが、autocorrectに処理を書くとautocorrectの処理を定義出来ます。

RuboCop::Cop::Copを継承したClassのテストを書く

先程作ったClassのテストは下記のような形で行うようにしました。

RuboCop::RSpec::ExpectOffenseをincludeしてexpect_offenseに、コードとメッセージを渡してあげるような形でテストするようです👀

expect_offense内の記載内容をいい感じに取得する方法がわからなかったので、とりあえずテストを実行して失敗したときの値が、それっぽかったらコピペするような形にしたが、もっといい方法がありそう。。。

require 'rubocop'
require 'rubocop/rspec/support'

RSpec.configure do |config|
  config.include(RuboCop::RSpec::ExpectOffense)
end

describe CustomCops::UseSelectInWhere do
  subject(:cop) { described_class.new }

  it 'raised offense when use `pluck` in `where`.' do
    expect_offense(<<-RUBY)
      Foo.where(id: Bar.pluck(:foo_id))
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use select instead of pluck in where to use subqueries to reduce SQL executions.
    RUBY
  end
end

作成したClassを.rubocop.ymlrequireする

.rubocop.ymlで先程作ったclassのファイルをrequireすることでルールを追加します。

require: './cops/use_select_in_where'

rubocop実行時に静的解析されるようになりました🙌

$ be rubocop sample/
sample/use_select_in_where.rb:4:23: C: CustomCops/UseSelectInWhere: Use select instead of pluck in where to use subqueries to reduce SQL executions.
  scope :recent, -> { Foo.where(id: Bar.pluck(:foo_id)) }
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

おわりに

レビューでよくコメントが入る内容や、プロジェクト独自で拡張したクラスを使う必要がある場合等、rubocopの独自ルールとして定義しておくと、レビュー時の抜け漏れ等が防げて良さそうですね✨

追記: RuboCop Version 1.0で作り方が変わるかも

Version 1.0に向けてCop周りの仕様の見直しが結構入っているようなので、これから作るときは下記を参考にしたほうが良さそう👀

rubocop/v1_upgrade_notes.adoc at master · rubocop-hq/rubocop · GitHub

参考

sinsoku.hatenablog.com

qiita.com

koic.hatenablog.com

Ruby(with ActiveSupport)で指定日が第何、何曜日かを取得するときのMEMO

第2、第4火曜日だけなんかしらの処理を実行したいとき等、指定日が第何火曜日を知りたいケースの場合に、RubyとかActiveSupportにこういう系のメソッドが無いかなと思ったんですが、 曜日判定は出来ても第何曜日の判定を行うようなメソッドは見当たらず、ちょっと迷って色々考えたのでメモしておきます✍

やりかた(with ActiveSupport)

下記のような方針で考えてみました📅

  • 曜日に関してはDate#wdayを使って曜日を表す数字を取得して、hashで文字列を取得
  • 何番目に関しては対象の日付と月初の日付の週数をDate#cweekを使って取得して差分を取得

実際のコードはこちら

def nth_day_of_week(now: Time.current)
  wdays = { 0 => "", 1 => "", 2 => "", 3 => "", 4 => "", 5 => "", 6 => "" }
  # NOTE: 月初と現在が同じ週だった場合に1としたいので、月の最初の週は0とするためcweekから1を引く
  beginning_of_month_cweek = now.beginning_of_month.to_date.cweek - 1
  nth = now.to_date.cweek - beginning_of_month_cweek
  # NOTE: 年跨ぎの場合、nthが上記方針だとマイナスになるケースがあるので、その場合は前週の結果に+1する方針とする
  # 例) "2019/12/31".in_time_zone.to_date.cweek => 1, "2019/12/1".in_time_zone.to_date.cweek => 48
  nth = now.ago(1.week).to_date.cweek - beginning_of_month_cweek if nth.negative?
  return { nth: nth, wday: wdays[now.wday] }
end

nth_day_of_week
# => {:nth=>2, :wday=>"日"}

nth_day_of_week(now: 1.week.ago)
# => {:nth=>1, :wday=>"日"}

考慮漏れとかもあるかもですが、一旦こんな感じで取れそうでした・・・!(考えてみると意外と難しい・・・🤔💦)

追記: 月初の週数を取得せずに週数を取得する

Twitterで教えてもらった方法、月初を取得しないのでシンプル✨

def nth_day_of_week(now: Time.current)
  wdays = { 0 => "", 1 => "", 2 => "", 3 => "", 4 => "", 5 => "", 6 => "" }
  nth =  now.day / 8 + 1
  return { nth: nth, wday: wdays[now.wday] }
end

この辺の考慮は別途必要・・・!(割と大変)※でもこの辺の年末の考え方は、12月が12月として扱いたいとかありそうなので、要件によって変わるかも?

具体例を以下に示す。年初において以下の曜日に該当する場合、その日は新年第1週の日としてではなく、旧年最終週の日として扱う。
* 1月1日金曜日・1月2日土曜日・1月3日日曜日
* 1月1日土曜日・1月2日日曜日
* 1月1日日曜日
同様に、年末において以下の曜日に該当する場合、その日は旧年最終週の日としてではなく、新年第1週の日として扱う。
* 12月31日月曜日
* 12月30日月曜日・12月31日火曜日
* 12月29日月曜日・12月30日火曜日・12月31日水曜日
https://ja.wikipedia.org/wiki/ISO_8601

それにしても年またぎとかの考慮難しい・・・。

参考

shuzo-kino.hateblo.jp

指定された日付が、その月において何週目にあたるのかを計算する Ruby のメソッド · GitHub