Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions lib/permittable/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -507,15 +507,23 @@ 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)

mapping = model.defined_enums[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

Expand Down
23 changes: 23 additions & 0 deletions spec/generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading