defmodule DoIt.Argument do @moduledoc """ This module parse and validate arguments. """ import DoIt.Helper, only: [validate_list_type: 2] @argument_types [:boolean, :integer, :float, :string] @type t :: %__MODULE__{ name: atom, type: atom, description: String.t(), allowed_values: list } @enforce_keys [:name, :type, :description] defstruct [:name, :type, :description, :allowed_values] def validate_definition(%DoIt.Argument{} = argument) do argument |> validate_definition_name |> validate_definition_type |> validate_definition_description |> validate_definition_allowed_values end def validate_definition_name(%DoIt.Argument{name: nil}), do: raise(DoIt.ArgumentDefinitionError, message: "name is required for argument definition") def validate_definition_name(%DoIt.Argument{name: name} = argument) when is_atom(name), do: argument def validate_definition_name(%DoIt.Argument{name: _}), do: raise(DoIt.ArgumentDefinitionError, message: "name must be an atom") def validate_definition_type(%DoIt.Argument{type: nil}), do: raise(DoIt.ArgumentDefinitionError, message: "type is required for argument definition") def validate_definition_type(%DoIt.Argument{type: type} = argument) when type in @argument_types, do: argument def validate_definition_type(%DoIt.Argument{type: type}), do: raise(DoIt.ArgumentDefinitionError, message: "unrecognized argument type '#{type}', allowed types are #{@argument_types |> Enum.map_join(", ", &Atom.to_string/1)}" ) def validate_definition_description(%DoIt.Argument{description: nil}), do: raise(DoIt.ArgumentDefinitionError, message: "description is required for argument definition" ) def validate_definition_description(%DoIt.Argument{description: description} = argument) when is_binary(description), do: argument def validate_definition_description(%DoIt.Argument{description: _}), do: raise(DoIt.ArgumentDefinitionError, message: "description must be a string") def validate_definition_allowed_values(%DoIt.Argument{allowed_values: nil} = argument), do: argument def validate_definition_allowed_values(%DoIt.Argument{type: type, allowed_values: _}) when type == :boolean, do: raise(DoIt.ArgumentDefinitionError, message: "allowed_values cannot be used with type boolean" ) def validate_definition_allowed_values( %DoIt.Argument{type: type, allowed_values: allowed_values} = argument ) when is_list(allowed_values) do case validate_list_type(allowed_values, type) do true -> argument _ -> raise DoIt.ArgumentDefinitionError, message: "all values in allowed_values must be of type #{Atom.to_string(type)}" end end def validate_definition_allowed_values(%DoIt.Argument{allowed_values: _}), do: raise(DoIt.ArgumentDefinitionError, message: "allowed_values must be a list") def parse_input(arguments, parsed) do cond do Enum.count(arguments) != Enum.count(parsed) -> {:error, "wrong number of arguments (given #{Enum.count(parsed)} expected #{Enum.count(arguments)})"} Enum.empty?(arguments) -> {:ok, []} true -> argument_keys = arguments |> Enum.map(fn %{name: name} -> name end) |> Enum.reverse() { :ok, Enum.zip(argument_keys, parsed) } end end def validate_input([], _), do: {:ok, []} def validate_input(arguments, parsed) do case parsed |> Enum.map(fn {key, value} -> argument = Enum.find(arguments, fn %DoIt.Argument{name: name} -> name == key end) {argument, value} |> validate_input_value() |> validate_input_allowed_values() end) |> List.flatten() |> Enum.map(fn {%DoIt.Argument{name: name}, value} -> {name, value} end) |> Enum.split_with(fn {_, {:error, _}} -> false _ -> true end) do {valid_arguments, []} -> {:ok, valid_arguments} {_, invalid_arguments} -> { :error, Enum.map(invalid_arguments, fn {_, {:error, message}} -> message end) } end end def validate_input_value({_, {:error, _}} = error), do: error def validate_input_value({%DoIt.Argument{} = argument, values}) when is_list(values) do validate_input_value({argument, values}, []) end def validate_input_value({%DoIt.Argument{type: :integer} = argument, value}) when is_integer(value), do: {argument, value} def validate_input_value({%DoIt.Argument{name: name, type: :integer} = argument, value}) do {argument, String.to_integer(value)} rescue ArgumentError -> {argument, {:error, "invalid integer value '#{value}' for argument #{Atom.to_string(name)}"}} end def validate_input_value({%DoIt.Argument{type: :float} = argument, value}) when is_float(value), do: {argument, value} def validate_input_value({%DoIt.Argument{name: name, type: :float} = argument, value}) do {argument, String.to_float(value)} rescue ArgumentError -> {argument, {:error, "invalid float value '#{value}' for argument #{Atom.to_string(name)}"}} end def validate_input_value({%DoIt.Argument{} = argument, value}) do {argument, "#{value}"} end def validate_input_value({%DoIt.Argument{} = argument, [value | values]}, acc) do case validate_input_value({argument, value}) do {%DoIt.Argument{}, {:error, _}} = error -> error {%DoIt.Argument{}, val} -> validate_input_value({argument, values}, acc ++ [val]) end end def validate_input_value({%DoIt.Argument{} = argument, []}, acc) do {argument, acc} end def validate_input_allowed_values({_, {:error, _}} = error), do: error def validate_input_allowed_values({%DoIt.Argument{allowed_values: nil} = argument, value}) do {argument, value} end def validate_input_allowed_values({%DoIt.Argument{} = argument, values}) when is_list(values) do validate_input_allowed_values({argument, values}, []) end def validate_input_allowed_values( {%DoIt.Argument{name: name, allowed_values: allowed_values} = argument, value} ) do case Enum.find(allowed_values, fn allowed -> value == allowed end) do nil -> {argument, {:error, "value '#{value}' isn't allowed for argument #{Atom.to_string(name)}"}} _ -> {argument, value} end end def validate_input_allowed_values({%DoIt.Argument{} = argument, [value | values]}, acc) do case validate_input_allowed_values({argument, value}) do {%DoIt.Argument{}, {:error, _}} = error -> error {%DoIt.Argument{}, val} -> validate_input_allowed_values({argument, values}, acc ++ [val]) end end def validate_input_allowed_values({%DoIt.Argument{} = argument, []}, acc) do {argument, acc} end end