lib/ecto_autoslug_field/slug_generator.ex

defmodule EctoAutoslugField.SlugGenerator do
  @moduledoc """
  This module works with slugs itself. It is just a wrapper around 'Slugify'.

  It is suited for inner use.
  """

  alias Ecto.Changeset

  import Ecto.Changeset,
    only: [
      put_change: 3,
      get_change: 3
    ]

  @doc """
  This is a public wrapper around `do_build_slug/1` functions.

  Default slug builder.
  """
  @spec build_slug(Keyword.t(), Changeset.t() | nil) :: String.t() | nil
  def build_slug(sources, _changeset), do: do_build_slug(sources)

  @doc """
  This function conditionally generates slug.

  This function prepares sources and then calls `do_generate_slug/3`.
  """
  @spec maybe_generate_slug(Changeset.t(), atom() | list(), Keyword.t()) ::
          Changeset.t()
  def maybe_generate_slug(changeset, source, opts) when is_atom(source) do
    source_value = get_field_data(changeset, source, opts)
    do_generate_slug(changeset, source_value, opts)
  end

  def maybe_generate_slug(changeset, sources, opts) do
    cleaned_sources =
      sources
      |> Enum.map(fn v -> get_field_data(changeset, v, opts) end)
      |> Enum.filter(fn v -> has_value?(v) end)

    do_generate_slug(changeset, cleaned_sources, opts)
  end

  # Private functions:

  defp do_build_slug(source) when is_binary(source) do
    source
    |> Slug.slugify()
  end

  defp do_build_slug(sources) do
    sources
    |> Enum.join("-")
    |> Slug.slugify()
  end

  defp do_generate_slug(changeset, sources, opts) do
    slug_key = Keyword.get(opts, :to)
    slug_builder = Keyword.get(opts, :slug_builder)
    always_change = Keyword.get(opts, :always_change, false)

    slug_field = Map.get(changeset.data, slug_key)

    if always_change == true or slug_field == nil do
      # We only generate slug on two occasions:
      # 1. `always_change` flag is set to true
      # 2. `slug_field` is not set, meaning it is a first-time-slug
      do_put_change(changeset, slug_key, slug_builder, sources)
    else
      changeset
    end
  end

  defp do_put_change(changeset, _, _, []), do: changeset
  defp do_put_change(changeset, _, _, nil), do: changeset

  defp do_put_change(changeset, slug_key, slug_builder, sources) do
    # `slug_builder` will be called only if the slug-building occasion
    # was met and the `sources` is not empty.
    slug_string = slug_builder.(sources, changeset)
    put_change(changeset, slug_key, slug_string)
  end

  defp get_field_data(changeset, source, opts) when is_atom(source) do
    always_change = Keyword.get(opts, :always_change, false)
    source_value = get_change(changeset, source, nil)

    if always_change do
      source_value || Map.get(changeset.data, source)
    else
      source_value
    end
  end

  defp get_field_data(_, source, _) when is_binary(source), do: source

  defp has_value?(nil), do: false
  defp has_value?(string) when is_binary(string), do: String.trim(string) != ""
  defp has_value?(_), do: true
end