lib/taly.ex

defmodule Taly do
  @moduledoc """
  `Taly` provides a standard API to handle the based of map, keyword or list options

  ## Example
      import Taly

      def schema() do
        form do
          field "name", type: :string, required: true
          field "age", type: :integer
          field "worth", type: :float, default: 100.1, required: true
          field "sex", type: :atom, default: :man
          field "other", type: :any
          field "marry", type: :boolean
          field "like_date", type: :date, format: "{YYYY}/{M}/{D}"
          field "birth", type: :datetime
        end
      end

      data = %{
        "name" => "john",
        "age" => 1,
        "marry" => false,
        "worth" => "100000.1",
        "sex" => :women,
        "other" => {"hello"},
        # "like_date" => "2021/2/12",
        "like_date" => ~D[2021-02-12],
        "birth" => "2021-02-12 12:12:12",
      }
      Taly.Validate.validate(schema(), data)

      #=> {:ok, %{
                  "age" => 1,
                  "name" => "john",
                  "other" => {"hello"},
                  "sex" => :women,
                  "marry" => false,
                  "worth" => 100_000.1,
                  "birth" => ~N[2021-02-12 12:12:12],
                  "like_date" => ~D[2021-02-12]
                }}
  """
  alias Taly.Form

  defmacro __using__(_opts) do
    quote do
      import Taly.Field
      import Taly
      # @before_compile Taly.Field
    end
  end

  @doc """
  Pack `field` and `final` into a `Form.t()`.

  ## Example

      form do
        field :name, type: :string
        field do
          {:ok, value}
        end
      end
  """
  defmacro form(opts) do

    cells = case opts[:do] do
      {:__block__, _, cells_} ->
        cells_
      _ ->
        [opts[:do]]
    end
    quote do
      Enum.reduce(unquote(cells), %Form{}, fn cell, r ->
        cond do
          is_function(cell) ->
            %Form{r | final: cell}

          true ->
            Form.add_field(r, cell)
        end
      end)
    end
  end

  @doc """
  if you use `do-block`, the parameters is `key`, `value`, `kwargs`. the result must
  return `{:ok, value}` or `{:error, "Error"}`.
  Detailed usage reference the test file.

  ## Example
      import Taly

      form_1 =
        form do
          field :name, type: :string
        end

      form_2 =
          form do
            field :name, requried: true do
              {:ok, key}
            end
          end

      data = [name: "john"]
      Taly.Validate.validate(form_1, data)
      Taly.Validate.validate(form_2, data)
  """
  defmacro field(name, opt \\ [], contents \\ []) do
    Taly.Field.field(name, opt, contents)
  end

  @doc """
  this defmacro function use to create a option item.

  ## Example
      import Taly

      schema_opt = opt type: :integer
      list_data = ["123", 456]

      Taly.Validate.validate(schema_opt, list_data)
      #=> {:ok, [123, 456]}
  """
  defmacro opt(option, contents \\ []) do
    Taly.Field.opt(option, contents)
  end

  @doc """
  Use `validate/3` to validate data, allows finally to return the result what you want.
  The parameter is `result` and `kwargs`.

    * `result`: Result that use `validate/3` validate data with schema or `Form`.

    * `kwargs`: it's a map.the Key `:org` is input data. the `:path` is the key path.
    `:schema` is the schema option.

  ## Example
      import Taly

      def final_test() do
        schema_1 =
          form do
            field :name

            final do
              IO.inspect(kwargs, label: "final kwargs")
              result
              # the result is {:ok, [name: "john"]}, of course you
              # can {:error, "Error"}.
            end
          end

        form do
          field :obj, type: :dict, form: schema_1

          final do
            result
            # this result is the real output data, you can return what you
            # want. for example [1, 2, 3].
          end
        end
      end

      schema = Example.final_test()
      data = %{:obj => [name: "john"]}
      Taly.Validate.validate(schema, data)
      #=> {:ok, %{obj: [name: "john"]}}
  """
  defmacro final(contents) do
    do_something = contents[:do]
    quote do
      fn var!(result), var!(kwargs) ->
        unquote(do_something)
      end
    end
  end
end