SvEx.Plugin.Behavior behaviour (SvEx v0.4.2)

The contract a plugin implements.

A plugin installs one infrastructure concern into a generated project — a cache, a container runtime, a vault, an auth stack — and knows nothing about what the project is for. Most of a plugin is data: the files it owns, the dependencies it needs, the capabilities it requires and provides. Only two callbacks are code, and both are escape hatches.

This behaviour is public and stable from day one because a plugin may live in a separate repository and must install without editing SvEx's source (R4).

Defining a plugin

defmodule MyOrg.Valkey do
  use SvEx.Plugin.Behavior, name: :valkey, version: "1.3.0"

  @impl true
  def files(_config) do
    [
      {"lib/APP/cache.ex", :sole_owner},
      {"compose.yaml", :contributes, key: :valkey_service}
    ]
  end

  @impl true
  def deps, do: [{:nebulex, "~> 3.0"}]

  @impl true
  def requires, do: [:container_runtime]
end

name/0 and version/0 come from the use options. files/1 must be implemented. deps/0, requires/0, provides/0 and conflicts/0 default to [] and are overridable. transform/2 and upgrade/3 are optional.

Why so little of it is code

deps/0 is a declared list rather than something read back out of the plugin's own mix.exs (D10). A plugin that reflects its own project copies in_umbrella: true and path deps straight into the target and breaks it.

Everything a plugin returns must be deterministic — the same inputs must produce the same bytes today and in eighteen months, because an update regenerates a historical version and diffs it (D8). No timestamps, no randomness, no map iteration order leaking into output.

The two escape hatches, and how they must fail

transform/2 and upgrade/3 exist for what the file list cannot express. Both must let exceptions propagate. Never wrap the body in a bare rescue.

This is not style. Fireside swallowed exceptions from its equivalents in a bare rescue and bumped the recorded version anyway, so a transform that did nothing was recorded as having succeeded — the manifest became a claim rather than a fact. SvEx records a version only after every step actually succeeded (G5), and that is only true if failure is loud.

Summary

Types

A named capability, used to resolve ordering and conflicts.

The resolved contents of the project's target.exs.

A dependency to add to the target project, never to the plugin.

A file the plugin writes.

The plugin's identity — unique within a project.

An explicit path to the generated project's root.

A semver string. Integers are not accepted (D5).

Callbacks

Capabilities this plugin cannot coexist with. Defaults to [].

Dependencies to add to the target project. Defaults to [].

Every file this plugin writes, with its ownership mode.

The plugin's identity. Supplied by use SvEx.Plugin.Behavior, name: ....

Capabilities this plugin satisfies for others. Defaults to [].

Capabilities this plugin needs from others. Defaults to [].

Anything the file list cannot express, run at install time.

Migrate a project from one version of this plugin to another.

The plugin's semver version. Supplied by use SvEx.Plugin.Behavior, version: ....

Types

capability()

@type capability() :: atom()

A named capability, used to resolve ordering and conflicts.

config()

@type config() :: map()

The resolved contents of the project's target.exs.

dep()

@type dep() :: {atom(), String.t()} | {atom(), String.t(), keyword()}

A dependency to add to the target project, never to the plugin.

file_spec()

@type file_spec() :: {Path.t(), mode_keys()} | {Path.t(), mode_keys(), keyword()}

A file the plugin writes.

The options carry :key for a :contributes entry's idempotency key, and :when to restrict the entry to one base, as in when: [base: :web].

mode()

@type mode() :: :sole_owner | :contributes | :seed | 0 | 1 | 2

mode_keys()

@type mode_keys() :: :sole_owner | :contributes | :seed

mode_values()

@type mode_values() :: 0 | 1 | 2

name()

@type name() :: atom()

The plugin's identity — unique within a project.

root()

@type root() :: Path.t()

An explicit path to the generated project's root.

version()

@type version() :: String.t()

A semver string. Integers are not accepted (D5).

Callbacks

conflicts()

@callback conflicts() :: [capability()]

Capabilities this plugin cannot coexist with. Defaults to [].

deps()

@callback deps() :: [dep()]

Dependencies to add to the target project. Defaults to [].

files(config)

@callback files(config()) :: [file_spec()]

Every file this plugin writes, with its ownership mode.

name()

@callback name() :: name()

The plugin's identity. Supplied by use SvEx.Plugin.Behavior, name: ....

provides()

@callback provides() :: [capability()]

Capabilities this plugin satisfies for others. Defaults to [].

requires()

@callback requires() :: [capability()]

Capabilities this plugin needs from others. Defaults to [].

transform(root, config)

(optional)
@callback transform(root(), config()) :: :ok

Anything the file list cannot express, run at install time.

Must raise on failure — see the module documentation.

upgrade(root, version, version)

(optional)
@callback upgrade(root(), version(), version()) :: :ok

Migrate a project from one version of this plugin to another.

Must raise on failure — see the module documentation.

version()

@callback version() :: version()

The plugin's semver version. Supplied by use SvEx.Plugin.Behavior, version: ....

Functions

__using__(opts)

(macro)

Declares the module a plugin.

Requires :name (an atom) and :version (a semver string), and supplies overridable deps/0, requires/0, provides/0 and conflicts/0 returning []. Both options are validated at compile time, so a malformed plugin fails when it is built rather than when a project tries to install it.

is_mode(value)

(macro)

is_mode_key(value)

(macro)

is_mode_value(value)

(macro)

mode(value)

(macro)

mode(value, type)

(macro)

mode_enumerators()

(macro)

mode_keys()

(macro)

mode_values()

(macro)