Skip to content

Commit

Permalink
feat: data generations
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Jun 23, 2024
1 parent 9bb797e commit ecc2d2c
Show file tree
Hide file tree
Showing 3 changed files with 272 additions and 3 deletions.
133 changes: 130 additions & 3 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,96 @@ defmodule Peri do
end
end

defguardp is_enumerable(data) when is_map(data) or is_list(data)
@doc """
Checks if the given data is an enumerable, specifically a map or a list.
## Parameters
- `data`: The data to check.
## Examples
iex> is_enumerable(%{})
true
iex> is_enumerable([])
true
iex> is_enumerable(123)
false
iex> is_enumerable("string")
false
"""
defguard is_enumerable(data) when is_map(data) or is_list(data)

@doc """
Checks if the given data conforms to the specified schema.
## Parameters
- `schema`: The schema definition to validate against.
- `data`: The data to be validated.
## Returns
- `true` if the data conforms to the schema.
- `false` if the data does not conform to the schema.
## Examples
iex> schema = %{name: :string, age: :integer}
iex> data = %{name: "Alice", age: 30}
iex> Peri.conforms?(schema, data)
true
iex> invalid_data = %{name: "Alice", age: "thirty"}
iex> Peri.conforms?(schema, invalid_data)
false
"""
def conforms?(schema, data) do
case validate(schema, data) do
{:ok, _} -> true
{:error, _errors} -> false
end
end

if Code.ensure_loaded?(StreamData) do
@doc """
Generates sample data based on the given schema definition using `StreamData`.
This function validates the schema first, and if the schema is valid, it uses the
`Peri.Generatable.gen/1` function to generate data according to the schema.
Note that this function returns a `Stream`, so you traverse easily the data generations.
## Parameters
- `schema`: The schema definition to generate data for.
## Returns
- `{:ok, stream}` if the data is successfully generated.
- `{:error, errors}` if there are validation errors in the schema.
## Examples
iex> schema = %{name: :string, age: {:integer, {:range, {18, 65}}}}
iex> {:ok, stream} = Peri.generate(schema)
iex> [data] = Enum.take(stream, 1)
iex> is_map(data)
true
iex> data[:age] in 18..65
true
"""
def generate(schema) do
with {:ok, schema} <- validate_schema(schema) do
{:ok, Peri.Generatable.gen(schema)}
end
end
end

@doc """
Validates a given data map against a schema.
Expand Down Expand Up @@ -279,8 +360,54 @@ defmodule Peri do
end
end

defguardp is_numeric(n) when is_integer(n) or is_float(n)
defguardp is_numeric_type(t) when t in [:integer, :float]
@doc """
Checks if the given data is a numeric value, specifically a integer or a float.
## Parameters
- `data`: The data to check.
## Examples
iex> is_numeric(123)
true
iex> is_numeric(0xFF)
true
iex> is_numeric(12.12)
true
iex> is_numeric("string")
false
iex> is_numeric(%{})
false
"""
defguard is_numeric(n) when is_integer(n) or is_float(n)

@doc """
Checks if the given type as an atom is a numeric (integer or float).
## Parameters
- `data`: The data to check.
## Examples
iex> is_numeric(:integer)
true
iex> is_numeric(:float)
true
iex> is_numeric(:list)
false
iex> is_numeric({:enum, _})
false
"""
defguard is_numeric_type(t) when t in [:integer, :float]

@doc false
defp validate_field(nil, nil, _data), do: :ok
Expand Down
141 changes: 141 additions & 0 deletions lib/peri/generatable.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
if Code.ensure_loaded?(StreamData) do
defmodule Peri.Generatable do
require Peri

def gen(:atom), do: StreamData.atom(:alphanumeric)
def gen(:string), do: StreamData.string(:alphanumeric)
def gen(:integer), do: StreamData.integer()
def gen(:float), do: StreamData.float()
def gen(:boolean), do: StreamData.boolean()

def gen({:enum, choices}) do
choices
|> Enum.map(&StreamData.constant/1)
|> StreamData.one_of()
end

def gen({:list, type}) do
type
|> gen()
|> StreamData.list_of()
end

def gen({:tuple, types}) do
types
|> Enum.map(&gen/1)
|> List.to_tuple()
|> StreamData.tuple()
end

def gen({type, {:eq, eq}}) when Peri.is_numeric_type(type) do
StreamData.constant(eq)
end

def gen({type, {:neq, neq}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 != neq))
end

def gen({type, {:gt, gt}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 > gt))
end

def gen({type, {:gte, gte}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 >= gte))
end

def gen({type, {:lt, lt}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 < lt))
end

def gen({type, {:lte, lte}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 <= lte))
end

def gen({type, {:range, {min, max}}}) when Peri.is_numeric_type(type) do
stream = gen(type)
StreamData.filter(stream, &(&1 in min..max))
end

def gen({:string, {:regex, regex}}) do
stream = gen(:string)
StreamData.filter(stream, &Regex.match?(regex, &1))
end

def gen({:string, {:eq, eq}}) do
StreamData.constant(eq)
end

def gen({:string, {:min, min}}) do
stream = gen(:string)
StreamData.filter(stream, &(String.length(&1) >= min))
end

def gen({:string, {:max, max}}) do
stream = gen(:string)
StreamData.filter(stream, &(String.length(&1) <= max))
end

def gen({type, {:default, _}}), do: gen(type)
def gen({type, {:dependent, _, _, _}}), do: gen(type)

def gen({type, {:transform, mapper}}) do
stream = gen(type)
StreamData.map(stream, mapper)
end

def gen({:either, {type_1, type_2}}) do
stream_1 = gen(type_1)
stream_2 = gen(type_2)
StreamData.one_of([stream_1, stream_2])
end

def gen({:oneof, types}) do
types
|> Enum.map(&gen/1)
|> StreamData.one_of()
end

def gen({:custom, {mod, fun}}) do
Stream.filter(StreamData.term(), fn val ->
case apply(mod, fun, [val]) do
:ok -> true
{:ok, _} -> true
{:error, _reason, _info} -> false
end
end)
end

def gen({:custom, {mod, fun, args}}) do
Stream.filter(StreamData.term(), fn val ->
case apply(mod, fun, [val | args]) do
:ok -> true
{:ok, _} -> true
{:error, _reason, _info} -> false
end
end)
end

def gen({:custom, cb}) do
Stream.filter(StreamData.term(), fn val ->
case cb.(val) do
:ok -> true
{:ok, _} -> true
{:error, _reason, _info} -> false
end
end)
end

def gen(schema) when Peri.is_enumerable(schema) do
for {k, v} <- schema do
stream = gen(v)
{k, stream}
end
|> StreamData.fixed_map()
end
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ defmodule Peri.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:stream_data, "~> 1.1", optional: true},
{:credo, "~> 1.7", only: :dev, runtime: false},
{:ex_doc, "~> 0.14", only: :dev, runtime: false}
]
Expand Down

0 comments on commit ecc2d2c

Please sign in to comment.