defmodule Estructura.StreamData do @moduledoc """ Additional generators to be used with `__generator__/1` generated by `Use Estructura` """ @typep seed() :: :rand.state() @typep size() :: non_neg_integer() @typep generator_fun(a) :: (seed(), size() -> StreamData.LazyTree.t(a)) @typedoc """ An opaque type that represents an `Estructura.StreamData` generator that generates values of type `a`. """ @opaque t(a) :: %StreamData{generator: generator_fun(a)} | Enumerable.t() @doc "Identity function as a stream data generator" @spec id(keyword()) :: StreamData.t((term() -> term())) def id(_options \\ []) do StreamData.constant(&Function.identity/1) end @doc "Helper to generate _unshrinkable_ streams as per `Stream.iterate/2`." @spec iterate(value, (value -> value)) :: StreamData.t(value) when value: term() def iterate(initial_value, next_fun) when is_function(next_fun, 1) do initial_value |> StreamData.constant() |> StreamData.bind(&StreamData.constant(next_fun.(&1))) |> StreamData.unshrinkable() end @spec date(keyword()) :: StreamData.t(Date.t()) @doc """ Generates an instance of `Date.t()`. This generator is unshrinkable. """ def date(options \\ []) do from = Keyword.get(options, :from, ~D|2000-01-01|) to = Keyword.get(options, :to, Date.utc_today()) step = Keyword.get(options, :step, :random) from |> iterate(fn d -> next = case step do days when is_integer(days) -> Date.add(d, days) :random -> Date.add(d, Enum.random(-30..30//1)) {:random, num} -> Date.add(d, Enum.random(-num..num//1)) end case Date.diff(to, next) do diff when diff <= 0 -> to diff when diff > 0 -> Date.add(from, diff) end end) end @spec datetime(keyword()) :: StreamData.t(DateTime.t()) @doc """ Generates an instance of `DateTime.t()`. This generator is unshrinkable. """ def datetime(options \\ []) do from = Keyword.get(options, :from, ~U|2000-01-01T00:00:00Z|) to = Keyword.get(options, :to, DateTime.utc_now()) step = Keyword.get(options, :step, :random) from |> iterate(fn dt -> next = case step do secs when is_integer(secs) -> DateTime.add(dt, secs, :second) {num, unit} when is_integer(num) -> DateTime.add(dt, num, unit) :random -> DateTime.add(dt, Enum.random(-86_400..86_400//1), :second) {:random, num} -> DateTime.add(dt, Enum.random(-num..num//1), :second) end case DateTime.diff(to, next) do diff when diff <= 0 -> to diff when diff > 0 -> DateTime.add(from, diff) end end) end end