Skip to content

BaseCharacteristic could generate update :update accept from its public attributes #171

@matt-beanland

Description

@matt-beanland

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions