lib/type_check/type/stream_data.ex

defmodule TypeCheck.Type.StreamData do
  @moduledoc """
  Transforms types into StreamData generators.

  With the exception of `wrap_with_gen/2`,
  methods in this module are
  only compiled when the optional dependency
  `:stream_data` is added to your project's dependencies.
  """

  defstruct [:type, :generator_function]

  @doc """
  Customizes a type with a _custom_ generator.

  `generator_function` can be a arity-zero function
  (in which case it should simply return a StreamData generator)

  or a arity-one function, in which case it is passed
  the value that would be generated by default and it can be altered
  by e.g. using `StreamData.map/2` or `StreamData.bind/2`.

  Note that these functions _must_ be of the form `&Module.function/arity`
  because this is the only form of function capture that can be stored at compile-time.

  ## Example:

      iex> defmodule IntString do
      ...>   use TypeCheck
      ...>   import TypeCheck.Type.StreamData
      ...>   @type! t() :: ((val :: binary()) when Integer.parse(val) != :error)
      ...>                 |> wrap_with_gen(&IntString.gen/0)
      ...>
      ...>   def gen() do
      ...>     StreamData.integer()
      ...>     |> StreamData.map(&to_string/1)
      ...>   end
      ...> end
      ...>
      ...> IntString.t() |> TypeCheck.Type.StreamData.to_gen() |> StreamData.seeded(42) |> Enum.take(10)
      ["0", "2", "1", "-3", "-5", "-4", "-3", "-4", "3", "-6"]
  """
  def wrap_with_gen(type, generator_function) when is_function(generator_function, 0) or is_function(generator_function, 1) do
    %__MODULE__{type: type, generator_function: generator_function}
  end

  defimpl TypeCheck.Protocols.ToCheck do
    def to_check(s, param) do
      TypeCheck.Protocols.ToCheck.to_check(s.type, param)
    end
  end

  defimpl TypeCheck.Protocols.Inspect do
    def inspect(s, opts) do
      TypeCheck.Protocols.Inspect.inspect(s.type, opts)
    end
  end

  if Code.ensure_loaded?(StreamData) do

    defimpl TypeCheck.Protocols.ToStreamData do
      def to_gen(s) do
        if is_function(s.generator_function, 0) do
          s.generator_function.()
        else
          s.type
          |> TypeCheck.Protocols.ToStreamData.to_gen()
          |> s.generator_function.()
        end
      end
    end

    @doc """
    When given a type, it is transformed to a StreamData generator
    that can be used in a property test.

        iex> import TypeCheck.Type.StreamData
        iex> generator = TypeCheck.Type.build({:ok | :error, integer()}) |> to_gen()
        iex> StreamData.seeded(generator, 42) |> Enum.take(10)
        [
        {:ok, -1},
        {:ok, 2},
        {:ok, -2},
        {:ok, -4},
        {:ok, 1},
        {:ok, 1},
        {:ok, 2},
        {:ok, 4},
        {:ok, -7},
        {:ok, 5}
        ]
    """
    def to_gen(type) do
      TypeCheck.Protocols.ToStreamData.to_gen(type)
    end

    def arbitrary_primitive_type_gen do
      choices = primitive_types_list()
      Elixir.StreamData.one_of(choices)
    end

    defp primitive_types_list() do
      import TypeCheck.Builtin

      simple =
        [
          any(),
          atom(),
          binary(),
          bitstring(),
          boolean(),
          float(),
          function(),
          integer(),
          number()
        ]
        |> Enum.map(&Elixir.StreamData.constant/1)

      lit = Elixir.StreamData.term() |> Elixir.StreamData.map(&literal/1)

      [lit | simple]
    end

    def arbitrary_type_gen() do
      # TODO WIP
      StreamData.one_of(
        primitive_types_list() ++ [list_gen(), map_gen(), fixed_list_gen(), fixed_tuple_gen()]
      )
    end

    defp list_gen() do
      lazy_type_gen()
      |> StreamData.map(&TypeCheck.Builtin.list/1)
    end

    defp map_gen() do
      {lazy_type_gen(), lazy_type_gen()}
      |> StreamData.map(fn {key_type, value_type} ->
        TypeCheck.Builtin.map(key_type, value_type)
      end)
    end

    def fixed_list_gen() do
      lazy_type_gen()
      |> StreamData.list_of()
      |> StreamData.map(&TypeCheck.Builtin.fixed_list/1)
    end

    defp fixed_tuple_gen() do
      lazy_type_gen()
      |> StreamData.list_of(max_length: 255)
      |> StreamData.map(&TypeCheck.Builtin.fixed_tuple/1)
    end

    defp lazy_type_gen() do
      # Lazily call content generator
      # To prevent infinite expansion recursion
      StreamData.constant({})
      |> StreamData.bind(fn _ ->
        arbitrary_type_gen()
        |> StreamData.scale(fn size -> div(size, 2) end)
      end)
    end

  else

    def arbitrary_type_gen() do
      raise ArgumentError, """
      `arbitrary_type_gen/0` depends on the optional library `:stream_data`.
      To use this functionality, add `:stream_data` to your application's deps.
      """
    end
  end
end