Zenn
🐵

【Rails】gemへのモンキーパッチを試してみた

2024/12/26に公開

概要

※以下の記事の続編です。
https://zenn.dev/goals_techblog/articles/887a21aafcaa42

annotateというmodelファイルにスキーマ情報を書き出してくれるgemで、スキーマ情報がうまく更新されない問題が発生し、対処法まで割り当てたところが前回の記事内容でした。
今回は、直接gemに修正を加えるにはどうしたらよいか調べてみました。

参考記事

https://commerce-engineer.rakuten.careers/entry/tech/0028

さくっと試してみたかったので、参考記事の

config/initializersにモジュールごと上書き

の方法で実現してみました。

config/initializersについての公式ドキュメント

上書きファイルの作成

config/initializers以下に上書きしたいメソッドのファイルを作成

config/initializers/annotate_models.rb
require 'annotate'

module AnnotateModels
  class << self
    def annotate_one_file(file_name, info_block, position, options = {})
      return false unless File.exist?(file_name)
      old_content = File.read(file_name)
      return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/

      # Ignore the Schema version line because it changes with each migration
      header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
      old_header = old_content.match(header_pattern).to_s
      new_header = info_block.match(header_pattern).to_s

      column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
      old_columns = old_header && old_header.scan(column_pattern).sort
      new_columns = new_header && new_header.scan(column_pattern).sort

      return false if old_columns == new_columns

      abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`." if options[:frozen]

      # Replace inline the old schema info with the new schema info
      wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
      wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ""
      wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"

      old_annotation = old_content.match(annotate_pattern(options)).to_s
      # if there *was* no old schema info or :force was passed, we simply
      # need to insert it in correct position
      if old_annotation.empty? || options[:force]
        magic_comments_block = magic_comments_as_string(old_content)
        old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
        old_content.sub!(annotate_pattern(options), '')

        new_content = if %w(after bottom).include?(options[position].to_s)
                        magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
                      elsif magic_comments_block.empty?
                        magic_comments_block + wrapped_info_block + old_content.lstrip
                      else
                        magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
                      end
      else
        # replace the old annotation with the new one

        # keep the surrounding whitespace the same
        space_match = old_annotation.match(/\A(?<start>\s*).*?\n(?<end>\s*)\z/m)
        new_annotation = space_match[:start] + wrapped_info_block + space_match[:end]

        new_content = old_content.sub(annotate_pattern(options), new_annotation)
      end

      File.open(file_name, 'wb') { |f| f.puts new_content }
      true
    end
  end
end

※大元のメソッドから変更した箇所は、以下のcolumn_patternの正規表現のみになります。

- column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
+ column_pattern = /^#[\t ]+.+[\t ]+.+$/

before: [\w\*\.]+ 英数字およびアンダーバー「*」「.」の文字集合が1文字以上
after: .+ 任意の文字1文字以上

※変更したかった経緯は前回の記事をみてもらえると嬉しいです!!!

こちらの処理が適用されているか確認するために
bundle exec rake db:migrate
コマンドを実施。

→正規表現をゆるくしたことで、今まで弾かれていたcolumn情報が検出されるようになり、スキーマ情報が想定通りに出力されました!🎉

所感

今回はさくっと試したかったので、こちらの方法をとりましたが、参考記事にもあるとおり、以下のようなデメリットもある様で、管理面での考慮は別途必要そうです。

モジュールの処理を丸々上書きしてしまうので、一部だけ変えたい時にも全部の処理を書く必要がある
config/initializersにパッチファイルが散在し、第三者から見た時にどこでパッチしているかがわかりにくい

Railsを扱い始めたのは転職後になりますが、まだまだ知らないことが多いなと実感しました。
ちゃんと公式ドキュメントも読み込みたい。

想定通りに動いたので、こちらの内容でannotateの公式リポジトリにPRを出してみようと思いました。

Goals Tech Blog

Discussion

ログインするとコメントできます