Packages
mob_dev
0.6.5
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/style.ex
defmodule MobDev.Style do
@moduledoc """
The styles lane (MOB_STYLES.md), minimum-viable slice: token-only style
packages. A style package ships `priv/mob_style.exs` declaring a theme
module; activation is `config :mob, :styles, [...]` plus
`config :mob, :default_style, :name` in `mob.exs`, and the runtime manifest
carries the resolved set so core can apply the default style's theme at boot.
Tokens-only is the doc's smallest tier ("repalette-only styles"); the
per-component native-override tier (cascade, `_style` dispatch — the mob_m3
case) is NOT implemented yet.
"""
@manifest_path "priv/mob_style.exs"
@supported_spec_versions [1]
@doc """
Loads a style manifest from `style_dir`. `{:ok, map}`, or `{:error, reason}`
when the file is missing/unreadable/not a map (styles REQUIRE a manifest —
there is no tier-0 equivalent).
"""
@spec load(Path.t()) :: {:ok, map()} | {:error, String.t()}
def load(style_dir) do
path = Path.join(style_dir, @manifest_path)
if File.exists?(path) do
case Code.eval_file(path) do
{map, _} when is_map(map) -> {:ok, map}
{other, _} -> {:error, "#{@manifest_path} must evaluate to a map, got: #{inspect(other)}"}
end
else
{:error, "missing #{@manifest_path}"}
end
rescue
e -> {:error, "could not evaluate #{@manifest_path}: #{Exception.message(e)}"}
end
@doc """
Validates the four required fields (MOB_STYLES.md "Minimum viable
manifest"): `:name` atom, `:mob_version` requirement string,
`:style_spec_version` integer, `:theme` module atom.
"""
@spec validate(map()) :: {:ok, map()} | {:error, [String.t()]}
def validate(manifest) when is_map(manifest) do
errors =
[]
|> check(
manifest,
:name,
&(is_atom(&1) and not is_nil(&1)),
":name is required and must be an atom"
)
|> check_version(manifest)
|> check_spec_version(manifest)
|> check(
manifest,
:theme,
&(is_atom(&1) and not is_nil(&1)),
":theme is required and must be a theme module atom"
)
case errors do
[] -> {:ok, manifest}
errs -> {:error, Enum.reverse(errs)}
end
end
def validate(other),
do:
{:error,
["style manifest must be a map (the priv/mob_style.exs data), got: #{inspect(other)}"]}
@doc """
Activated style names from `mob.exs`'s `config :mob, :styles` (Application
env fallback, mirroring `MobDev.Plugin.activated_names/0`).
"""
@spec activated_names() :: [atom()]
def activated_names, do: read_mob_config(:styles, [])
@doc "The configured `:default_style` name (or nil)."
@spec default_style() :: atom() | nil
def default_style, do: read_mob_config(:default_style, nil)
@doc """
The activated styles as `{style_dir, manifest}` pairs. Names that don't
resolve to a dep or whose manifest fails to load are skipped (surfaced by
validation at build).
"""
@spec activated() :: [{Path.t(), map()}]
def activated do
deps = Mix.Project.deps_paths()
for name <- activated_names(), dir = deps[name], not is_nil(dir) do
case load(dir) do
{:ok, manifest} -> {dir, manifest}
{:error, _} -> nil
end
end
|> Enum.reject(&is_nil/1)
end
@doc """
The runtime-manifest entries for the activated styles:
`[%{name, theme}]`. Raises at build when an activated style's manifest is
invalid or the configured `:default_style` isn't among the activated styles
— a misconfigured style must fail the BUILD, not silently render baseline.
"""
@spec runtime_entries!() :: %{styles: [map()], default_style: atom() | nil}
def runtime_entries! do
entries =
for {dir, manifest} <- activated() do
case validate(manifest) do
{:ok, m} ->
%{name: m.name, theme: m.theme}
{:error, errs} ->
raise ArgumentError,
"invalid style manifest in #{dir}:\n " <> Enum.join(errs, "\n ")
end
end
default = default_style()
if default != nil and not Enum.any?(entries, &(&1.name == default)) do
raise ArgumentError,
":default_style #{inspect(default)} is not among the activated styles " <>
"#{inspect(Enum.map(entries, & &1.name))} — add it to config :mob, :styles"
end
%{styles: entries, default_style: default}
end
# ── helpers ────────────────────────────────────────────────────────────────
defp check(errors, manifest, key, pred, msg) do
if pred.(Map.get(manifest, key)), do: errors, else: [msg | errors]
end
defp check_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_version(errors, _),
do: [":mob_version is required and must be a version requirement string" | errors]
defp check_spec_version(errors, %{style_spec_version: v}) when is_integer(v) do
if v in @supported_spec_versions do
errors
else
[
"style_spec_version #{v} is not supported (this mob_dev knows #{inspect(@supported_spec_versions)})"
| errors
]
end
end
defp check_spec_version(errors, _),
do: [":style_spec_version is required and must be an integer" | errors]
defp read_mob_config(key, default) 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(key, default)
else
Application.get_env(:mob, key, default)
end
rescue
_ -> Application.get_env(:mob, key, default)
end
end