Packages

Reference Elixir validator for WPL (Wellness Plan Language) — JSON Schema + semantic invariants

Current section

Files

Jump to
wpl_validator lib wpl validator rules duplicate_id.ex
Raw

lib/wpl/validator/rules/duplicate_id.ex

defmodule WPL.Validator.Rules.DuplicateId do
@moduledoc false
use WPL.Validator.Rule
alias WPL.Validator.{Error, WalkContext}
# -----------------------------------------------------------------------
# Scope key conventions (stored in WalkContext.scope):
# :cur_phase / :cur_week / :cur_day — IDs set by the walker
# {:dup_seen, scope_key} — map of id -> first_occurrence_path
# -----------------------------------------------------------------------
@impl true
def enter_plan(ctx, _plan) do
# Reset all duplicate-id tracking at top of each plan walk.
new_scope =
ctx.scope
|> Enum.reject(fn {k, _} -> match?({:dup_seen, _}, k) end)
|> Map.new()
%{ctx | scope: new_scope}
end
@impl true
def enter_phase(ctx, phase, path) do
check(ctx, {:dup_seen, "plan"}, "plan", Map.get(phase, "id"), path)
end
@impl true
def enter_week(ctx, week, path) do
phase_id = WalkContext.get_scope(ctx, :cur_phase, "")
check(ctx, {:dup_seen, "phase:#{phase_id}"}, "phase:#{phase_id}", Map.get(week, "id"), path)
end
@impl true
def enter_day(ctx, day, path) do
week_id = WalkContext.get_scope(ctx, :cur_week, "")
check(ctx, {:dup_seen, "week:#{week_id}"}, "week:#{week_id}", Map.get(day, "id"), path)
end
@impl true
def enter_block(ctx, block, path) do
day_id = WalkContext.get_scope(ctx, :cur_day, "")
check(
ctx,
{:dup_seen, "day-block:#{day_id}"},
"day:#{day_id}",
Map.get(block, "id"),
path
)
end
@impl true
def enter_activity(ctx, activity, path) do
day_id = WalkContext.get_scope(ctx, :cur_day, "")
check(
ctx,
{:dup_seen, "day-act:#{day_id}"},
"day:#{day_id}",
Map.get(activity, "id"),
path
)
end
# -----------------------------------------------------------------------
# Private helpers
# -----------------------------------------------------------------------
defp check(ctx, scope_key, scope_label, id, path) when is_binary(id) and id != "" do
seen = WalkContext.get_scope(ctx, scope_key, %{})
case Map.get(seen, id) do
nil ->
WalkContext.put_scope(ctx, scope_key, Map.put(seen, id, path))
first_path ->
WalkContext.emit(ctx, %Error{
path: path,
code: :duplicate_id,
message: "Duplicate id '#{id}' within scope #{scope_label}",
severity: :error,
meta: %{
duplicate_id: id,
scope: scope_label,
first_occurrence: first_path
}
})
end
end
defp check(ctx, _scope_key, _scope_label, _id, _path), do: ctx
end