Discovery and absorption for PhoenixKitCatalogue.Extension implementers
— the item/category form "extension slot" (spec §2 principle 8, §4 row
C4).
Discovery is duck-typed, mirroring PhoenixKitAI.Translatables'
ai_translatables/0 pattern: any module registered with
PhoenixKit.ModuleRegistry that exports catalogue_extensions/0 gets
its returned modules folded in, filtered to those reporting
enabled?/0. Catalogue never references an implementer by name — an
unregistered or disabled host changes nothing (spec §4 row C4's
regression rule: forms without a registered extension render exactly as
before).
Summary
Functions
Folds every enabled extension's submitted namespace into data.
All enabled extension modules contributed by registered PhoenixKit
modules, in registration order, deduplicated. An extension whose
key/0 is not a string, is empty, is underscore-prefixed, looks like
a language code, or names a catalogue-owned data key is dropped
(logged once per process) — see @reserved_keys.
Enabled extensions' contributed columns for the catalogue admin's
configurable :detail_items / :detail_categories tables (spec §2
principle 8, §4 row C4's column follow-up — the "Columns" modal on
CatalogueDetailLive).
A copied item's or category's data, with each extension's namespace
passed through that extension's optional duplicate_data/2: the
returned map replaces the namespace, nil drops it. Namespaces with no
such callback are copied as they are.
The data keys owned by registered extensions, enabled or not — a copy
must not mistake one for a language code.
Enabled extension modules that render a section for kind (:item or
:category) — i.e. export item_section/1 (or category_section/1).
Functions
@spec absorb(:item | :category, map(), map()) :: {:ok, map()} | {:error, {module(), [{atom(), String.t()}]}}
Folds every enabled extension's submitted namespace into data.
For each enabled extension E exporting the kind-appropriate cast
callback: takes params[E.key()] (a map, or %{} when absent/nil),
calls E.cast_item/2 (or cast_category/2) with the extension's
current value data[E.key()] || %{}, and merges the result under
data[E.key()]. Stops at the first extension that returns an error.
Same resilience contract as columns/1: a cast that raises, throws,
exits, or returns neither {:ok, _} nor {:error, _} is logged and
that extension's namespace keeps its current value — one broken
extension must not crash the form on every keystroke (absorb/3 runs
on validate).
@spec all() :: [module()]
All enabled extension modules contributed by registered PhoenixKit
modules, in registration order, deduplicated. An extension whose
key/0 is not a string, is empty, is underscore-prefixed, looks like
a language code, or names a catalogue-owned data key is dropped
(logged once per process) — see @reserved_keys.
@spec columns(:detail_items | :detail_categories) :: [ PhoenixKitCatalogue.Extension.column() ]
Enabled extensions' contributed columns for the catalogue admin's
configurable :detail_items / :detail_categories tables (spec §2
principle 8, §4 row C4's column follow-up — the "Columns" modal on
CatalogueDetailLive).
Each contributed id is namespaced under its extension's key/0
("<key>:<id>") so it can never collide with
PhoenixKitCatalogue.Web.TableConfig's own ids or another
extension's — enforced by rejecting a key/0 or column id that
itself carries the ":" delimiter (see
valid_column?/1), so key <> ":" <> id can never mean two different things.
Same resilience contract as sections/1 and absorb/3: a missing,
disabled, or raising extension — or one whose
item_columns/0/category_columns/0 returns a malformed entry —
contributes nothing rather than breaking the page. That contract also
covers what happens once a column is actually IN USE: a label or
render that raises, throws, exits, or returns a value
Phoenix.HTML.Safe can't turn into HTML degrades to a blank
label/empty cell for that one column instead of taking down the whole
page — see guarded_label/3 and guarded_render/3.
A copied item's or category's data, with each extension's namespace
passed through that extension's optional duplicate_data/2: the
returned map replaces the namespace, nil drops it. Namespaces with no
such callback are copied as they are.
Disabled extensions are asked too — their data is still in the row, and switching a module off must not let a copy carry its external ids. A callback that raises, or returns something else, drops its namespace (logged): a copy without the shop's fields is recoverable, a second row claiming the same external product is not.
@spec owned_keys() :: [String.t()]
The data keys owned by registered extensions, enabled or not — a copy
must not mistake one for a language code.
@spec sections(:item | :category) :: [module()]
Enabled extension modules that render a section for kind (:item or
:category) — i.e. export item_section/1 (or category_section/1).