Description
Every typed characteristic resource that uses BaseCharacteristic as a fragment ends up writing two parallel lists: the public attributes, and the update :update accept [...] list. They are almost always identical — the accept list is "every public attribute on this characteristic" with very few exceptions.
attributes do
attribute :device_name, :string, public?: true
attribute :family, :atom, public?: true
attribute :model, :string, public?: true
attribute :technology, :atom, public?: true
end
actions do
create :create do
accept [:name, :device_name, :family, :model, :technology]
argument :instance_id, :uuid
argument :feature_id, :uuid
change manage_relationship(:instance_id, :instance, type: :append)
change manage_relationship(:feature_id, :feature, type: :append)
end
update :update do
accept [:device_name, :family, :model, :technology]
end
end
We have 10 of these typed-characteristic modules in diffo_example and the accept lists drift gently away from the attribute lists as edits land. The :create accept list adds the BaseCharacteristic :name slot; the :update accept list omits it — that's the only meaningful difference.
What we'd find useful
That BaseCharacteristic ships a default :create and :update action that already accept all public attributes the consumer declares, with :name automatically included on :create and excluded from :update. A consumer would only need to override when they want a non-default shape:
defmodule MyApp.SpeedCharacteristic do
use Ash.Resource, fragments: [Diffo.Provider.BaseCharacteristic], domain: MyApp.SRM
attributes do
attribute :downstream_mbps, :integer, public?: true
attribute :upstream_mbps, :integer, public?: true
end
# :create and :update generated by BaseCharacteristic
end
Why it matters
The current shape is boilerplate that every typed characteristic re-declares, and it's the kind of duplication that goes wrong silently — if you add a new attribute but forget to add it to the update accept list, the field is silently un-settable through the standard update path. We hit exactly this during migration (the :device_name field was missing from a couple of update accept lists and a NoSuchInput error surfaced at test time, not at compile time).
The relationship management (manage_relationship(:instance_id, ...) and manage_relationship(:feature_id, ...)) is equally repetitive across all typed characteristics — that's a sibling case for the same generation.
A possible direction
A spark transformer in BaseCharacteristic could synthesise the default :create and :update actions at compile time from the resource's declared public attributes (modulo :name placement). Consumers who want a narrower accept list just override by declaring the action explicitly. Same shape could apply to BaseInstance, BaseParty, BasePlace where it makes sense.
Related: #170 — same "diffo holds the inputs, consumer re-derives them" pattern as the after-action change modules.
Description
Every typed characteristic resource that uses
BaseCharacteristicas a fragment ends up writing two parallel lists: the public attributes, and theupdate :update accept [...]list. They are almost always identical — the accept list is "every public attribute on this characteristic" with very few exceptions.We have 10 of these typed-characteristic modules in diffo_example and the accept lists drift gently away from the attribute lists as edits land. The
:createaccept list adds the BaseCharacteristic:nameslot; the:updateaccept list omits it — that's the only meaningful difference.What we'd find useful
That
BaseCharacteristicships a default:createand:updateaction that already accept all public attributes the consumer declares, with:nameautomatically included on:createand excluded from:update. A consumer would only need to override when they want a non-default shape:Why it matters
The current shape is boilerplate that every typed characteristic re-declares, and it's the kind of duplication that goes wrong silently — if you add a new attribute but forget to add it to the update accept list, the field is silently un-settable through the standard update path. We hit exactly this during migration (the
:device_namefield was missing from a couple of update accept lists and a NoSuchInput error surfaced at test time, not at compile time).The relationship management (
manage_relationship(:instance_id, ...)andmanage_relationship(:feature_id, ...)) is equally repetitive across all typed characteristics — that's a sibling case for the same generation.A possible direction
A spark transformer in
BaseCharacteristiccould synthesise the default:createand:updateactions at compile time from the resource's declared public attributes (modulo:nameplacement). Consumers who want a narrower accept list just override by declaring the action explicitly. Same shape could apply toBaseInstance,BaseParty,BasePlacewhere it makes sense.Related: #170 — same "diffo holds the inputs, consumer re-derives them" pattern as the after-action change modules.