Skip to content

Commit

Permalink
Auto-register namespaces (#83)
Browse files Browse the repository at this point in the history
* Auto-register namespaces

Oaken now auto-registers namespaced models as well.

So in addition to `accounts` => `Account`, we can also auto-register partial and fully nested namespaces, in this order:

```
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.

* Add a deeply-nested model to aid test coverage

* Add a partial namespace test
  • Loading branch information
kaspth authored Jun 30, 2024
1 parent 8b760ff commit 1da4cdf
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 7 deletions.
28 changes: 25 additions & 3 deletions lib/oaken/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 3 additions & 0 deletions test/dummy/app/models/menu/item/detail.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Menu::Item::Detail < ApplicationRecord
belongs_to :menu_item, class_name: "Menu::Item"
end
10 changes: 10 additions & 0 deletions test/dummy/db/migrate/20240630172609_create_menu_item_details.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 10 additions & 1 deletion test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 0 additions & 3 deletions test/dummy/db/seeds.rb
Original file line number Diff line number Diff line change
@@ -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}" },
Expand Down
2 changes: 2 additions & 0 deletions test/dummy/db/seeds/accounts/kaspers_donuts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions test/dummy/test/models/oaken_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 1da4cdf

Please sign in to comment.