Nested, namespaced model incompatible with `form_with` path helper

While I suspect this is probably not a “bug” per-se, I’ve experienced some confusing behavior when using nested, namespaced models with the form helper. Here is a complete repro to explain the issue better:

# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  gem "rails"
  gem "sqlite3", "~> 1.4"
end

require "active_record"
require "logger"
require "action_controller/railtie"

ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)

ActiveRecord::Schema.define do
  create_table :accounts, force: true do |t|
    t.string :name
  end

  create_table :account_entries, force: true do |t|
    t.string :name
    t.references :account, foreign_key: true
  end
end

class Account < ActiveRecord::Base
  has_many :entries, class_name: "Account::Entry"
end

class Account::Entry < ActiveRecord::Base
  belongs_to :account
end

class TestApp < Rails::Application
  config.root = __dir__
  config.hosts << "example.org"
  config.secret_key_base = "secret_key_base"

  config.logger = Logger.new($stdout)
  Rails.logger  = config.logger

  routes.draw do
    get "/" => "test#index"

    resources :accounts do 
      resources :entries, module: :account
    end
  end
end

class TestController < ActionController::Base
  include Rails.application.routes.url_helpers

  def index
    account = Account.create! name: "Test Account"
    entry = account.entries.create! name: "Test Entry"

    render inline: <<~ERB, locals: { account: account, entry: entry }
      <%= form_with model: [account, entry] do |f| %>
        <%= f.text_field :name %>
      <% end %>
    ERB
  end
end

require "minitest/autorun"
require "rack/test"

class BugTest < Minitest::Test
  include Rack::Test::Methods

  def test_generates_wrong_path
    begin
      get "/"
    rescue => exception
      assert_match(/ActionView::Template::Error \(undefined method `account_account_entry_path' for an instance of/, exception.message)
    end
  end

  private
    def app
      Rails.application
    end
end

Given this behavior, it leaves me with a couple of questions:

  • Is overriding the url the correct solution to this? Or is there a less cumbersome way to handle this?
  • Does this potentially indicate a modeling / routes issue? I’ve placed Entry in the Account::Entry namespace as a way to communicate, “An Entry is highly coupled and relevant to an Account”, but it seems to fight against many of the Rails helpers like this. Is this an improper usage of namespacing?

Any guidance / ideas would be appreciated!

1 Like

I tried something like this a few years ago, and after months trying to get it to work on a side branch I abandoned it and went with flat models, and namespaced controllers. I can’t describe how frustrating it was.

I kept finding phantom indications on the internet of people doing this, but they never worked. If you somehow get it please let me know, but i think you’ve isolated the issue quicker than i did. I feel like i finally ran across a thread where DHH advised flat models (the path I ended up taking), and against namespacing models, but i can’t find it.

My constraint here is actually on the model side of things. If I were to make Entry a flat model, it wouldn’t necessarily make much sense in my domain, hence why I’ve namespaced it with Account::Entry, which produces a semantic DB table of account_entries.

I suppose a model of AccountEntry vs. Account::Entry may work better here, but would love to take advantage of namespacing as it does provide good organization to the project and communicate the domain concepts well.

I was surprised how few threads are out there surrounding this topic, leading me to believe I’m either missing something obvious or misusing namespaces entirely.

Yes, that was very much like my consideration, and what you describe is what i ended up doing.

We have a model I wanted to namespace as Location::Description — because there are other models that have Descriptions too, so a flat Description model would not make sense. I remember now, the path helpers were definitely a gotcha. IIRC I tried monkey patching the helper class, but I couldn’t get it to work, plus it felt really hacky… and that’s when I believe I ran across the DHH advice in some thread.

So I went with LocationDescription as my “flat model” name — it inherits from a Description base class, and the db table is still location_descriptions as desired.

I did use namespacing in the controllers, we now have a Locations::DescriptionsController.

Now that i’m searching again, namespacing models does seem possible, just involved. Here’s a good discussion of why it is the way it is, and what’s necessary to override it, by SO Rails guru “max”:

Thanks for the response, this is a really helpful thread to read through! I hadn’t thought about LocationDescription rather than Location::Description. That sounds like a much less frustrating approach.

That said, after reading through the thread you posted, I did some additional digging on this and found a thread talking about overriding ActiveModel::Naming, and a SO answer by max reiterating this approach.

Another relevant thread suggests adding in use_relative_model_naming?, but this seemed less documented than overriding ActiveModel::Naming to me.

I’ve implemented this:

def model_name
  ActiveModel::Name.new(self, Account)
end

And seems to work great!

The output of polymorphic_path([entry.account, entry]) returns the expected account_entry_path according to what I would expect based on these routes:

resources :accounts do
  scope module: :account do
    resources :entries, only: %i[ edit update show destroy ]
  end
end

Thought I’d leave this here for future reference in case others stumble upon it.

In my case, having the /account namespace in the models folder is a huge organizational benefit due to the size of the codebase, so the complexity tradeoff is worth it. Otherwise, I’d probably opt for the AccountEntry approach in the future. Just wish this one hadn’t required so much digging as it seems to be a common confusion in Rails.

1 Like