lib/talos.ex

defmodule Talos do
  alias Talos.Types.MapType
  alias Talos.Types.MapType.Field
  alias Talos.Types.ListType
  alias Talos.Types.EnumType

  @moduledoc """
  Documentation for Talos.

  Talos is params type validation library, can be used with any Elixir application

  Sample usage:

  ```elixir
  defmodule MyAppWeb.UserController do
    # just import talos with functions helpers
    import Talos

    @interests_type enum(members: ["sports", "games", "food"]) # <- subtype
    # here we define expected struct
    @user_type map(fields: [
      field(key: "email", type: string(min_length: 5, max_length: 255, regexp: ~r/.*@.*/)),
      field(key: "age", type: integer(gteq: 18, allow_nil: true)),
      field(key: "interests", type: list(type: @interests_type), optional: true)
    ])

    def create(conn, params) do
      case Talos.valid?(@user_type, params) do
        true ->
          user = MyApp.create_user(params)
          conn
          |> put_flash(:info, "User created successfully.")
          |> redirect(to: Routes.user_path(conn, :show, user))
        false ->
          conn
          |> put_flash(:info, "Wrong params passed.")
          |> render("new.html")
      end
    end
  end
  ```
  """

  @spec valid?(struct | module, any) :: boolean
  def valid?(%{__struct__: type_module} = data_type, data) do
    type_module.valid?(data_type, data)
  end

  def valid?(module, value) do
    module.valid?(module, value)
  end

  @doc """
  Reduces `data` to what is defined in it's type.
  """
  @spec permit(struct | module, any) :: any
  def permit(%{__struct__: type_module} = data_type, data) do
    case type_module do
      MapType -> MapType.permit(data_type, data)
      ListType -> ListType.permit(data_type, data)
      EnumType -> EnumType.permit(data_type, data)
      _another -> data
    end
  end

  def permit(_module, value) do
    value
  end

  @spec errors(struct | module, any) :: any
  def errors(%{__struct__: type_module} = data_type, data) do
    type_module.errors(data_type, data)
  end

  def errors(module, value) do
    module.errors(module, value)
  end

  # Functions Helpers

  def map(args \\ []) do
    talos_build_struct(%MapType{}, args)
  end

  def field(args \\ []) do
    talos_build_struct(
      %Field{
        key: Keyword.get(args, :key),
        type: Keyword.get(args, :key)
      },
      args
    )
  end

  def enum(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.EnumType{}, args)
  end

  def const(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.ConstType{}, args)
  end

  def any(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.AnyType{}, args)
  end

  def boolean(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.BooleanType{}, args)
  end

  def fixed(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.FixedType{}, args)
  end

  def float(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.FloatType{}, args)
  end

  def integer(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.IntegerType{}, args)
  end

  def list(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.ListType{}, args)
  end

  def number(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.NumberType{}, args)
  end

  def string(args \\ []) when is_list(args) do
    talos_build_struct(%Talos.Types.StringType{}, args)
  end

  defp talos_build_struct(type, args) do
    if !Keyword.keyword?(args), do: raise(ArgumentError)

    type
    |> Map.keys()
    |> Enum.reduce(type, fn key, res ->
      value =
        case Keyword.get(args, key) do
          nil -> Map.get(type, key)
          v -> v
        end

      Map.put(res, key, value)
    end)
  end
end