defmodule Planck.Headless.Config.JsonBinding do
@moduledoc false
# Skogsra binding that reads ~/.planck/config.json and .planck/config.json.
# Later files win (project-local overrides user-global).
# The merged map is cached in persistent_term after the first resolution.
#
# Set `config :planck_headless, :skip_json_config, true` in test envs to
# skip this binding entirely: `init/1` returns `:error` so Skogsra bypasses
# the binding without emitting `:not_found` warnings for every key.
#
# Call invalidate/0 before ResourceStore.reload/0 to pick up file changes.
use Skogsra.Binding
alias Planck.Headless.Config
require Logger
@cache_key {__MODULE__, :config}
@impl true
def init(_env) do
if Application.get_env(:planck_headless, :skip_json_config, false),
do: :error,
else: {:ok, cached_config()}
end
@impl true
def get_env(env, config) do
key = env.keys |> List.last() |> to_string()
case Map.fetch(config, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, :not_found}
end
end
@doc "Clear the JSON config cache so the next resolution reloads from disk."
@spec invalidate() :: :ok
def invalidate do
:persistent_term.erase(@cache_key)
:ok
end
# ---------------------------------------------------------------------------
# Private
# ---------------------------------------------------------------------------
@spec cached_config() :: map()
defp cached_config do
case :persistent_term.get(@cache_key, :miss) do
:miss ->
config = load_files()
:persistent_term.put(@cache_key, config)
config
config ->
config
end
end
@spec load_files() :: map()
defp load_files do
Enum.reduce(Config.config_files!(), %{}, fn path, acc ->
case load_file(path) do
{:ok, map} -> Map.merge(acc, map)
:skip -> acc
end
end)
end
@spec load_file(Path.t()) :: {:ok, map()} | :skip
defp load_file(path) do
with {:ok, content} <- read_file(Path.expand(path)) do
decode_json(content, path)
end
end
@spec read_file(Path.t()) :: {:ok, String.t()} | :skip
defp read_file(path) do
case File.read(path) do
{:ok, content} ->
{:ok, content}
{:error, :enoent} ->
:skip
{:error, reason} ->
Logger.warning(
"[Planck.Headless.Config] cannot read #{path}: #{:file.format_error(reason)}"
)
:skip
end
end
@spec decode_json(String.t(), Path.t()) :: {:ok, map()} | :skip
defp decode_json(content, path) do
case Jason.decode(content) do
{:ok, map} when is_map(map) ->
{:ok, map}
{:ok, _other} ->
Logger.warning("[Planck.Headless.Config] #{path} must be a JSON object, skipping")
:skip
{:error, err} ->
Logger.warning(
"[Planck.Headless.Config] invalid JSON in #{path}: #{Exception.message(err)}"
)
:skip
end
end
end