diff --git a/lib/oaken/seeds.rb b/lib/oaken/seeds.rb index 2942261..de06bab 100644 --- a/lib/oaken/seeds.rb +++ b/lib/oaken/seeds.rb @@ -6,16 +6,38 @@ def self.defaults(**defaults) = attributes.merge!(**defaults) def self.defaults_for(*keys) = attributes.slice(*keys) def self.attributes = @attributes ||= {}.with_indifferent_access - def self.method_missing(name, ...) - if type = name.to_s.classify.safe_constantize + # Oaken's main auto-registering logic. + # + # So when you first call e.g. `accounts.create`, we'll hit `method_missing` here + # and automatically call `register Account`. + # + # We'll also match partial and full nested namespaces like in this order: + # + # accounts => Account + # account_jobs => AccountJob | Account::Job + # account_job_tasks => AccountJobTask | Account::JobTask | Account::Job::Task + # + # If you have classes that don't follow this naming convention, you must call `register` manually. + def self.method_missing(meth, ...) + name = meth.to_s.classify + name = name.sub!(/(?<=[a-z])(?=[A-Z])/, "::") until name.nil? or type = name.safe_constantize + + if type register type - public_send(name, ...) + public_send(meth, ...) else super end end def self.respond_to_missing?(name, ...) = name.to_s.classify.safe_constantize || super + # Register a model class to be accessible as an instance method via `include Oaken::Seeds`. + # Note: Oaken's auto-register via `method_missing` means it's less likely you need to call this manually. + # + # register Account, Account::Job, Account::Job::Task + # + # Oaken uses the `table_name` of the passed classes for the method names, e.g. here they'd be + # `accounts`, `account_jobs`, and `account_job_tasks`, respectively. def self.register(*types) types.each do |type| stored = provider.new(type) and define_method(stored.key) { stored } diff --git a/test/dummy/app/models/menu/item/detail.rb b/test/dummy/app/models/menu/item/detail.rb new file mode 100644 index 0000000..fc163d1 --- /dev/null +++ b/test/dummy/app/models/menu/item/detail.rb @@ -0,0 +1,3 @@ +class Menu::Item::Detail < ApplicationRecord + belongs_to :menu_item, class_name: "Menu::Item" +end diff --git a/test/dummy/db/migrate/20240630172609_create_menu_item_details.rb b/test/dummy/db/migrate/20240630172609_create_menu_item_details.rb new file mode 100644 index 0000000..889c265 --- /dev/null +++ b/test/dummy/db/migrate/20240630172609_create_menu_item_details.rb @@ -0,0 +1,10 @@ +class CreateMenuItemDetails < ActiveRecord::Migration[7.1] + def change + create_table :menu_item_details do |t| + t.references :menu_item, null: false, foreign_key: true + t.text :description, null: false + + t.timestamps + end + end +end diff --git a/test/dummy/db/schema.rb b/test/dummy/db/schema.rb index 714fb06..887232a 100644 --- a/test/dummy/db/schema.rb +++ b/test/dummy/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_10_17_160024) do +ActiveRecord::Schema[7.1].define(version: 2024_06_30_172609) do create_table "accounts", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false @@ -26,6 +26,14 @@ t.index ["user_id"], name: "index_administratorships_on_user_id" end + create_table "menu_item_details", force: :cascade do |t| + t.integer "menu_item_id", null: false + t.text "description", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["menu_item_id"], name: "index_menu_item_details_on_menu_item_id" + end + create_table "menu_items", force: :cascade do |t| t.integer "menu_id", null: false t.string "name" @@ -69,6 +77,7 @@ add_foreign_key "administratorships", "accounts" add_foreign_key "administratorships", "users" + add_foreign_key "menu_item_details", "menu_items" add_foreign_key "menu_items", "menus" add_foreign_key "menus", "accounts" add_foreign_key "orders", "menu_items", column: "item_id" diff --git a/test/dummy/db/seeds.rb b/test/dummy/db/seeds.rb index 317c840..9d1ef9f 100644 --- a/test/dummy/db/seeds.rb +++ b/test/dummy/db/seeds.rb @@ -1,9 +1,6 @@ Oaken.prepare do defaults name: -> { "Shouldn't be used for users.name" }, title: -> { "Global Default Title" } - section :registrations - register Menu::Item - section :roots user_counter, email_address_counter = 0, 0 users.defaults name: -> { "Customer #{user_counter += 1}" }, diff --git a/test/dummy/db/seeds/accounts/kaspers_donuts.rb b/test/dummy/db/seeds/accounts/kaspers_donuts.rb index 94b309f..b1b48f5 100644 --- a/test/dummy/db/seeds/accounts/kaspers_donuts.rb +++ b/test/dummy/db/seeds/accounts/kaspers_donuts.rb @@ -11,6 +11,8 @@ sprinkled_donut = menu_items.create menu: menu, name: "Sprinkled", price_cents: 10_10 menus.label basic: menu +menu_item_details.create :plain, menu_item: plain_donut, description: "Plain, but mighty." + section :orders supporter = users.create name: "Super Supporter" orders.insert_all [user_id: supporter.id, item_id: plain_donut.id] * 10 diff --git a/test/dummy/test/models/oaken_test.rb b/test/dummy/test/models/oaken_test.rb index 90d99f7..af86d18 100644 --- a/test/dummy/test/models/oaken_test.rb +++ b/test/dummy/test/models/oaken_test.rb @@ -22,6 +22,26 @@ class OakenTest < ActiveSupport::TestCase assert menus.basic end + test "auto-registering with full namespaces" do + assert_respond_to self, :menu_items + assert_respond_to self, :menu_item_details + + menu_item_details.plain.tap do |detail| + assert_equal "Plain", detail.menu_item.name + assert_equal "Plain, but mighty.", detail.description + assert_kind_of Menu::Item::Detail, detail + end + end + + test "auto-registering with partial namespaces" do + Menu::HiddenDiscount = Class.new do + def self.table_name = "menu_hidden_discounts" + def self.column_names = [] + end + + assert_kind_of Oaken::Stored::ActiveRecord, Oaken::Seeds.menu_hidden_discounts + end + test "global attributes" do plan = plans.upsert price_cents: 10_00