Packages
mob_dev
0.6.11
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/plugin.ex
defmodule MobDev.Plugin do
@moduledoc """
Compile-time host-config surface for code-generated plugins.
Spec-v2 plugins that generate their contributions from the host app's
configuration — e.g. a `mob_ash` plugin reading the host's registered
Ash domains, or a `mob_ecto` plugin reading its schemas — read that
config through this function rather than calling `Application.get_env/3`
directly. Routing every host-config read through one named surface is
what later lets the plugin audit (see `MOB_PLUGINS.md` and
`MOB_PLUGIN_SECURITY.md`) verify exactly which keys a generator touches.
When a generator runs under `with_host_config_audit/3` (which the
build-time generator runner uses), every read is checked against the
plugin's declared `:host_config_keys` and recorded; an undeclared read
fails the build loudly. Outside an audit scope (e.g. in tests) it is a
plain `Application.get_env/3`.
"""
# Process-dictionary key holding the active host-config audit scope, if any.
@audit_key :"$mob_plugin_host_config_audit"
@doc """
Reads `key` from the host application's environment, returning `default`
when the key is unset.
`otp_app` is the host app's OTP application name — the atom under which it
registers `config :my_app, ...`. Code-generated plugins call this during
the compile step:
domains = MobDev.Plugin.host_config(:my_app, :ash_domains, [])
Under an audit scope, reading a key the plugin didn't declare in its
manifest `:host_config_keys` raises — the generator must declare what it
touches so `mix mob.audit_plugins` can verify it.
"""
@spec host_config(atom(), atom(), term()) :: term()
def host_config(otp_app, key, default \\ nil)
when is_atom(otp_app) and is_atom(key) do
case Process.get(@audit_key) do
nil ->
:ok
%{plugin: plugin, allowed: allowed} = ctx ->
unless key in allowed do
raise ArgumentError,
"plugin #{inspect(plugin)} read host config key #{inspect(key)} not declared in its " <>
"manifest :host_config_keys (declared: #{inspect(allowed)}). Add it to the manifest."
end
Process.put(@audit_key, %{ctx | reads: [{otp_app, key} | ctx.reads]})
end
Application.get_env(otp_app, key, default)
end
@doc """
Runs `fun` with host-config auditing scoped to `plugin` (allowing only the
keys in `allowed`, the plugin's manifest `:host_config_keys`). Returns
`{result, reads}` where `reads` is the ordered list of `{otp_app, key}` the
generator actually touched. Nested scopes restore the prior one on exit.
"""
@spec with_host_config_audit(atom(), [atom()], (-> result)) :: {result, [{atom(), atom()}]}
when result: term()
def with_host_config_audit(plugin, allowed, fun)
when is_atom(plugin) and is_list(allowed) and is_function(fun, 0) do
prev = Process.get(@audit_key)
Process.put(@audit_key, %{plugin: plugin, allowed: allowed, reads: []})
try do
result = fun.()
%{reads: reads} = Process.get(@audit_key)
{result, Enum.reverse(reads)}
after
if prev, do: Process.put(@audit_key, prev), else: Process.delete(@audit_key)
end
end
@doc """
The activated plugin names — `config :mob, :plugins` from `mob.exs`.
Activation is the second opt-in step (see `MOB_PLUGINS.md`): a plugin in
`deps` contributes nothing until it appears here. Falls back to the loaded
Application env, then `[]`.
"""
@spec activated_names() :: [atom()]
def activated_names do
config_file = Path.join(File.cwd!(), "mob.exs")
if File.exists?(config_file) do
config_file
|> Config.Reader.read!()
|> Keyword.get(:mob, [])
|> Keyword.get(:plugins, [])
else
Application.get_env(:mob, :plugins, [])
end
rescue
_ -> Application.get_env(:mob, :plugins, [])
end
@doc """
The activated plugins as `{plugin_dir, manifest}` pairs, ready for
`MobDev.Plugin.Merge`.
Resolves each activated name to its dependency directory and loads its
manifest (nil for a tier-0 plugin). Activated names that don't resolve to a
dep are skipped — `mix mob.plugins` is where that mismatch surfaces to users.
"""
@spec activated() :: [{Path.t(), map() | nil}]
def activated do
deps = Mix.Project.deps_paths()
for name <- activated_names(), dir = deps[name], not is_nil(dir) do
case MobDev.Plugin.Manifest.load(dir) do
{:ok, manifest} -> {dir, manifest}
{:error, _reason} -> {dir, nil}
end
end
end
end