コラム 3. プログラミング情報
2019年11月08日に公開

ぬぐぐ。Rails6でlib以下のカスタムバリデーションが呼び出せない【Zeitwerkとは】

この記事は、

この記事は、筆者がRails6.0で lib ディレクトリ以下に置いた、カスタムバリデーションファイルを呼び出すために、四苦八苦した奮闘日記です。

「ええからはよ結論ださんかい!」と、筆者のように前戯もまともにできないようなせっかちなお方のために、先に結論をご用意しました。

ユーザーモデルからemailバリデーションを呼び出す前提となっています。

どうぞコピペください。

【先に結論】lib以下のカスタムバリデーションファイルを呼び出す方法

libのディレクトリ構造
lib
├── tasks
└── validator
    └── email_validator.rb
config/application.rb
module Myapp8
  class Application < Rails::Application
		...
    config.autoload_paths += %W(#{config.root}/lib/validator) # 追記
    # config.autoload_paths << config.root.join("lib/validator")  # もしくはこれでも良い
lib/validator/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    # emailの書式を検証するバリデーションサンプル
    email = value.downcase
		format = /\A\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*\z/
    record.errors.add(attribute, :invalid) unless email =~ format
  end
end
app/models/user.rb
class User < ApplicationRecord
  validates email: true
end

以上がlib以下のカスタムバリデーションファイルを呼び出す方法でした。

それでは、良きバリデーションライフを!

何故、男はRails6.0でカスタムバリデーションが呼び出せなかったのか?

さて、ここからが本題です。

筆者が何故、カスタムバリデーションを呼び出せなかったのか?

それは、「Zeitwerk」を使わなければならんのだ。と言う思い込みが原因でした。

Zeitwerkとは?

Zeitwerk(ツァイトベルク)とは、Rails6.0から新たに組み込まれた「オートロードシステム」のことです。

  • Zeitwerkを有効にする
config/application.rb
module Myapp8
  class Application < Rails::Application
    
    config.load_defaults "6.0" # 6.0を指定すると自動的に有効になる
  • Zeitwerkが有効になっているか確認する
Rails コンソール
$ rails c
> Rails.autoloaders.zeitwerk_enabled?
=> true
  • Zeitwerkを使わない場合
config/application.rb
module Myapp8
  class Application < Rails::Application

    config.load_defaults "6.0"
    config.autoloader = :classic # クラシックモードを指定する

オートロードシステムって?

オートロードシステムとは、「命名規則に則ったファイルを自動でrequireしてくれる機能」のことです。

参考

Railsでlibディレクトリをautoload対象に含める方法

requireって?

requireとは、「外部ファイルを読み込むRubyのメソッド」のことです。

Railsのapplication.rbには初めからrequireが多用されていますね。

これは、外部のRubyファイルを読み込んでいるのです。

config/application.rb
require "rails"
require "active_model/railtie"
require "active_job/railtie"
...

なるほど。

Rails6.0ではZeitwerkを使ってファイルを自動読み込みしていると言うことなんですね。

オートロードが見ているパスを確認する

じゃあ一体どこのファイルを自動読み込みしているのか?

それは、Railsコンソールから確認が取れます。

$ rails c
> puts ActiveSupport::Dependencies.autoload_paths

実行するとファイルパスが大量に出てきました。

/Users/andou/rails/myapp8/app/channels
/Users/andou/rails/myapp8/app/controllers
/Users/andou/rails/myapp8/app/controllers/concerns
...

ほうほう。

Zeitwerkはこのファイルパス配下にある、命名規則に則ったファイルを読み込んでいるんですね。

命名規則ってどんな規則?

命名規則は、クラスとモジュール名をディレクトリ構造によって命名する規則です。

クラス・モジュールの命名規則サンプル
lib/my_gem.rb         -> MyGem
lib/my_gem/foo.rb     -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::Zoo

クラス名もしくはモジュール名にディレクトリ名をつけて命名することで、ディレクトリ構造が深くても自動でファイルが読み込まれるようになります。

デフォルトのオートロードのパス

デフォルトではautoload_pathslib のディレクトリパスが存在しません。

つまり、autoload_pathsにlibディレクトリのパスを追加することで、lib以下のファイルが読み込まれるようになります。

ここまでを3行まとめ

  • Rails6.0ではZeitwerkを使ってファイルの自動読み込みを行なっている
  • Zeitwerkは、autoload_pathsに指定されたパスのファイルを読み込んでいる
  • autoload_pathsにlibのパスを追加すればlib以下のファイルが読み込まれる

【犯した罪1】Zeitwerkを使わなければならないと言う思い込み

ここまで調べたところで、もう頭の中は、Zeitwerk(ツァイトベルク)一色。

Zeitwerkを使ってlibディレクトリのパスを追加する方法を探せばいいのね、と言う思い込みが出来上がりました。

下のように、Zeitwerkをカスタマイズすることでlibディレクトリのパスを追加することができるのですが。。。

config/application.rb
loader = Zeitwerk::Loader.new
loader.push_dir(config.root.join("./lib/validator"))
loader.setup    

やっとできた!と一息ついたとこで気づいたのです。

Railsガイドに以下のような文言があることに。

RailsアプリケーションのZeitwerkは手動で設定しないでください。

定数の自動読み込みと再読み込み (Zeitwerk) - Railsガイド

な、な、なんあななあああんだと!

じゃあやっぱりこっちなのか?

config/application.rb
config.autoload_paths += %W(#{config.root}/lib/validator)

いやいや、同じページにこんな文言があるんですけど。

自動読み込みパスの配列は、config/application.rbのconfig.autoload_pathsを書き換えることで拡張可能ではありますが、やめておきましょう。

ぬぐぐぐぐぐぐ。どっちじゃー!

libディレクトリはどうしたらえんじゃー!と久々に発狂しましたね。(脳内でね)

こんな時は自分で結論付けるしかない

結局どっちも動きます。そう、どっちも正解なのです。

こんな時は他の根拠付けをして自分で結論付けるしかありません。

そこで再度検索すると…。

見つけました。Rails6 Beta2時点の情報です。

オートロードパスの設定ポイントとして、引き続きconfig.autoload_pathsが残されます。

アプリの初期化中に手動でActiveSupport::Dependencies.autoload_pathsに設定することもできます。

Rails 6 Beta2時点のZeitwerk情報(要訳)

ここから、正解よりの正解はconfig.autoload_pathsを使う方だなと結論付けました。

まとめ

Rails6.0でオートロードパスを追加するにはconfig.autoload_pathsを使おう!

【犯した罪2】カスタムバリデーションが呼び出せない

筆者は当初、

  1. オートロードパスにlibのパスを追加し、
  2. その配下のファイル名を命名規則に則れば、
  3. lib/validatorディレクトリのファイルは読み込まれるから
  4. オートロードパスにlib/validatorとわざわざ指定しなくても良いんだ!

と考えていました。

libのディレクトリ構造
lib
├── tasks
└── validator
    └── email_validator.rb
config/application.rb(libのパスを追加)
config.autoload_paths += %W(#{config.root}/lib) 
lib/validator/email_validator.rb(命名規則の則ったクラス名)
class Validator::EmailValidator < ActiveModel::EachValidator
  
end
app/models/user.rb
class User < ApplicationRecord
  validates email: true
end

でも、これじゃ呼び出せませんでした。とほほ。。。

何故か?

カスタムバリデーションのクラス名に命名のルールがあったためです。

クラス名の規約

カスタムバリデーションのクラス名にはルールがあります。

それは、「検証名Validator」の形式でクラス名を宣言しなければならないことです。

そして、モデルから呼び出すときは、クラス名から「validator」を取り除いた上で、アンダースコア形式に変換して呼び出します。

バリデーションのクラス名 モデル側の呼び出し
EmailValidator email
UserEmailValidator user_email

上記の理由で「Validator::EmailValidator」と言うクラス名は呼び出すことができなかったのです。

まとめ

lib/validatorディレクトリにカスタムバリデーションファイルを作成する場合。

カスタムバリデーションのクラス名は「検証名Validator」にし、オートロードパスにはlib/validatorまで指定しよう。

lib/validator/email_validator.rb
class EmailValidator < ActiveModel::EachValidator
  
end
config/application.rb
config.autoload_paths += %W(#{config.root}/lib/validator)

最後に

このようにすごく遠回りした結果、一番上の結論コードにたどり着きました。

実はあのコードは、Rails5.2の設定と全く変わりありません。

前のプロジェクトからコピペしたら1分で済むことを、丸2日格闘していました。

まあこうやって腹の底まで理解できたから良しとしよう!と思う今日この頃です。

以上、Rails6.0で lib ディレクトリ以下に置いたカスタムバリデーションファイルを呼び出すために、四苦八苦した筆者の奮闘日記でした。

(おしまい)

現在、カテゴリー「Rails apiとNuxt.jsでSPA開発」のデモアプリを構築中です。記事になるまでもう少々のお時間が必要です。ブログの更新が止まって申し訳ありません。デモアプリの進捗状況は こちらの記事 で随時お伝えしてまいります。
スポンサー広告
スポンサー広告