PhoenixKitCatalogue.Extensions (PhoenixKitCatalogue v0.44.2)

Copy Markdown View Source

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

absorb(kind, params, data)

@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).

all()

@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.

columns(kind)

@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.

duplicate_data(kind, data)

@spec duplicate_data(:item | :category, map()) :: map()

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.

owned_keys()

@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.

sections(kind)

@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).