Packages

An `Elixir` library to write command line apps in a cleaner and elegant way!

Current section

Files

Jump to
nexus_cli lib nexus cli validation.ex
Raw

lib/nexus/cli/validation.ex

defmodule Nexus.CLI.Validation do
@moduledoc """
Provides validation functions for commands, flags, and arguments within the Nexus.CLI DSL.
"""
alias Nexus.CLI.Argument
alias Nexus.CLI.Command
alias Nexus.CLI.Flag
@supported_types [:boolean, :string, :integer, :float]
defmodule ValidationError do
defexception message: "Validation error"
@spec exception(String.t()) :: %__MODULE__{message: String.t()}
def exception(msg) do
%__MODULE__{message: msg}
end
end
@doc """
Validates a command, including its subcommands, flags, and arguments.
"""
@spec validate_command(Command.t()) :: Command.t()
def validate_command(%Command{} = command) do
command
|> validate_command_name()
|> validate_subcommands()
|> validate_flags()
|> validate_arguments()
end
defp validate_command_name(%Command{name: nil}) do
raise ValidationError, "Command name is required and must be an atom."
end
defp validate_command_name(%Command{name: name} = command) when is_atom(name) do
command
end
defp validate_command_name(%Command{name: name}) do
raise ValidationError, "Command name must be an atom, got: #{inspect(name)}."
end
defp validate_subcommands(%Command{subcommands: subcommands} = command) do
# Validate each subcommand
subcommands = Enum.map(subcommands, &validate_command/1)
# Check for duplicate subcommand names
subcommand_names = Enum.map(subcommands, & &1.name)
duplicates = find_duplicates(subcommand_names)
if duplicates != [] do
raise ValidationError,
"Duplicate subcommand names in command '#{command.name}': #{Enum.join(duplicates, ", ")}."
end
%{command | subcommands: subcommands}
end
defp validate_flags(%Command{flags: flags} = command) do
flags = Enum.map(flags, &validate_flag/1)
flag_names = Enum.map(flags, & &1.name)
duplicates = find_duplicates(flag_names)
if duplicates != [] do
raise ValidationError,
"Duplicate flag names in command '#{command.name}': #{Enum.join(duplicates, ", ")}."
end
# Check for duplicate short flag names
short_names = for flag <- flags, flag.short, do: flag.short
duplicates_short = find_duplicates(short_names)
if duplicates_short != [] do
raise ValidationError,
"Duplicate short flag aliases in command '#{command.name}': #{Enum.join(duplicates_short, ", ")}."
end
%{command | flags: flags}
end
defp validate_arguments(%Command{args: args} = command) do
args = Enum.map(args, &validate_argument/1)
arg_names = Enum.map(args, & &1.name)
duplicates = find_duplicates(arg_names)
if duplicates != [] do
raise ValidationError,
"Duplicate argument names in command '#{command.name}': #{Enum.join(duplicates, ", ")}."
end
%{command | args: args}
end
@doc """
Validates a flag.
"""
@spec validate_flag(Flag.t()) :: Flag.t()
def validate_flag(%Flag{} = flag) do
flag
|> validate_flag_name()
|> validate_flag_type()
|> validate_flag_default()
end
defp validate_flag_name(%Flag{name: nil}) do
raise ValidationError, "Flag name is required and must be an atom."
end
defp validate_flag_name(%Flag{name: name} = flag) when is_atom(name) do
flag
end
defp validate_flag_name(%Flag{name: name}) do
raise ValidationError, "Flag name must be an atom, got: #{inspect(name)}."
end
defp validate_flag_type(%Flag{type: type} = flag) do
validate_type(type)
flag
end
defp validate_flag_default(%Flag{default: default, type: type, name: name} = flag) do
if default != nil do
unless valid_default?(default, type) do
raise ValidationError,
"Default value for flag '#{name}' must be of type #{inspect(type)}, got: #{inspect(default)}."
end
end
flag
end
@doc """
Validates an argument.
"""
@spec validate_argument(Argument.t()) :: Argument.t()
def validate_argument(%Argument{} = arg) do
arg
|> validate_argument_name()
|> validate_argument_type()
|> validate_argument_default()
end
defp validate_argument_name(%Argument{name: nil}) do
raise ValidationError, "Argument name is required and must be an atom."
end
defp validate_argument_name(%Argument{name: name} = arg) when is_atom(name) do
arg
end
defp validate_argument_name(%Argument{name: name}) do
raise ValidationError, "Argument name must be an atom, got: #{inspect(name)}."
end
defp validate_argument_type(%Argument{type: type} = arg) do
validate_type(type)
arg
end
defp validate_argument_default(%Argument{default: default, type: type, name: name} = arg) do
if default != nil do
unless valid_default?(default, type) do
raise ValidationError,
"Default value for argument '#{name}' must be of type #{inspect(type)}, got: #{inspect(default)}."
end
end
arg
end
defp validate_type({:enum, values}) when is_list(values) do
if Enum.all?(values, &is_atom/1) or Enum.all?(values, &is_binary/1) do
:ok
else
raise ValidationError, "Enum values must be all atoms or all strings."
end
end
defp validate_type({:list, subtype}) do
validate_type(subtype)
end
defp validate_type(type) when type in @supported_types, do: :ok
defp validate_type(type) do
raise ValidationError, "Unsupported type: #{inspect(type)}."
end
defp valid_default?(default, {:enum, values}) do
Enum.member?(values, default)
end
defp valid_default?(default, {:list, subtype}) when is_list(default) do
Enum.all?(default, fn item -> valid_default?(item, subtype) end)
end
defp valid_default?(default, type) do
case type do
:boolean -> is_boolean(default)
:string -> is_binary(default)
:integer -> is_integer(default)
:float -> is_float(default)
_ -> false
end
end
defp find_duplicates(list) do
list
|> Enum.frequencies()
|> Enum.filter(fn {_item, count} -> count > 1 end)
|> Enum.map(fn {item, _count} -> item end)
end
end