diff --git a/lib/permittable/generator.rb b/lib/permittable/generator.rb index ec28371..6bb8961 100644 --- a/lib/permittable/generator.rb +++ b/lib/permittable/generator.rb @@ -507,7 +507,11 @@ def default_literal(value) # can be CALLED as `Model.name`: a `first-status` enum's # `Order.first-statuses.keys` parses as `Order.first - statuses.keys`, # which runs a query when the draft loads. defined_enums reaches the - # same mapping by name. + # same mapping by name. The model is also checked for the accessor + # itself (mirrors ColumnGuard.enum_keys_expr): a name can pluralize to a + # valid identifier that the model does not actually answer to — renamed + # or otherwise excluded — and calling it would raise NoMethodError when + # the draft loads. def enum_for(model, name) return nil unless model.respond_to?(:defined_enums) @@ -515,7 +519,11 @@ def enum_for(model, name) return nil unless mapping plural = name.pluralize - accessor = METHOD_NAME.match?(plural) ? "#{model.name}.#{plural}" : "#{model.name}.defined_enums[#{name.inspect}]" + accessor = if METHOD_NAME.match?(plural) && model.respond_to?(plural) + "#{model.name}.#{plural}" + else + "#{model.name}.defined_enums[#{name.inspect}]" + end { mapping: mapping, accessor: accessor } end diff --git a/spec/generator_spec.rb b/spec/generator_spec.rb index c2f41e4..6c8f7d6 100644 --- a/spec/generator_spec.rb +++ b/spec/generator_spec.rb @@ -484,6 +484,29 @@ def lock_default expect(violations(klass, { gen_odd_enum: { "first-status" => "ajar" } }, action: "update")) .to eq([{ param: "gen_odd_enum.first-status", code: "inclusion" }]) end + + it "reaches an enum whose pluralized accessor the model does not answer to, so the draft loads" do + stub_const("GenOrderNoReader", Class.new(TestModel) do + self.table_name = "gen_orders" + if ActiveRecord.version >= Gem::Version.new("7.0") + enum :status, { pending: 0, shipped: 1 } + else + enum status: { pending: 0, shipped: 1 } + end + # Simulate an accessor renamed or excluded out from under the enum: + # "statuses" matches the identifier regex, but the model no longer + # responds to it. + singleton_class.send(:undef_method, :statuses) + end) + + draft = described_class.draft(model: GenOrderNoReader) + expect(draft).to include('optional :status, :string, in: GenOrderNoReader.defined_enums["status"].keys') + + klass = load_draft(draft) + expect(violations(klass, { gen_order_no_reader: { status: "shipped" } }, action: "update")).to eq([]) + expect(violations(klass, { gen_order_no_reader: { status: "lost" } }, action: "update")) + .to eq([{ param: "gen_order_no_reader.status", code: "inclusion" }]) + end end context "with defaults the model declares rather than the database" do