Mob.Plugins (mob v0.7.0)

Copy Markdown View Source

On-device access to the activated plugins' tier-3/4 contributions.

Tiers 3 (multi-screen) and 4 (sub-app) are pure-Elixir and runtime-wired: the host needs to know, while running, which screens / lifecycle hooks / settings / notification handlers the activated plugins declared. mob_dev bakes that into the host's priv/generated/mob_plugins.exs at build time (see mix mob.regen_plugin_manifest); this module reads it once at boot and feeds the data to the core wiring (Mob.App registers the screens, the lifecycle dispatcher calls the hooks, the notification dispatch consults the handlers, and the settings store namespaces by plugin).

When no tier-3/4 plugin is active (or the manifest hasn't been regenerated) the file is absent and every accessor returns the empty set — tiers 0-2 are unaffected.

Summary

Functions

Applies the default style's theme at boot (Mob.Theme.set/1 with the style package's theme module). No default → no-op (neutral baseline / the host's own use Mob.App, theme:). A broken theme module logs instead of failing boot — the app renders baseline rather than not at all.

Boot-time entry point: load the host's manifest and register the activated plugins' screens so they're navigable. Called from Mob.App.start/0.

Activated plugins' pure-Elixir composite components (%{atom, expand: {M, F}}) — the manifest expand: ui_components form.

The configured :default_style name (or nil — neutral baseline).

Routes a notification payload to the first matching plugin handler.

Reads a plugin setting, falling back to the schema default when unset.

Caches an already-built manifest (used by load/1 and tests).

Activated plugins' lifecycle declarations.

Reads the host app's generated manifest and caches it in :persistent_term.

The cached manifest, or the empty set if nothing has been loaded.

Activated plugins' NIF module atoms. boot/1 loads each at startup so an iOS plugin NIF's load callback fires eagerly (registering any permission handler it owns) before a screen can request that permission.

Activated plugins' notification handlers, in dispatch order.

Writes a plugin setting after validating its value against the schema type.

Reads + evaluates the manifest for otp_app without caching. Returns the empty set when the priv dir or file is absent, or the file is malformed (a missing manifest must never crash boot).

Reads + evaluates a manifest from an explicit path (empty set on any failure).

Registers each manifest-declared composite expander into Mob.Composite. Malformed entries are skipped (the build validator is where shape errors surface; a stale hand-edited manifest must not crash boot).

Registers each manifest screen into Mob.Nav.Registry under an atom derived from its default_route, so the host (or the plugin's own screens) can navigate to it by route. The module is also directly navigable. The host still chooses where to surface a plugin screen in its navigation/1 structure — registration only makes the destination resolvable.

Resolves a plugin://<plugin>/<file> image reference to its on-device bundle path (assets/plugin/<plugin>/<file>), the convention native_build copies plugin images to. The renderer calls this when an image src uses the plugin:// scheme; a non-plugin URL returns :passthrough so normal image handling continues. Returns :error for a malformed plugin:// reference.

Activated plugins' screen declarations (%{plugin, module, default_route}).

Activated plugins' settings declarations.

Resolves the screen a host pushes to let users edit a plugin's settings.

Starts the tier-4 plugin supervisor (runs each plugin's lifecycle.on_start, starts its supervised children, and the lifecycle event dispatcher). Called from Mob.App.start/0 after the host's own on_start/0. No-op when no plugin declares a :lifecycle.

Activated token-only style packages (%{name, theme}), from MOB_STYLES.md's lane.

Functions

apply_default_style()

@spec apply_default_style() :: :ok

Applies the default style's theme at boot (Mob.Theme.set/1 with the style package's theme module). No default → no-op (neutral baseline / the host's own use Mob.App, theme:). A broken theme module logs instead of failing boot — the app renders baseline rather than not at all.

boot(otp_app)

@spec boot(atom() | nil) :: :ok

Boot-time entry point: load the host's manifest and register the activated plugins' screens so they're navigable. Called from Mob.App.start/0.

nil (no resolvable host app — e.g. on host BEAM / tests) is a no-op. Safe to call when no tier-3/4 plugin is active: the manifest is empty and nothing is registered.

composites()

@spec composites() :: [map()]

Activated plugins' pure-Elixir composite components (%{atom, expand: {M, F}}) — the manifest expand: ui_components form.

default_style()

@spec default_style() :: atom() | nil

The configured :default_style name (or nil — neutral baseline).

dispatch_notification(payload)

@spec dispatch_notification(map()) :: :handled | :unhandled

Routes a notification payload to the first matching plugin handler.

Walks notification_handlers/0 in order; the first handler whose :match (a map prefix-matched against the payload, or a {M,F,arity} predicate) wins, and its {M,F,arity} handler is invoked with the payload. Returns :handled or :unhandled (the host's own handle_info takes the unhandled case).

This is the pure routing core; the central notification delivery that feeds it is wired natively (Phase 3).

get_setting(plugin, key)

@spec get_setting(atom(), atom()) :: term()

Reads a plugin setting, falling back to the schema default when unset.

Backed by Mob.State (the persistent K/V store) under a per-plugin namespaced key. Returns nil for an unknown plugin/key.

install(manifest)

@spec install(map()) :: :ok

Caches an already-built manifest (used by load/1 and tests).

lifecycle()

@spec lifecycle() :: [map()]

Activated plugins' lifecycle declarations.

load(otp_app)

@spec load(atom()) :: map()

Reads the host app's generated manifest and caches it in :persistent_term.

otp_app is the host application name (e.g. :mob_plugin_demo); the file is resolved under its priv/. Called once from Mob.App.start/0. Returns the loaded manifest (also retrievable later via manifest/0).

manifest()

@spec manifest() :: map()

The cached manifest, or the empty set if nothing has been loaded.

nifs()

@spec nifs() :: [atom()]

Activated plugins' NIF module atoms. boot/1 loads each at startup so an iOS plugin NIF's load callback fires eagerly (registering any permission handler it owns) before a screen can request that permission.

notification_handlers()

@spec notification_handlers() :: [map()]

Activated plugins' notification handlers, in dispatch order.

put_setting(plugin, key, value)

@spec put_setting(atom(), atom(), term()) :: :ok | {:error, term()}

Writes a plugin setting after validating its value against the schema type.

Returns :ok, {:error, {:invalid_type, type}}, or {:error, :unknown_setting}.

read(otp_app)

@spec read(atom()) :: map()

Reads + evaluates the manifest for otp_app without caching. Returns the empty set when the priv dir or file is absent, or the file is malformed (a missing manifest must never crash boot).

read_path(path)

@spec read_path(Path.t()) :: map()

Reads + evaluates a manifest from an explicit path (empty set on any failure).

register_composites()

@spec register_composites() :: :ok

Registers each manifest-declared composite expander into Mob.Composite. Malformed entries are skipped (the build validator is where shape errors surface; a stale hand-edited manifest must not crash boot).

register_screens()

@spec register_screens() :: :ok

Registers each manifest screen into Mob.Nav.Registry under an atom derived from its default_route, so the host (or the plugin's own screens) can navigate to it by route. The module is also directly navigable. The host still chooses where to surface a plugin screen in its navigation/1 structure — registration only makes the destination resolvable.

resolve_image(ref)

@spec resolve_image(String.t()) :: {:ok, String.t()} | :passthrough | :error

Resolves a plugin://<plugin>/<file> image reference to its on-device bundle path (assets/plugin/<plugin>/<file>), the convention native_build copies plugin images to. The renderer calls this when an image src uses the plugin:// scheme; a non-plugin URL returns :passthrough so normal image handling continues. Returns :error for a malformed plugin:// reference.

screens()

@spec screens() :: [map()]

Activated plugins' screen declarations (%{plugin, module, default_route}).

settings()

@spec settings() :: [map()]

Activated plugins' settings declarations.

settings_editor(plugin)

@spec settings_editor(atom()) :: {:ok, module()} | :error

Resolves the screen a host pushes to let users edit a plugin's settings.

A tier-4 plugin owns its settings UX; the host only needs the entry point, so it calls this to get the module to push_screen/2. Returns :error when the plugin declared no editor_screen.

start_lifecycle()

@spec start_lifecycle() :: :ok

Starts the tier-4 plugin supervisor (runs each plugin's lifecycle.on_start, starts its supervised children, and the lifecycle event dispatcher). Called from Mob.App.start/0 after the host's own on_start/0. No-op when no plugin declares a :lifecycle.

styles()

@spec styles() :: [map()]

Activated token-only style packages (%{name, theme}), from MOB_STYLES.md's lane.