lib/drops/types.ex

defmodule Drops.Types do
  @moduledoc ~S"""
  Drops.Types is a module that provides functions for creating type structs
  from DSL's type specs represented by plain tuples.
  """
  alias Drops.Types.{
    Type,
    Sum,
    List,
    Cast,
    Map,
    Map.Key
  }

  def from_spec(%{primitive: _} = type, _opts) do
    type
  end

  def from_spec(%{} = spec, opts) do
    atomize = opts[:atomize] || false

    keys =
      Enum.map(spec, fn {{presence, name}, type_spec} ->
        case type_spec do
          %{primitive: _} ->
            %Key{path: [name], presence: presence, type: type_spec}

          _ ->
            %Key{path: [name], presence: presence, type: from_spec(type_spec, opts)}
        end
      end)

    %Map{
      primitive: :map,
      constraints: infer_constraints({:type, {:map, []}}, opts),
      atomize: atomize,
      keys: keys
    }
  end

  def from_spec({:sum, {left, right}}, opts) do
    %Sum{left: from_spec(left, opts), right: from_spec(right, opts), opts: opts}
  end

  def from_spec({:type, {:list, member_type}} = spec, opts)
      when is_tuple(member_type) or is_map(member_type) do
    %List{
      primitive: :list,
      constraints: infer_constraints(spec, opts),
      member_type: from_spec(member_type, opts)
    }
  end

  def from_spec({:cast, {input_type, output_type, cast_opts}}, opts) do
    %Cast{
      input_type: from_spec(input_type, opts),
      output_type: from_spec(output_type, opts),
      opts: cast_opts
    }
  end

  def from_spec([left, right], opts) when is_tuple(left) and is_tuple(right) do
    %Sum{left: from_spec(left, opts), right: from_spec(right, opts), opts: opts}
  end

  def from_spec([left, right], opts) when is_map(left) and is_map(right) do
    %Sum{left: from_spec(left, opts), right: from_spec(right, opts), opts: opts}
  end

  def from_spec([left, right], opts) do
    %Sum{left: left, right: right, opts: opts}
  end

  def from_spec(spec, opts) do
    %Type{
      primitive: infer_primitive(spec, opts),
      constraints: infer_constraints(spec, opts)
    }
  end

  def infer_primitive({:type, {type, _}}, _opts) do
    type
  end

  def infer_constraints({:type, {:list, member_type}}, _opts)
      when is_tuple(member_type) or is_map(member_type) do
    [predicate(:type?, :list)]
  end

  def infer_constraints({:type, {type, predicates}}, _opts)
      when length(predicates) > 0 do
    {:and, [predicate(:type?, type) | Enum.map(predicates, &predicate/1)]}
  end

  def infer_constraints({:type, {type, []}}, _opts) do
    [predicate(:type?, type)]
  end

  def predicate({name, args}) do
    predicate(name, args)
  end

  def predicate(name) do
    predicate(name, [])
  end

  def predicate(name, args) do
    {:predicate, {name, args}}
  end
end