lib/kuddle/config/types.ex

defmodule Kuddle.Config.Types do
  @moduledoc """
  Annotation cast types.

  User specific types can be registered by setting the kuddle_config types:

  ## Example

      config :kuddle_config,
        types: [
          typename: {Module, cast_function_name},

          geopoint: {MyGeoPoint, :cast},
        ]

  The cast function must return {:ok, any()} or :error if it cannot cast the given value.

  Kuddle Config has some default types they can be overwritten by setting the default_types:

      config :kuddle_config,
        default_types: [
          date: {Date, :from_iso8601},
          utc_datetime: {Kuddle.Config.Types.DateTime, :cast},
          naive_datetime: {NaiveDateTime, :from_iso8601},
          time: {Time, :from_iso8601},
          decimal: {Kuddle.Config.Types.Decimal, :cast},
          atom: {Kuddle.Config.Types.Atom, :cast},
          boolean: {Kuddle.Config.Types.Boolean, :cast},
          tuple: {Kuddle.Config.Types.Tuple, :cast},
          list: {Kuddle.Config.Types.List, :cast},
        ]

  The purpose of the default_types is to provide some sane default which doesn't require any
  additional configuration from you, the user.

  However they can be disabled by setting the default_types config.
  """
  @default_types [
    date: {Date, :from_iso8601},
    utc_datetime: {Kuddle.Config.Types.DateTime, :cast},
    naive_datetime: {NaiveDateTime, :from_iso8601},
    time: {Time, :from_iso8601},
    decimal: {Kuddle.Config.Types.Decimal, :cast},
    atom: {Kuddle.Config.Types.Atom, :cast},
    boolean: {Kuddle.Config.Types.Boolean, :cast},
    tuple: {Kuddle.Config.Types.Tuple, :cast},
    list: {Kuddle.Config.Types.List, :cast},
  ]

  default_types = Application.get_env(:kuddle_config, :default_types, @default_types)

  user_types = Application.get_env(:kuddle_config, :types, [])

  all_types = Keyword.merge(default_types, user_types)

  @doc """
  Cast given value to a different type, normally the input will a string.
  """
  @spec cast(atom() | String.t(), any()) :: {:ok, any()} | :error
  def cast(type, value) do
    case internal_cast(type, value) do
      {:ok, value} ->
        {:ok, value}

      {:error, _reason} ->
        :error

      :error ->
        :error
    end
  end

  @spec internal_cast(atom() | String.t(), any()) :: {:ok, any()} | {:error, term} | :error
  def internal_cast(type, value)

  for {type, {module, func_name}} <- all_types do
    str = Atom.to_string(type)

    def internal_cast(unquote(str), value) do
      internal_cast(unquote(type), value)
    end

    def internal_cast(unquote(type), value) do
      unquote(module).unquote(func_name)(value)
    end
  end

  def internal_cast(_type, _) do
    :error
  end
end