lib/draft.ex

defmodule Draft do
    @moduledoc """
    Data Validation for Elixir.
    """
    alias Draft.Extract

    def valid?(data) when is_struct(data) do
        valid?(data, Extract.rules(data))
    end

    def valid?(data, settings) do
        errors(data, settings) == []
    end

    def validate(data) when is_struct(data) do
        validate(data, Extract.rules(data))
    end

    def validate(data, settings) do
        case errors(data, settings) do
            errors when errors != [] -> {:error, errors}
            _ -> {:ok, data}
        end
    end

    def errors(data) when is_struct(data) do
        errors(data, Extract.rules(data))
    end

    def errors(data, settings) do
        results(data, settings)
    end

    def results(data) when is_struct(data) do
        results(data, Extract.rules(data))
    end

    def results(data, settings) 
    when not(is_list(data)) and not(is_map(data)) do
        results([value: data], value: settings)
    end

    def results(data, settings) do
        settings
        |> Enum.map(fn {key, rules} ->
            Enum.reduce_while(rules, [], fn rule, _acc -> 
                {name, opts} = 
                    cond do
                        is_tuple(rule) ->
                            rule

                        is_atom(rule) ->
                            {rule, []}

                        true ->
                            message = "rule must be atom or {name, opts} not #{rule}"
                            raise ArgumentError, message: message
                    end

                validator = Draft.Registry.validator(name)

                exists = Map.has_key?(data, key)
                            
                cond do
                    exists and is_atom(validator) and not(is_nil(validator)) ->
                        value = Map.get(data, key)
                        case validator.validate(value, data, opts) do
                            {:ok, _} ->
                                {:cont, []}

                            {:error, error} ->
                                {:halt, [{key, error}]}
                        end

                    true ->
                        {:cont, []}
                end
            end)
        end)
        |> List.flatten()
    end

    def validate_required(attr, required)
    def validate_required(attr, required) when is_list(attr) do
        attr
        |> Enum.into(%{})
        |> validate_required(required)
    end
    def validate_required(attr, required) when is_map(required) do
        validate_required(attr, Map.to_list(required))
    end
    def validate_required(attr, required) when is_map(attr) and is_list(required) do
        notfound =
            Enum.find(required, fn key -> 
                attr
                |> Map.has_key?(key)
                |> Kernel.not()
            end) 
        if is_nil(notfound) do
            {:ok, attr}
        else
            {:error, notfound}
        end
    end

end