Packages
mob_dev
0.5.14
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/manifest.ex
defmodule MobDev.Plugin.Manifest do
@moduledoc """
Reads, validates, and classifies a plugin's `priv/mob_plugin.exs` manifest.
The manifest is data, not code (see `MOB_PLUGINS.md`): a plain Elixir map
describing what a plugin contributes. This module is the single place that
turns that map into a validated, classified description — tier, hot-push
status, activation — that `mix mob.plugins` reports and the compile-time
merge will later consume.
A tier-0 plugin has no manifest at all; `load/1` returns `{:ok, nil}` for
that case, and `tier(nil)` is `0`.
"""
@manifest_path "priv/mob_plugin.exs"
# Spec versions this mob_dev understands. Bumped when MOB_PLUGINS.md makes a
# breaking schema change; old plugins keep validating against old specs.
@supported_spec_versions [1, 2]
@native_sections [
:nifs,
:nifs_generator,
:android,
:ios,
:ui_components,
:ui_components_generator
]
@screen_sections [:screens, :screens_generator, :migrations, :assets]
@subapp_sections [:lifecycle, :settings, :notifications]
@visual_sections [:ui_components, :ui_components_generator]
@tier1_sections [:nifs, :nifs_generator, :android, :ios]
# Sections whose Elixir half can still hot-push even when the plugin has
# native code (the partial case).
@pushable_sections [:screens, :screens_generator, :lifecycle, :migrations]
@doc """
Loads the manifest for a plugin checked out at `plugin_dir`.
Returns `{:ok, nil}` when there is no `priv/mob_plugin.exs` (a tier-0
plugin), `{:ok, map}` when one is present and evaluates to a map, or
`{:error, reason}` when the file is unreadable or doesn't yield a map.
Does not validate field contents — call `validate/1` for that.
"""
@spec load(Path.t()) :: {:ok, map() | nil} | {:error, String.t()}
def load(plugin_dir) do
path = Path.join(plugin_dir, @manifest_path)
if File.exists?(path) do
eval(path)
else
{:ok, nil}
end
end
defp eval(path) do
case Code.eval_file(path) do
{map, _bindings} when is_map(map) ->
{:ok, map}
{other, _bindings} ->
{:error, "#{path} must evaluate to a map, got: #{inspect(other)}"}
end
rescue
e -> {:error, "#{path} failed to evaluate: #{Exception.message(e)}"}
end
@doc """
Validates a manifest map against the spec's required top-level fields.
Returns `{:ok, manifest}` or `{:error, reasons}` with a list of every
problem found (validation never stops at the first error). `nil` (no
manifest) is valid — a tier-0 plugin has nothing to validate.
"""
@spec validate(map() | nil) :: {:ok, map() | nil} | {:error, [String.t()]}
def validate(nil), do: {:ok, nil}
def validate(manifest) when is_map(manifest) do
errors =
[]
|> check_name(manifest)
|> check_mob_version(manifest)
|> check_spec_version(manifest)
case errors do
[] -> {:ok, manifest}
errs -> {:error, Enum.reverse(errs)}
end
end
def validate(other),
do:
{:error, ["manifest must be a map (the priv/mob_plugin.exs data), got: #{inspect(other)}"]}
defp check_name(errors, %{name: name}) when is_atom(name) and not is_nil(name), do: errors
defp check_name(errors, _), do: [":name is required and must be an atom" | errors]
defp check_mob_version(errors, %{mob_version: req}) when is_binary(req) do
case Version.parse_requirement(req) do
{:ok, _} -> errors
:error -> [":mob_version #{inspect(req)} is not a valid version requirement" | errors]
end
end
defp check_mob_version(errors, _),
do: [
":mob_version is required and must be a version requirement string (e.g. \"~> 0.6\")"
| errors
]
defp check_spec_version(errors, %{plugin_spec_version: v}) when is_integer(v) do
if v in @supported_spec_versions do
errors
else
[
"plugin_spec_version #{v} is not supported (this mob_dev knows #{inspect(@supported_spec_versions)})"
| errors
]
end
end
defp check_spec_version(errors, _),
do: [":plugin_spec_version is required and must be an integer" | errors]
@doc """
Classifies the plugin tier (0–4) from which capability sections are present.
Highest matching section wins. `nil` (no manifest) is tier 0. A manifest
with required fields but no capability sections is the tier-1 floor (the
"minimum viable manifest").
"""
@spec tier(map() | nil) :: 0..4
def tier(nil), do: 0
def tier(m) when is_map(m) do
cond do
has_any?(m, @subapp_sections) -> 4
has_any?(m, @screen_sections) -> 3
has_any?(m, @visual_sections) -> 2
has_any?(m, @tier1_sections) -> 1
true -> 1
end
end
@doc """
Whether the plugin's contributions can be hot-pushed without a native rebuild.
Computed from populated sections, not from tier: `true` for pure-Elixir
plugins, `false` for native-only ones (NIFs / components), and `:partial`
when a plugin mixes native code with hot-pushable Elixir (screens, lifecycle).
"""
@spec hot_pushable(map() | nil) :: true | false | :partial
def hot_pushable(nil), do: true
def hot_pushable(m) when is_map(m) do
cond do
not has_any?(m, @native_sections) -> true
has_any?(m, @pushable_sections) -> :partial
true -> false
end
end
defp has_any?(map, keys), do: Enum.any?(keys, &Map.has_key?(map, &1))
end