defmodule Settings.Approvals do alias Settings.Approvals.RegexMatcher @type settings :: Settings.t() @type scope :: :project | :global @type kind :: binary @type prefix :: binary @type subject :: binary @spec get_approvals(settings, scope) :: map def get_approvals(settings, :global) do case Settings.get(settings, "approvals", %{}) do nil -> %{} approvals when is_map(approvals) -> approvals _ -> %{} end end def get_approvals(settings, :project) do with {:ok, project} <- Settings.get_project(settings) do case Map.get(project, "approvals", %{}) do nil -> %{} approvals when is_map(approvals) -> approvals _ -> %{} end else _ -> %{} end end @spec get_approvals(settings, scope, kind) :: list(prefix) def get_approvals(settings, :global, kind) do settings |> get_approvals(:global) |> Map.get(kind, []) |> validate_approval_list(settings, :global, kind) end def get_approvals(settings, :project, kind) do settings |> get_approvals(:project) |> Map.get(kind, []) |> validate_approval_list(settings, :project, kind) end @spec approve(settings, scope, kind, prefix) :: settings def approve(settings, :global, kind, prefix) do Settings.update(settings, "approvals", fn approvals when is_map(approvals) -> existing = Map.get(approvals, kind, []) updated = existing |> Enum.concat([prefix]) |> Enum.uniq() |> Enum.sort() Map.put(approvals, kind, updated) other -> UI.warn( "Settings.Approvals", """ FOUND CORRUPTED GLOBAL APPROVALS: #{inspect(other, limit: :infinity, pretty: true)} """ ) %{ kind => [prefix] } end) end def approve(settings, :project, kind, prefix) do case Settings.get_project(settings) do {:ok, %{"name" => name}} -> # Use Settings.update to modify the nested project data safely Settings.update(settings, "projects", fn projects when is_map(projects) -> project = Map.get(projects, name, %{}) approvals = Map.get(project, "approvals", %{}) existing = Map.get(approvals, kind, []) updated_prefixes = existing |> Enum.concat([prefix]) |> Enum.uniq() |> Enum.sort() updated_approvals = Map.put(approvals, kind, updated_prefixes) updated_project = Map.put(project, "approvals", updated_approvals) Map.put(projects, name, updated_project) _ -> %{ name => %{"approvals" => %{kind => [prefix]}} } end) _ -> # Check if a project is selected but doesn't have data - create it case Settings.get_selected_project() do {:ok, project_name} -> Settings.update(settings, "projects", fn projects when is_map(projects) -> Map.put(projects, project_name, %{ "name" => project_name, "approvals" => %{kind => [prefix]} }) _ -> %{project_name => %{"name" => project_name, "approvals" => %{kind => [prefix]}}} end) _ -> settings end end end @spec approved?(settings, kind, subject) :: boolean def approved?(settings, kind, subject) do # Check both global and project scopes for any matching regex pattern [:global, :project] |> Enum.any?(&approved?(settings, &1, kind, subject)) end def approved?(settings, :global, kind, subject) do get_approvals(settings, :global, kind) |> Enum.any?(&RegexMatcher.matches?(&1, subject)) end def approved?(settings, :project, kind, subject) do get_approvals(settings, :project, kind) |> Enum.any?(&RegexMatcher.matches?(&1, subject)) end @spec prefix_approved?(settings, kind, subject) :: boolean def prefix_approved?(settings, kind, subject) do # Check both global and project scopes for any matching prefix [:global, :project] |> Enum.any?(&prefix_approved?(settings, &1, kind, subject)) end def prefix_approved?(settings, :global, kind, subject) do get_approvals(settings, :global, kind) |> Enum.any?(&String.starts_with?(subject, &1)) end def prefix_approved?(settings, :project, kind, subject) do get_approvals(settings, :project, kind) |> Enum.any?(&String.starts_with?(subject, &1)) end # ============================================================================ # Data validation and corruption handling # ============================================================================ # Validates and repairs corrupted approval lists. # When approval data is corrupted (e.g., not a list, contains non-strings, or is null), # this function warns the user and repairs the data by writing the corrected format # back to the settings file. @spec validate_approval_list(any, settings, scope, kind) :: list(prefix) defp validate_approval_list(data, settings, scope, kind) when is_list(data) do # Filter out non-string entries and warn if any were found valid_entries = Enum.filter(data, &is_binary/1) if length(valid_entries) != length(data) do invalid_count = length(data) - length(valid_entries) UI.warn( "Settings.Approvals", "Found #{invalid_count} invalid entries in #{scope} #{kind} approvals. Cleaning up..." ) # Fix the corrupted data by re-approving only the valid entries repair_approval_list(settings, scope, kind, valid_entries) end valid_entries end defp validate_approval_list(data, settings, scope, kind) when is_nil(data) do UI.warn( "Settings.Approvals", "Found null #{scope} #{kind} approvals. Repairing..." ) repair_approval_list(settings, scope, kind, []) [] end defp validate_approval_list(data, settings, scope, kind) do UI.warn( "Settings.Approvals", "Found corrupted #{scope} #{kind} approvals (expected list, got #{inspect(data)}). Repairing..." ) repair_approval_list(settings, scope, kind, []) [] end # Repairs corrupted approval data by writing the correct format to settings. @spec repair_approval_list(settings, scope, kind, list(prefix)) :: :ok defp repair_approval_list(settings, :global, kind, valid_entries) do Settings.update(settings, "approvals", fn approvals -> approvals = if is_map(approvals), do: approvals, else: %{} # Get the current list for this kind (may have been updated by another process) current = Map.get(approvals, kind, []) # Filter out invalid entries from current list and merge with our valid entries # Handle case where current is corrupted (not a list) all_valid = if is_list(current) do current |> Enum.filter(&is_binary/1) else [] end |> Enum.concat(valid_entries) |> Enum.uniq() |> Enum.sort() Map.put(approvals, kind, all_valid) end) :ok end defp repair_approval_list(settings, :project, kind, valid_entries) do with {:ok, %{"name" => name}} <- Settings.get_project(settings) do Settings.update(settings, "projects", fn projects -> projects = if is_map(projects), do: projects, else: %{} project = Map.get(projects, name, %{}) approvals = Map.get(project, "approvals", %{}) # Get the current list for this kind (may have been updated by another process) current = Map.get(approvals, kind, []) # Filter out invalid entries from current list and merge with our valid entries # Handle case where current is corrupted (not a list) all_valid = if is_list(current) do current |> Enum.filter(&is_binary/1) else [] end |> Enum.concat(valid_entries) |> Enum.uniq() |> Enum.sort() updated_approvals = Map.put(approvals, kind, all_valid) updated_project = Map.put(project, "approvals", updated_approvals) Map.put(projects, name, updated_project) end) end :ok end end