Packages
fnord
0.9.33
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
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.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/settings/validation.ex
defmodule Settings.Validation do
@moduledoc """
Reads and normalizes project-scoped validation settings.
Validation rules live under a project's `validation` key in `settings.json`.
Each rule maps one or more project-relative path globs to a command string
that should be executed when matching files change.
"""
@type rule :: %{
path_globs: [String.t()],
command: String.t()
}
@doc """
Returns the normalized validation rules for the currently selected project.
"""
@spec list() :: [rule()]
def list() do
case Settings.get_selected_project() do
{:ok, project_name} -> list(project_name)
{:error, :project_not_set} -> []
end
end
@doc """
Returns the normalized validation rules for the named project.
"""
@spec list(String.t()) :: [rule()]
def list(project_name) when is_binary(project_name) do
Settings.new()
|> Settings.get_project_data(project_name)
|> get_rules()
|> normalize_rules()
end
@doc """
Returns true when the current project has at least one valid validation rule.
"""
@spec configured?() :: boolean
def configured?() do
list() != []
end
@doc """
Returns true when the named project has at least one valid validation rule.
"""
@spec configured?(String.t()) :: boolean
def configured?(project_name) when is_binary(project_name) do
list(project_name) != []
end
@doc """
Adds a normalized validation rule to the currently selected project so the
active project's validation command set includes another file-matching rule.
"""
@spec add_rule(String.t(), String.t() | [String.t()]) ::
{:ok, [rule()]} | {:error, :invalid_rule | :project_not_set}
def add_rule(command, path_globs) do
case Settings.get_selected_project() do
{:ok, project_name} -> add_rule(project_name, command, path_globs)
{:error, :project_not_set} -> {:error, :project_not_set}
end
end
@doc """
Adds a normalized validation rule to the named project as part of managing
the project's persisted validation rule set.
"""
@spec add_rule(String.t(), String.t(), String.t() | [String.t()]) ::
{:ok, [rule()]} | {:error, :invalid_rule | :project_not_found}
def add_rule(project_name, command, path_globs)
when is_binary(project_name) and is_binary(command) do
case normalize_candidate_rule(command, path_globs) do
nil ->
{:error, :invalid_rule}
rule ->
update_rules(project_name, fn rules ->
{:ok, rules ++ [rule]}
end)
end
end
@doc """
Removes the validation rule at the given 1-based index from the currently
selected project so the active project's validation rule set no longer
includes that entry.
"""
@spec remove_rule(integer()) ::
{:ok, [rule()]} | {:error, :invalid_index | :project_not_set | :project_not_found}
def remove_rule(index) do
case Settings.get_selected_project() do
{:ok, project_name} -> remove_rule(project_name, index)
{:error, :project_not_set} -> {:error, :project_not_set}
end
end
@doc """
Removes the validation rule at the given 1-based index from the named
project as part of maintaining its persisted validation rules.
"""
@spec remove_rule(String.t(), integer()) ::
{:ok, [rule()]} | {:error, :invalid_index | :project_not_found}
def remove_rule(project_name, index) when is_binary(project_name) and is_integer(index) do
case valid_index?(index) do
false ->
{:error, :invalid_index}
true ->
update_rules(project_name, fn rules ->
remove_rule_at(rules, index)
end)
end
end
@doc """
Clears all validation rules for the currently selected project, resetting the
active project's validation rule set.
"""
@spec clear() :: :ok | {:error, :project_not_found}
def clear() do
case Settings.get_selected_project() do
{:ok, project_name} -> clear(project_name)
{:error, :project_not_set} -> :ok
end
end
@doc """
Clears all validation rules for the named project by replacing its persisted
validation rules with an empty list.
"""
@spec clear(String.t()) :: :ok | {:error, :project_not_found}
def clear(project_name) when is_binary(project_name) do
case update_rules(project_name, fn _rules ->
{:ok, []}
end) do
{:ok, _rules} -> :ok
{:error, :project_not_found} -> {:error, :project_not_found}
end
end
@spec get_rules(map() | nil) :: list()
defp get_rules(nil), do: []
defp get_rules(project_data) when is_map(project_data) do
Map.get(project_data, "validation", [])
end
@spec normalize_rules(list()) :: [rule()]
defp normalize_rules(rules) when is_list(rules) do
rules
|> Enum.map(&normalize_rule/1)
|> Enum.reject(&is_nil/1)
end
@spec normalize_rule(term()) :: rule() | nil
defp normalize_rule(%{"path_globs" => path_globs, "command" => command}) do
with {:ok, normalized_globs} <- normalize_globs(path_globs),
{:ok, normalized_command} <- normalize_command(command) do
%{
path_globs: normalized_globs,
command: normalized_command
}
else
_ -> nil
end
end
defp normalize_rule(_other), do: nil
# Builds a candidate rule from user input. Rejects the entire rule if any
# individual glob is blank after trimming, since that indicates malformed
# input rather than a glob to silently drop.
@spec normalize_candidate_rule(term(), term()) :: rule() | nil
defp normalize_candidate_rule(command, path_globs) when is_list(path_globs) do
has_blank =
Enum.any?(path_globs, fn
g when is_binary(g) -> String.trim(g) == ""
_ -> false
end)
if has_blank do
nil
else
normalize_rule(%{"command" => command, "path_globs" => path_globs})
end
end
defp normalize_candidate_rule(command, path_globs) do
normalize_rule(%{"command" => command, "path_globs" => path_globs})
end
@spec dump_rule(rule()) :: map()
defp dump_rule(%{path_globs: path_globs, command: command}) do
%{
"path_globs" => path_globs,
"command" => command
}
end
@spec put_rules(map(), [rule()]) :: map()
defp put_rules(project_data, rules) when is_map(project_data) and is_list(rules) do
Map.put(project_data, "validation", Enum.map(rules, &dump_rule/1))
end
@spec update_rules(String.t(), ([rule()] -> {:ok, [rule()]} | {:error, term()})) ::
{:ok, [rule()]} | {:error, term()}
defp update_rules(project_name, fun) when is_binary(project_name) and is_function(fun, 1) do
settings = Settings.new()
case Settings.get_project_data(settings, project_name) do
nil ->
{:error, :project_not_found}
_project_data ->
current_rules = list(project_name)
case fun.(current_rules) do
{:ok, updated_rules} ->
settings =
Settings.update(
settings,
"projects",
fn projects ->
current_projects = normalize_projects(projects)
project_data = Map.fetch!(current_projects, project_name)
Map.put(current_projects, project_name, put_rules(project_data, updated_rules))
end,
%{}
)
updated_rules =
settings
|> Settings.get_project_data(project_name)
|> get_rules()
|> normalize_rules()
{:ok, updated_rules}
{:error, reason} ->
{:error, reason}
end
end
end
@spec normalize_projects(term()) :: map()
defp normalize_projects(projects) when is_map(projects), do: projects
defp normalize_projects(_other), do: %{}
@spec remove_rule_at([rule()], integer()) :: {:ok, [rule()]} | {:error, :invalid_index}
defp remove_rule_at(rules, index) do
zero_based_index = index - 1
case zero_based_index < length(rules) do
true -> {:ok, List.delete_at(rules, zero_based_index)}
false -> {:error, :invalid_index}
end
end
@spec valid_index?(term()) :: boolean()
defp valid_index?(index) when is_integer(index) and index > 0, do: true
defp valid_index?(_index), do: false
@spec normalize_globs(term()) :: {:ok, [String.t()]} | :error
defp normalize_globs(globs) when is_list(globs) do
globs
|> Enum.filter(&is_binary/1)
|> Enum.flat_map(&split_glob/1)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> case do
[] -> :error
normalized -> {:ok, normalized}
end
end
defp normalize_globs(glob) when is_binary(glob) do
normalize_globs([glob])
end
defp normalize_globs(_other), do: :error
# Splits a glob string that may contain multiple space-separated patterns
# into individual entries. Uses shell-style tokenization so quoted segments
# with spaces are preserved as a single pattern.
@spec split_glob(String.t()) :: [String.t()]
defp split_glob(glob) do
OptionParser.split(glob)
end
@spec normalize_command(term()) :: {:ok, String.t()} | :error
defp normalize_command(command) when is_binary(command) do
case String.trim(command) do
"" -> :error
normalized -> {:ok, normalized}
end
end
defp normalize_command(_other), do: :error
end