Skip to content

Leave a singular association with nested attributes unwrapped - #54

Merged
nz merged 1 commit into
nz/optional-has-onefrom
nz/nested-attributes-unwrap
Aug 12, 2026
Merged

Leave a singular association with nested attributes unwrapped#54
nz merged 1 commit into
nz/optional-has-onefrom
nz/nested-attributes-unwrap

Conversation

@nz

@nz nz commented Aug 12, 2026

Copy link
Copy Markdown
Member

Third of the stack, based on #53 — review that first, and this diff will shrink to its own commit once #53 merges.

Nested attributes on a singular association raise today

accepts_nested_attributes_for assigns through the public reader, and ActiveRecord asks whatever it finds there whether it is a new record. A None answers nil? but is not nil, so it reaches that question and raises:

class Book < ApplicationRecord
  include Errgonomic::Rails::ActiveRecordOptional

  belongs_to :author, optional: true
  accepts_nested_attributes_for :author
end

Book.create!(title: "Nested", author_attributes: { name: "Cixin Liu" })
# => Errgonomic::UnwrappedAccessError: undefined method `new_record?' for None

This is not new to the has_one work in #53: it is true on the released gem for any optional: true belongs_to, and it takes out build, update and destroy for the association. In the application driving this rollout it blocks six models that are otherwise ready — account.service_agreement, order_form_item.price, price.plan, cluster.space, cluster.vault, member_invitation.user.

Same shape as the encrypts exclusion

ActiveRecord needs a bare nil here, the way its encryption machinery needs the raw value behind encrypts. So this takes the shape already established for that: the association is excluded and keeps its plain reader.

The exclusion is read from nested_attributes_options, ActiveRecord's own register, rather than recorded as the macro goes past — the same way the encrypts exclusion is read from encrypted_attributes. That matters after #52: exclusions are computed when a reader is about to be wrapped, so anything that tried to append to the computed list would be silently dropped.

The concern's compromise 5 is reworded from "attributes declared with encrypts" to the general rule it now expresses: readers that ActiveRecord's own machinery reads raw. Still five compromises, not six.

Testing

rake — 30 tests, 81 assertions, and 118 doctests, all passing. The new cases cover the full nested cycle on a has_one (build, update, destroy), build on an optional: true belongs_to, and that errgonomic_optionals and errgonomic_optional_exclusions report the association as unwrapped so a conversion can be checked.

accepts_nested_attributes_for assigns through the public reader and asks
whatever it finds there whether it is a new record. A None answers nil? but
is not nil, so it reached that question and raised, taking out build,
update and destroy for the association — on an optional belongs_to as much
as on a has_one.

ActiveRecord needs a bare nil here, the way it needs the raw value behind
encrypts, so this takes the same shape: the association is recorded as an
exclusion and keeps its plain reader.
@nz
nz merged commit e9e2a3a into nz/optional-has-one Aug 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants