lib/tyyppi/value.ex

defmodule Tyyppi.Value do
  @moduledoc """
  Value type to be used with `Tyyppi`.

  It wraps the standard _Elixir_ type in a struct, also providing optional coercion,
    validation, documentation, and `Access` implementation.

  ## Built-in constructors

  * `any` 
  * `atom` 
  * `string` 
  * `boolean` 
  * `integer` 
  * `non_neg_integer` 
  * `pos_integer` 
  * `timeout` 
  * `pid` 
  * `mfa` 
  * `mod_arg` 
  * `fun` 
  * `one_of` 
  * `formulae` 
  * `list` — creates a `[type()]` wrapped into a value
  * `struct` 
  """

  require Logger
  require Tyyppi

  Tyyppi.formulae_guard()

  alias Tyyppi.Value
  alias Tyyppi.Value.{Coercions, Encodings, Generations, Validations}

  @typedoc "Type of the coercion function allowed"
  @type coercer :: (Tyyppi.Valuable.value() -> Tyyppi.Valuable.either())

  @typedoc "Type of the encoder function, that might be used for e. g. Json serialization"
  @type encoder :: (Tyyppi.Valuable.value(), keyword() -> binary()) | nil

  @typedoc "Type of the validation function allowed"
  @type validator ::
          (Tyyppi.Valuable.value() -> Tyyppi.Valuable.either())
          | (Tyyppi.Valuable.value(), %{required(atom()) => any()} -> Tyyppi.Valuable.either())

  @typedoc """
  Type of the generator function, producing the stream of `value()`
  """
  @type generator ::
          (() -> Tyyppi.Valuable.generation()) | (any() -> Tyyppi.Valuable.generation())

  @type t(wrapped) :: %{
          __struct__: Tyyppi.Value,
          value: Tyyppi.Valuable.value(),
          documentation: String.t(),
          type: Tyyppi.T.t(wrapped),
          coercion: coercer(),
          validation: validator(),
          encoding: encoder(),
          generation: {generator(), any()} | generator() | nil,
          __meta__: %{
            defined?: boolean(),
            optional?: boolean(),
            errors: [any()],
            subsection: String.t()
          },
          __context__: %{optional(atom()) => any()}
        }
  @type t() :: t(term())

  defstruct value: nil,
            type: Tyyppi.parse(any()),
            documentation: "",
            coercion: &Tyyppi.void_coercion/1,
            validation: &Tyyppi.void_validation/1,
            encoding: nil,
            generation: nil,
            __meta__: %{defined?: false, optional?: false, errors: [], subsection: ""},
            __context__: %{}

  defmacrop defined,
    do: quote(do: %__MODULE__{__meta__: %{defined?: true}, value: var!(value)})

  defmacrop meta,
    do: quote(do: %__MODULE__{__meta__: var!(meta)})

  defmacrop value,
    do: quote(do: %__MODULE__{__meta__: var!(meta), value: var!(value)})

  @behaviour Access

  @impl Access
  @doc false
  def fetch(defined(), :value), do: {:ok, value}
  def fetch(%__MODULE__{}, :value), do: :error
  def fetch(%__MODULE__{__meta__: %{errors: []}}, :errors), do: :error
  def fetch(meta(), :errors), do: {:ok, meta.errors}

  def fetch(%__MODULE__{documentation: <<_::8, _::binary>> = documentation}, :documentation),
    do: {:ok, documentation}

  def fetch(%__MODULE__{}, :documentation), do: :error

  @impl Access
  @doc false
  def pop(value() = data, :value) do
    {value, %__MODULE__{data | __meta__: Map.put(meta, :defined?, false), value: nil}}
  end

  def pop(meta() = data, :errors) do
    {with([] <- meta.errors, do: nil), %__MODULE__{data | __meta__: Map.put(meta, :errors, [])}}
  end

  def pop(%__MODULE__{documentation: documentation} = data, :documentation),
    do: {documentation, %__MODULE__{data | documentation: ""}}

  def pop(%__MODULE__{}, key),
    do: raise(BadStructError, struct: __MODULE__, term: key)

  @impl Access
  @doc false
  def get_and_update(value() = data, :value, fun) do
    case fun.(value) do
      :pop ->
        pop(data, :value)

      {get_value, update_value} ->
        case validate(data, update_value) do
          {:ok, update_value} ->
            {get_value, update_value}

          # raise ArgumentError, message: inspect(error)
          {:error, error} ->
            meta = %{meta | defined?: false, errors: error ++ meta.errors}
            {get_value, %__MODULE__{data | __meta__: meta}}
        end
    end
  end

  def get_and_update(%__MODULE__{}, key, _),
    do: raise(BadStructError, struct: __MODULE__, term: key)

  #############################################################################
  @doc false
  @spec validation(data :: t()) :: (Tyyppi.Valuable.value() -> Tyyppi.Valuable.either())
  def validation(%__MODULE__{__meta__: %{optional?: true}, value: nil}), do: &{:ok, &1}

  def validation(%__MODULE__{validation: f}) when is_function(f, 1), do: &f.(&1)

  def validation(%__MODULE__{__context__: c, validation: f}) when is_function(f, 2),
    do: &f.(&1, c)

  def validation(%__MODULE__{}), do: &{:ok, &1}

  @doc false
  @spec valid?(t()) :: boolean()
  def valid?(meta()), do: meta.defined?
  def valid?(_), do: false

  @spec value_type?(nil | Tyyppi.T.t(wrapped)) :: boolean() when wrapped: term()
  @doc false
  def value_type?(%Tyyppi.T{module: Tyyppi.Value, name: :t}), do: true
  def value_type?(_), do: false

  @spec value?(any()) :: boolean()
  @doc false
  def value?(%Tyyppi.Value{}), do: true
  def value?(_), do: false

  @spec validate(data :: t(), any()) :: Tyyppi.Valuable.either()
  def validate(%__MODULE__{__meta__: %{optional?: true} = meta} = data, nil) do
    defined? = Tyyppi.of_type?(data.type, nil)
    {:ok, %__MODULE__{data | __meta__: Map.put(meta, :defined?, defined?), value: nil}}
  end

  def validate(meta() = data, nil),
    do: {:ok, %__MODULE__{data | __meta__: Map.put(meta, :defined?, false), value: nil}}

  def validate(meta() = data, value) do
    with {:coercion, {:ok, cast}} <- {:coercion, data.coercion.(value)},
         true <- Tyyppi.of_type?(data.type, cast),
         {:validation, {:ok, value}} <- {:validation, validation(data).(cast)} do
      {:ok, %__MODULE__{data | __meta__: Map.put(meta, :defined?, true), value: value}}
    else
      false ->
        {:error, [type: [expected: to_string(data.type), got: value]]}

      {operation, {:error, error}} ->
        {:error, [{operation, [message: error, got: value]}]}
    end
  end

  ##############################################################################

  @behaviour Tyyppi.Valuable

  @impl Tyyppi.Valuable
  def generation(%__MODULE__{generation: {g, params}}) when is_function(g, 1), do: g.(params)
  def generation(%__MODULE__{generation: g} = data) when is_function(g, 1), do: g.(data)
  def generation(%__MODULE__{generation: g}) when is_function(g, 0), do: g.()

  @impl Tyyppi.Valuable
  def validate(%__MODULE__{value: value} = data), do: validate(data, value)

  @impl Tyyppi.Valuable
  def coerce(%__MODULE__{value: value} = data), do: data.coercion.(value)

  @impl Tyyppi.Valuable
  def flatten(data, opts \\ [])

  def flatten(%__MODULE__{value: %type{value: value}}, opts) do
    force = Keyword.get(opts, :force, true)

    if force or Tyyppi.can_flatten?(type),
      do: type.flatten(value, opts),
      else: value
  end

  def flatten(%type{value: value}, opts) do
    force = Keyword.get(opts, :force, true)

    if force or Tyyppi.can_flatten?(type),
      do: type.flatten(value, opts),
      else: value
  end

  def flatten(%__MODULE__{value: value}, _opts), do: value
  def flatten(value, _opts), do: value

  ##############################################################################
  @type factory_option ::
          {:value, any()}
          | {:documentation, String.t()}
          | {:type, Tyyppi.T.t(term())}
          | {:coercion, coercer()}
          | {:validation, validator()}
          | {:encoding, encoder()}
          | {:generation, {generator(), any()} | generator() | nil}

  @keys ~w|value documentation type coercion validation encoding generation|a

  @spec any() :: t()
  @doc "Creates a not defined `any()` wrapped by `Tyyppi.Value`"
  def any,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(any()),
      coercion: &Coercions.any/1,
      generation: &Generations.any/0
    }

  @spec any(any() | [factory_option()]) :: t()
  @doc "Factory for `any()` wrapped by `Tyyppi.Value`"
  def any([{:value, _} | _] = options), do: put_options(any(), options)
  def any([{:documentation, _} | _] = options), do: put_options(any(), options)
  def any(any), do: any(value: any)

  @spec atom() :: t()
  @doc "Creates a not defined `atom()` wrapped by `Tyyppi.Value`"
  def atom,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(atom()),
      coercion: &Coercions.atom/1,
      generation: {&Generations.atom/1, :alphanumeric}
    }

  @spec atom(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `atom()` wrapped by `Tyyppi.Value`"
  def atom([{:value, _} | _] = options), do: put_options(atom(), options)
  def atom([{:documentation, _} | _] = options), do: put_options(atom(), options)
  def atom(atom), do: atom(value: atom)

  @spec string() :: t()
  @doc "Creates a not defined `String.t()` wrapped by `Tyyppi.Value`"
  def string,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(String.t()),
      coercion: &Coercions.string/1,
      generation: &Generations.string/0
    }

  @spec string(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `String.t()` wrapped by `Tyyppi.Value`"
  def string([{:value, _} | _] = options), do: put_options(string(), options)
  def string([{:documentation, _} | _] = options), do: put_options(string(), options)
  def string(string), do: string(value: string)

  @spec boolean() :: t()
  @doc "Creates a not defined `boolean()` wrapped by `Tyyppi.Value`"
  def boolean,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(boolean()),
      coercion: &Coercions.boolean/1,
      generation: &Generations.boolean/0
    }

  @spec boolean(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `boolean()` wrapped by `Tyyppi.Value`"
  def boolean(options) when is_list(options), do: put_options(boolean(), options)
  def boolean(boolean), do: boolean(value: boolean)

  @spec integer() :: t()
  @doc "Creates a not defined `integer()` wrapped by `Tyyppi.Value`"
  def integer,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(integer()),
      coercion: &Coercions.integer/1,
      generation: &Generations.integer/0
    }

  @spec integer(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `integer()` wrapped by `Tyyppi.Value`"
  def integer(options) when is_list(options), do: put_options(integer(), options)
  def integer(integer), do: integer(value: integer)

  @spec non_neg_integer() :: t()
  @doc "Creates a not defined `non_neg_integer()` wrapped by `Tyyppi.Value`"
  def non_neg_integer,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(non_neg_integer()),
      coercion: &Coercions.integer/1,
      validation: &Validations.non_neg_integer/1,
      generation: &Generations.non_neg_integer/0
    }

  @spec non_neg_integer(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `non_neg_integer()` wrapped by `Tyyppi.Value`"
  def non_neg_integer(options) when is_list(options), do: put_options(non_neg_integer(), options)
  def non_neg_integer(non_neg_integer), do: non_neg_integer(value: non_neg_integer)

  @spec pos_integer() :: t()
  @doc "Creates a not defined `pos_integer()` wrapped by `Tyyppi.Value`"
  def pos_integer,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(pos_integer()),
      coercion: &Coercions.integer/1,
      validation: &Validations.pos_integer/1,
      generation: &Generations.pos_integer/0
    }

  @spec pos_integer(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `pos_integer()` wrapped by `Tyyppi.Value`"
  def pos_integer(options) when is_list(options), do: put_options(pos_integer(), options)
  def pos_integer(pos_integer), do: pos_integer(value: pos_integer)

  @spec float() :: t()
  @doc "Creates a not defined `float()` wrapped by `Tyyppi.Value`"
  def float,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(float()),
      coercion: &Coercions.float/1,
      generation: &Generations.float/0
    }

  @spec float(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `float()` wrapped by `Tyyppi.Value`"
  def float(options) when is_list(options), do: put_options(float(), options)
  def float(float), do: float(value: float)

  @spec date() :: t()
  @doc "Creates a not defined `date()` wrapped by `Tyyppi.Value`"
  def date,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(Date.t()),
      coercion: &Coercions.date/1,
      generation: &Generations.date/0
    }

  @spec date(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `date()` wrapped by `Tyyppi.Value`"
  def date(options) when is_list(options), do: put_options(date(), options)
  def date(date), do: date(value: date)

  @spec date_time() :: t()
  @doc "Creates a not defined `date_time()` wrapped by `Tyyppi.Value`"
  def date_time,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(DateTime.t()),
      coercion: &Coercions.date_time/1,
      generation: &Generations.date_time/0
    }

  @spec date_time(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `date_time()` wrapped by `Tyyppi.Value`"
  def date_time(options) when is_list(options), do: put_options(date_time(), options)
  def date_time(date_time), do: date_time(value: date_time)

  @spec timeout() :: t()
  @doc "Creates a not defined `timeout()` wrapped by `Tyyppi.Value`"
  def timeout,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(timeout()),
      coercion: &Coercions.timeout/1,
      validation: &Validations.timeout/1,
      generation: &Generations.timeout/0
    }

  @spec timeout(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `timeout()` wrapped by `Tyyppi.Value`"
  def timeout(options) when is_list(options), do: put_options(timeout(), options)
  def timeout(timeout), do: timeout(value: timeout)

  @spec pid() :: t()
  @doc "Creates a not defined `pid()` wrapped by `Tyyppi.Value`"
  def pid,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(pid()),
      coercion: &Coercions.pid/1,
      encoding: &Encodings.pid/2,
      generation: &Generations.pid/0
    }

  @spec pid(options :: any() | [factory_option()]) :: t()
  @doc "Factory for `pid()` wrapped by `Tyyppi.Value`"
  def pid([{:value, _} | _] = options), do: put_options(pid(), options)
  def pid([{:documentation, _} | _] = options), do: put_options(pid(), options)
  def pid(pid), do: pid(value: pid)

  @spec pid(p1 :: non_neg_integer(), p2 :: non_neg_integer(), p3 :: non_neg_integer()) :: t()
  @doc "Factory for `pid()` wrapped by `Tyyppi.Value`"
  def pid(p1, p2, p3)
      when is_integer(p1) and p1 >= 0 and is_integer(p2) and p2 >= 0 and is_integer(p3) and
             p3 >= 0,
      do: pid(value: Enum.join([p1, p2, p3], "."))

  @spec mfa() :: t()
  @doc "Creates a not defined `mfa` wrapped by `Tyyppi.Value`"
  def mfa,
    do: %Tyyppi.Value{
      type: Tyyppi.parse({module(), atom(), non_neg_integer()}),
      validation: &Validations.mfa/2,
      coercion: &Coercions.mfa/1,
      generation: &Generations.mfa/1
    }

  @spec mfa(
          options ::
            boolean()
            | function()
            | {module(), atom(), non_neg_integer()}
            | [{:existing, boolean()} | factory_option()]
        ) :: t()
  @doc "Factory for `mfa` wrapped by `Tyyppi.Value`"
  def mfa(existing) when is_boolean(existing),
    do: %Tyyppi.Value{mfa() | __context__: %{existing: existing}}

  def mfa(options) when is_list(options) do
    {existing, options} = Keyword.pop(options, :existing, false)
    existing |> mfa() |> put_options(options)
  end

  def mfa(fun) when is_function(fun), do: put_in(mfa(), [:value], fun)
  def mfa(mfa), do: mfa(value: mfa)

  @spec mfa(m :: module(), f :: atom(), a :: non_neg_integer()) :: t()
  @doc "Factory for `mfa` wrapped by `Tyyppi.Value`"
  def mfa(m, f, a), do: mfa(value: {m, f, a})

  @spec mod_arg() :: t()
  @doc "Creates a not defined `mod_arg` wrapped by `Tyyppi.Value`"
  def mod_arg,
    do: %Tyyppi.Value{
      type: Tyyppi.parse({module(), list()}),
      validation: &Validations.mod_arg/2,
      generation: &Generations.mod_arg/1
    }

  @spec mod_arg(
          options :: boolean() | {module(), list()} | [{:existing, boolean()} | factory_option()]
        ) :: t()
  @doc "Factory for `mod_arg` wrapped by `Tyyppi.Value`"
  def mod_arg(existing) when is_boolean(existing),
    do: %Tyyppi.Value{mod_arg() | __context__: %{existing: existing}}

  def mod_arg(options) when is_list(options) do
    {existing, options} = Keyword.pop(options, :existing, false)
    existing |> mod_arg() |> put_options(options)
  end

  def mod_arg(mod_arg), do: mod_arg(value: mod_arg)

  @spec mod_arg(m :: module(), args :: list()) :: t()
  @doc "Factory for `mod_arg` wrapped by `Tyyppi.Value`"
  def mod_arg(m, args) when is_atom(m) and is_list(args), do: mod_arg(value: {m, args})

  #############################################################################

  @spec fun(:any | arity() | keyword() | fun()) :: t() | no_return
  @doc "Creates a not defined `fun` wrapped by `Tyyppi.Value`"
  def fun(arity \\ :any)

  def fun(:any),
    do: %Tyyppi.Value{
      type: Tyyppi.parse(fun()),
      validation: &Validations.fun/2,
      generation: &Generations.fun/1
    }

  def fun(arity) when is_integer(arity) and arity >= 0 and arity <= 255,
    do: %Tyyppi.Value{fun(:any) | __context__: %{arity: arity}}

  def fun(options) when is_list(options),
    do:
      options
      |> Keyword.get(:value, :any)
      |> Function.info(:arity)
      |> elem(1)
      |> fun()
      |> put_options(options)

  def fun(f) when is_function(f), do: fun(value: f)

  @spec do_one_of(keyword()) :: t()
  defp do_one_of(options) when is_list(options) do
    {allowed, options} = Keyword.pop(options, :allowed, [])
    allowed |> one_of() |> put_options(options)
  end

  @spec one_of([any()]) :: t()
  @doc "Creates a `one_of` value wrapped by `Tyyppi.Value`"
  def one_of([{:value, _} | _] = options), do: do_one_of(options)
  def one_of([{:documentation, _} | _] = options), do: do_one_of(options)
  def one_of([{:allowed, _} | _] = options), do: do_one_of(options)

  def one_of(allowed) when is_list(allowed),
    do: %Tyyppi.Value{
      type: Tyyppi.parse(any()),
      validation: &Validations.one_of/2,
      generation: &Generations.one_of/1,
      __context__: %{allowed: allowed}
    }

  @spec one_of(any(), [any()]) :: t()
  def one_of(value, allowed), do: allowed |> one_of() |> put_in([:value], value)

  @spec formulae() :: t()
  @doc "Creates a not defined `formulae` wrapped by `Tyyppi.Value`"
  def formulae,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(any()),
      validation: &Validations.formulae/2,
      generation: &Generations.formulae/1
    }

  @doc "Factory for `formulae` wrapped by `Tyyppi.Value`"
  case Code.ensure_compiled(Formulae) do
    {:module, Formulae} ->
      @spec formulae(
              value :: any(),
              formulae :: Formulae.t() | binary() | {module(), atom(), list()}
            ) :: t()

      def formulae(value, {mod, fun, args}),
        do: formulae(value: value, formulae: {mod, fun, args})

      def formulae(value, formulae), do: formulae(value: value, formulae: formulae)

      @spec formulae(
              options ::
                Formulae.t() | binary() | [{:formulae, any()} | factory_option()]
            ) ::
              t()
      def formulae(formulae) when is_binary(formulae) or is_formulae(formulae),
        do: %Tyyppi.Value{formulae() | __context__: %{formulae: Formulae.compile(formulae)}}

    _ ->
      @spec formulae(
              value :: any(),
              formulae :: binary() | {module(), atom(), list()}
            ) :: t()
      def formulae(value, {mod, fun, args}),
        do: formulae(value: value, formulae: {mod, fun, args})

      @spec formulae(options :: binary() | [{:formulae, any()} | factory_option()]) :: t()
  end

  def formulae({mod, fun, args}),
    do: %Tyyppi.Value{formulae() | __context__: %{formulae: {mod, fun, args}}}

  def formulae(options) when is_list(options) do
    {formulae, options} = Keyword.pop(options, :formulae, [])
    formulae |> formulae() |> put_options(options)
  end

  @spec list() :: t()
  @doc "Creates a not defined `list` wrapped by `Tyyppi.Value`"
  def list,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(list()),
      validation: &Validations.list/2,
      generation: &Generations.list/1,
      __context__: %{type: Tyyppi.parse(any())}
    }

  @spec list(options :: Tyyppi.T.t(wrapped) | [{:type, Tyyppi.T.t(wrapped)} | factory_option()]) ::
          t(wrapped)
        when wrapped: term()
  @doc "Factory for `list` wrapped by `Tyyppi.Value`"
  def list(%Tyyppi.T{} = type), do: %Tyyppi.Value{list() | __context__: %{type: type}}

  def list(options) when is_list(options) do
    {type, options} = Keyword.pop(options, :type, [])
    type |> list() |> put_options(options)
  end

  @spec list(value :: list(), type :: Tyyppi.T.t(wrapped)) :: t(wrapped) when wrapped: term()
  def list(value, %Tyyppi.T{} = type) when is_list(value), do: list(value: value, type: type)

  @spec struct() :: t()
  @doc "Creates a not defined `struct` wrapped by `Tyyppi.Value`"
  def struct,
    do: %Tyyppi.Value{
      type: Tyyppi.parse(struct()),
      validation: &Validations.struct/1,
      generation: &Generations.struct/1
    }

  @spec struct(options :: [factory_option()]) :: t()
  @doc "Factory for `struct` wrapped by `Tyyppi.Value`"
  def struct(options) when is_list(options), do: Value.struct() |> put_options(options)

  @spec struct(value :: struct()) :: t()
  def struct(%_ts{} = value), do: Value.struct(value: value)

  #############################################################################

  @spec put_options(acc :: t(), options :: [factory_option()]) :: t()
  defp put_options(acc, options) do
    {result, unknowns} =
      Enum.reduce(options, {acc, []}, fn
        {k, v}, {acc, unknowns} when k in @keys ->
          {put_in(acc, [k], v), unknowns}

        {k, _}, {acc, unknowns} ->
          {acc, [k | unknowns]}
      end)

    unless unknowns == [] do
      raise("Unknown keys #{inspect(unknowns)} were ignored in `Value` constructor")
    end

    result
  end

  @spec optional(Value.t(wrapped)) :: Value.t(wrapped) when wrapped: term()
  def optional(%Value{__meta__: meta} = value) do
    type = Tyyppi.parse_quoted({:|, [], [nil, value.type.quoted]})
    generation = {&Generations.optional/1, value.generation}
    meta = %{meta | optional?: true}
    %Value{value | type: type, generation: generation, __meta__: meta}
  end

  #############################################################################

  if Code.ensure_loaded?(Jason.Encoder) do
    defimpl Jason.Encoder do
      @moduledoc false
      alias Jason.Encoder, as: E

      def encode(%Tyyppi.Value{__meta__: %{defined?: false}}, opts), do: E.encode(nil, opts)
      def encode(%Tyyppi.Value{encoding: nil, value: value}, opts), do: E.encode(value, opts)
      def encode(%Tyyppi.Value{encoding: encoder, value: value}, opts), do: encoder.(value, opts)
    end
  end

  defimpl Inspect do
    @moduledoc false
    import Inspect.Algebra

    def inspect(%Tyyppi.Value{value: value, __meta__: %{errors: errors}}, opts)
        when length(errors) > 0 do
      concat(["‹✗ #{inspect(Keyword.keys(errors))} ", to_doc(value, opts), "›"])
    end

    def inspect(%Tyyppi.Value{value: value, __meta__: %{defined?: true}}, opts) do
      concat(["‹", to_doc(value, opts), "›"])
    end

    def inspect(%Tyyppi.Value{value: value}, opts) do
      concat(["‹‽ ", to_doc(value, opts), "›"])
    end
  end
end