lib/spike.ex

defmodule Spike do
  @moduledoc """
  This module contains top-level functions useful to validate, manipulate and inspect
  Spike forms.

  To learn about how to create Spike forms, see `Spike.Form`.
  """

  @doc """
  Given a Spike form, returns a map of errors, where fields are refs of all
  forms that build the form, and each field is a list of errors.

  Returns empty map in case the form is valid.

      iex> form = Test.SimpleForm.new(%{})
      iex> Spike.errors(form)[form.ref]
      %{accepts_conditions: [acceptance: "must be accepted"], first_name: [presence: "must be present"]}
      iex> form = Spike.update(form, form.ref, %{first_name: "Spike", last_name: "Spiegel", accepts_conditions: "1"})
      iex> Spike.errors(form)
      %{}
      iex> form = Test.ComplexForm.new(%{company: %{}, partners: []})
      iex> Spike.errors(form)
      %{form.company.ref => %{name: [presence: "must be present"]}, form.ref => %{accepts_conditions: [acceptance: "must be accepted"]}}

  """
  defdelegate errors(form), to: Spike.Form

  @doc """
  Returns true if given form has no errors.

      iex> form = Test.SimpleForm.new(%{})
      iex> Spike.valid?(form)
      false
      iex> form = Spike.update(form, form.ref, %{first_name: "Spike", last_name: "Spiegel", accepts_conditions: "1"})
      iex> Spike.valid?(form)
      true

  """
  def valid?(form) do
    errors(form) == %{}
  end

  @doc """
  Given a Spike form, returns a map of errors, where fields are Strings
  representing where in nested set of forms errors happened and values are errors.

  Returns empty map in case the form is valid.

      iex> form = Test.ComplexForm.new(%{company: %{}, partners: [%{}]})
      iex> Spike.human_readable_errors(form)
      %{"accepts_conditions" => ["must be accepted"], "company.name" => ["must be present"]}

  """
  defdelegate human_readable_errors(form), to: Spike.Form

  @doc """
  Given a Spike form, returns a map of fields that have been updated since the
  form form was created, or since the last time `Spike.make_pristine/1` was
  called.

      iex> form = Test.SimpleForm.new(%{first_name: "Spike", last_name: "Spiegel"})
      iex> Spike.dirty_fields(form)
      %{}
      iex> form = Spike.update(form, form.ref, %{first_name: "Jet", last_name: "Black"})
      iex> Spike.dirty_fields(form)
      %{form.ref => [:first_name, :last_name]}
      iex> form = Spike.make_pristine(form)
      iex>  Spike.dirty_fields(form)
      %{}

  """
  defdelegate dirty_fields(form), to: Spike.Form

  @doc """
  Given Spike form, marks all of it's fields as dirty, recursively.

      iex> form = Test.ComplexForm.new(%{company: %{}, partners: [%{}]})
      iex> form = Spike.make_dirty(form)
      iex> Spike.dirty_fields(form)
      %{
         form.ref => [:accepts_conditions, :company, :partners],
         form.company.ref => [:country, :name],
         hd(form.partners).ref => [:name]
       }

  """
  defdelegate make_dirty(form), to: Spike.Form

  @doc """
  Given a dirty Spike form, makes it pristine, recursively.

      iex> form = Test.ComplexForm.new(%{company: %{}, partners: [%{}]})
      iex> form = Spike.make_dirty(form)
      iex> form = Spike.make_pristine(form)
      iex> Spike.dirty_fields(form)
      %{}

  """
  defdelegate make_pristine(form), to: Spike.Form

  @doc """
  Updates a Spike form, by finding a nested form by it's `ref` field, with given params.
  Marks updated fields as dirty.

      iex> form = Test.ComplexForm.new(%{company: %{name: "ACME Corp."}, partners: [%{}]})
      iex> form.company.name
      "ACME Corp."
      iex> form = Spike.update(form, form.company.ref, %{name: "Amazon"})
      iex> form.company.name
      "Amazon"
      iex> Spike.dirty_fields(form)
      %{form.company.ref => [:name], form.ref => [:company]}

  """
  defdelegate update(form, ref, params), to: Spike.Form

  @doc """
  Updates a top level Spike form with given params. Marks updated fields as dirty.

      iex> form = Test.SimpleForm.new(%{})
      iex> form = Spike.update(form, %{first_name: "Spike", last_name: "Spiegel", accepts_conditions: "1"})
      iex> Spike.errors(form)
      %{}

  """

  def update(%{ref: ref} = form, params) do
    update(form, ref, params)
  end

  @doc """
  Updates `embeds_many` association on given Spike form, appending the new item at the end
  of the existing list.

      iex> form = Test.ComplexForm.new(%{company: %{name: "ACME Corp."}, partners: []})
      iex> form.partners
      []
      iex> form = Spike.append(form, form.ref, :partners, %{name: "Hubert"})
      iex> form.partners |> Enum.map(& &1.name)
      ["Hubert"]
      iex> form = Spike.append(form, form.ref, :partners, %{name: "Wojciech"})
      iex> form.partners |> Enum.map(& &1.name)
      ["Hubert", "Wojciech"]

  """
  defdelegate append(form, ref, field, params), to: Spike.Form

  @doc """
  Updates `embeds_many` association on the top-level Spike form, appending the new item at the end
  of the existing list.

      iex> form = Test.ComplexForm.new(%{company: %{name: "ACME Corp."}, partners: []})
      iex> form.partners
      []
      iex> form = Spike.append(form, :partners, %{name: "Hubert"})
      iex> form.partners |> Enum.map(& &1.name)
      ["Hubert"]
      iex> form = Spike.append(form, :partners, %{name: "Wojciech"})
      iex> form.partners |> Enum.map(& &1.name)
      ["Hubert", "Wojciech"]

  """

  def append(%{ref: ref} = form, field, params) do
    append(form, ref, field, params)
  end

  @doc """
  Deletes a form from a Spike form by it's `ref`. Useful to remove items from `embeds_many` or `embeds_one`.

      iex> form = Test.ComplexForm.new(%{company: %{name: "ACME Corp."}, partners: [%{name: "John"}]})
      iex> form = Spike.delete(form, form.company.ref)
      iex> form = Spike.delete(form, form.partners |> hd() |> Map.get(:ref))
      iex> form.company
      nil
      iex> form.partners
      []

  """
  defdelegate delete(form, ref), to: Spike.Form

  @doc """
  Allows you to update fields in a Spike form that were marked as private.

      iex> form = Test.PrivateForm.new(%{private_field: "foo"}, cast_private: true)
      iex> form = Spike.update(form, form.ref, %{private_field: "bar"})
      iex> form.private_field
      "foo"
      iex> form = Spike.set_private(form, form.ref, :private_field, "bar")
      iex> form.private_field
      "bar"

  """
  defdelegate set_private(form, ref, field, value), to: Spike.Form

  @doc """
  Returns `true` if given form, identified by `ref` has error on given `field`.

      iex> form = Test.SimpleForm.new(%{first_name: "Jet", accepts_conditions: "0"})
      iex> Spike.has_errors?(form, form.ref, :first_name)
      false
      iex> Spike.has_errors?(form, form.ref, :accepts_conditions)
      true

  """
  defdelegate has_errors?(form, ref, field), to: Spike.ErrorHelpers

  @doc """
  Returns `true` if given form, identified by `ref` has specified error message
  on given `field`.

      iex> form = Test.SimpleForm.new(%{first_name: "Jet", accepts_conditions: "0"})
      iex> Spike.has_errors?(form, form.ref, :accepts_conditions, "must be accepted")
      true
      iex> Spike.has_errors?(form, form.ref, :accepts_conditions, "is invalid")
      false

  """
  defdelegate has_errors?(form, ref, field, message), to: Spike.ErrorHelpers

  @doc """
  This function should only be used from within "by" validations, where context
  of parent forms is required. The first element of returned list will be
  the top-level form, followed by association name, and another form (if present)
  followed by assciation name etc.

  This is useful when in a child form you want to perform a validation that relies
  on value that lives in a parent form or a sibling in `embeds_many`.
  """
  def validation_context(form) do
    form
    |> Spike.Form.ValidationContext.get_validation_context()
    |> case do
      list when list != [] > 0 ->
        list |> Enum.reverse() |> tl() |> Enum.reverse()

      otherwise ->
        otherwise
    end
  end
end