iT邦幫忙

2025 iThome 鐵人賽

DAY 30
0

本篇聚焦「讀者如何在自己的專案導入 Rails generator」,不再解釋 Mongory 本身
筆者以 Mongory 的 mongory:installmongory:matcher 兩個 generator 作為範本,完整拆解檔案結構、模板寫法(含 ERB 空白陷阱)、initializer 內容注入與常見誤區,讓讀者能在任何 Rails 專案中快速打造一套可維護、可擴充且可重複使用的 generator。

——

讀者能學到什麼

  • 如何在專案中建立 rails g your_namespace:installrails g your_namespace:xxx 的 generator。
  • 如何撰寫 .erb 模板並避免多餘空白與重複內容注入。
  • 如何讓 generator 具備「偵測環境、自我校正、可重入(idempotent)」特性。
  • 以 Mongory 的實作為範本,快速複製到讀者的專案域裡。

——

一、Rails generator 的最小骨架

在任何 Rails gem 或應用中,generator 的慣例路徑如下:

lib/
  generators/
    your_namespace/
      install/
        install_generator.rb
        templates/
          initializer.rb.erb
      thing/
        thing_generator.rb
        templates/
          thing.rb.erb

核心類別命名規則:

  • YourNamespace::Generators::InstallGenerator < Rails::Generators::Base
  • YourNamespace::Generators::ThingGenerator < Rails::Generators::NamedBase

差別在於 NamedBase 會幫讀者處理 name 參數(例如 rails g your_namespace:thing class_in)。

只要你的 Gem 檔案結構符合 Rails generator 慣例,就自動可以連結到指令。

——

二、實作示例 1:Install generator(建立 initializer)

以 Mongory 實作為範本,Install generator 目標是產生 config/initializers/xxx.rb,並可依「已安裝的 gem」自動調整模板內容。

關鍵觀念:

  • 在執行期間偵測環境(例如以 Bundler 解析 lockfile),決定模板中的選段要不要輸出。
  • 使用 template 'xxx.erb', 'target/path'。ERB 內可寫條件分支,但要注意空白控制(下一節詳述)。

參考骨架(節錄自 Mongory):

module YourNamespace
  module Generators
    class InstallGenerator < Rails::Generators::Base
      source_root File.expand_path('templates', __dir__)

      def create_initializer_file
        @use_ar      = gem_used?('activerecord')
        @use_mongoid = gem_used?('mongoid')
        @use_sequel  = gem_used?('sequel')

        template 'initializer.rb.erb', 'config/initializers/your_namespace.rb'
      end

      private

      def gem_used?(gem_name)
        Bundler.locked_gems.dependencies.key?(gem_name)
      end
    end
  end
end

模板 initializer.rb.erb 建議做到:

  • 內容自我說明(加上註解說明該怎麼擴充)。
  • 依偵測結果切換片段,例如只有在 @use_mongoid 時才註冊 Mongoid::CriteriaBSON::ObjectId 的轉換。
  • 使用 ERB 的「裁切語法」避免空白(見下一節)。

——

三、模板 ERB 的空白陷阱:-%> 與行末空白

很多生成器的模板在插入條件段落後,會多出空白行或多餘縮排,造成產生檔案髒亂、diff 不穩。Mongory 的 initializer.rb.erb 採用 ERB 的裁切語法抑制空白:

重點規則:

  • <% if cond -%><% end -%>
    • 行頭不要跟隨縮排習慣,會造成多餘空白。
    • 行尾加上 -%>,會去除換行,使產生程式碼乾淨。
  • <%= value -%>:輸出值後去除該標記右邊的換行與空白。
  • 嵌入條件塊時,盡量把結尾 -%> 放在行尾,避免產出空白行。

對照示例:

<% if @use_mongoid -%>
  mc.register(Mongoid::Criteria)
  mc.data_converter.configure do |dc|
    dc.register(Mongoid::Document, :as_document)
    dc.register(BSON::ObjectId, :to_s)
  end
<% end -%>

若拿掉 -%>,相同條件在不同環境下會殘留空白行,導致不必要的 diff。裁切語法能保證輸出緊湊、穩定。

——

四、實作示例 2:功能型 generator(以 matcher 為例)

NamedBase 類型的 generator 適合用來生成可命名的類別與對應測試,並更新既有設定檔。Mongory 的 指令mongory:matcher 做了三件事:

  1. name 產生兩個檔案(類別與規格測試)。
  2. 若 initializer 尚未存在,先呼叫 install 生成。
  3. 把新 matcher 的 require 行插入到 initializer(避免重複並維持排序/群組)。

核心步驟(抽象化示例):

class ThingGenerator < Rails::Generators::NamedBase
  source_root File.expand_path('templates', __dir__)

  def create_files
    @class_name   = "#{class_name}Thing"
    @file_name    = file_name

    template 'thing.rb.erb',  "lib/your_namespace/things/#{@file_name}_thing.rb"
    template 'thing_spec.rb.erb', "spec/your_namespace/things/#{@file_name}_thing_spec.rb"
  end

  def update_initializer
    initializer_path = 'config/initializers/your_namespace.rb'
    require_line = "require \"#\{Rails.root\}/lib/your_namespace/things/#{@file_name}_thing\""

    YourNamespace::Generators::InstallGenerator.start unless File.exist?(initializer_path)

    content = File.read(initializer_path)
    return if content.include?(require_line) # idempotent

    required_lines = content.scan(/.*require\s+[\"'].*_thing[\"'].*/)
    anchor = required_lines.empty? ? "# frozen_string_literal: true" : required_lines.last
    inject_into_file initializer_path, "\n#{require_line}\n", after: "#{anchor}\n"
  end
end

這樣的做法有幾個工程優點:

  • 可重入(不會重複插入相同行)。
  • 以「最後一條 require」為錨點,維持群組性與相對順序。
  • 初次使用會自動建立 initializer,不需要多教一步。

——

五、Initializer 設定範例(以 Mongory 為例,抽象可遷移)

以下列出 Mongory initializer 中幾個設計點,供讀者在自家專案借鏡:

  • 啟用語法糖(例:為 Symbol 加上查詢子句幫手):
mc.enable_symbol_snippets!
  • 註冊可被 DSL 擴充的「集合型別」(Array 或 ORM 查詢結果等):
mc.register(Array)
mc.register(ActiveRecord::Relation)  # 若專案使用 AR
mc.register(Mongoid::Criteria)       # 若專案使用 Mongoid
mc.register(Sequel::Dataset)         # 若專案使用 Sequel
  • Data/Key/Value converter 的分層配置:
mc.data_converter.configure do |dc|
  dc.register(ActiveRecord::Base, :attributes)
  # dc.register(Mongoid::Document, :as_document)
  # dc.register(BSON::ObjectId, :to_s)
  # dc.register(Sequel::Model) { values.transform_keys(&:to_s) }
end

mc.condition_converter.configure do |cc|
  cc.key_converter.configure do |kc|
    # kc.register(MyKeyObject, :to_string_key_pair)
  end

  cc.value_converter.configure do |vc|
    # vc.register(MyWrapperType) { unwrap_value }
  end
end

筆者建議把「範例註冊」與「專案實際註冊」以註解與段落清楚區隔,讀者將來維護時較不易混淆。

——

六、常見誤區與對策

  • 重複注入 require 行:未檢查既有內容。對策:在插入前 content.include?(line)
  • 插入錨點不穩:硬插入到檔案頂部/底部,導致群組混亂。對策:以「最後一條同類 require」為錨點。
  • 模板多餘空白行:ERB 未使用 -%>。對策:條件塊與輸出使用裁切語法,並檢視產出檔。
  • 未偵測環境:模板內容對 AR/Mongoid/Sequel 一視同仁。對策:以 Bundler 解析 lockfile 再決定片段輸出。
  • 非可重入設計:第二次執行 generator 破壞檔案。對策:所有變更皆設計為冪等(可重複執行)。

——

七、一步到位的驗收清單(拷貝就能用)

  1. 在專案加入上述骨架,將命名空間改為讀者自己的 YourNamespace
  2. 寫好 initializer.rb.erb,確保在不同 ORM 組合下輸出穩定(以 -%> 控制空白)。
  3. 功能型 generator(如 matcher)能:
    • 產生類別與測試檔。
    • 若無 initializer,先自動建立。
    • 以錨點插入 require,且不重複。
  4. 實際執行:
rails g your_namespace:install
rails g your_namespace:thing awesome

看見 initializer 被建立且 require 正確插入,並能跑起測試,即完成驗收。

——

八、延伸:把 Mongory 的做法用在讀者的專案

讀者可以直接把本文的骨架與策略搬到任何 Rails gem 或應用中:

  • 以 Install generator 建立「最小可運作」設定檔,內含註解型指南。
  • 以功能型 generator 漸進生成模組與測試,並把註冊/載入的工作集中到 initializer。
  • 嚴格使用 ERB 裁切語法,確保輸出穩定、diff 乾淨。

如需更完整參考,可對照 Mongory 專案中的:

  • lib/generators/mongory/install/install_generator.rb
  • lib/generators/mongory/matcher/matcher_generator.rb
  • lib/generators/mongory/install/templates/initializer.rb.erb

這些範本可以一鍵套用到讀者的命名空間與領域模型中。

——

結語:Rails generator 的價值在於「讓讀者的專案成為可複製的工程產品」。以 Mongory 為範本,讀者能快速打造穩定、可重入、可擴充的 generator 系統,持續把日常操作沉澱為一鍵自動化。

專案首頁(Ruby 版)


上一篇
Day 29:總結與行動呼籲:開源協作與下一個里程碑
下一篇
Bonus 2:C → Ruby bridge 落地手冊
系列文
Mongory:打造跨語言、高效能的萬用查詢引擎33
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言