Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d58df6a
add helper macro to avoid code duplication in tests
albertored May 31, 2021
a9c01d9
handle exclusiveMinimum, exclusiveMaximum and multipleOf in integer g…
albertored May 31, 2021
d5f60a9
handle minItems, maxItems and uniqueItems in array generation
albertored May 31, 2021
1531187
enum should work not only for strings
albertored May 31, 2021
795d7ce
not required properties can miss from generated object
albertored May 31, 2021
901df4a
handle $ref
albertored May 31, 2021
12fb57d
use StreamData for generating strings
albertored May 31, 2021
cee3db4
handle oneOf and anyOf
albertored May 31, 2021
25b5861
reduce size of strings generated from regex
albertored May 31, 2021
bc31724
add options for always generate also not required properties
albertored May 31, 2021
b892815
make generators unshrinkable
albertored May 31, 2021
59f4e22
handle allOf
albertored May 31, 2021
1314177
handle number type (float or integer)
albertored May 31, 2021
ac496f7
type can be a list
albertored May 31, 2021
a37fb5e
handle additionalItems and items as an array of schemas
albertored May 31, 2021
dacc754
empty objects and arrays
albertored May 31, 2021
6eefae9
split in submodules
albertored May 31, 2021
b4f770f
allOf fixes and tests
albertored Jun 1, 2021
8014f5a
integer fixes when endpoints are multiple of multipleOf
albertored Jun 1, 2021
87fe03f
generate patternProperties in objects
albertored Jun 1, 2021
011eb2d
correctly handle min/maxItems also with additionalItems
albertored Jun 1, 2021
7f199e2
rearrange folders
albertored Jun 1, 2021
3a2f7ae
allow to set {mod, fun} for custom format string generation
albertored Jun 3, 2021
3b2f5ff
hide StreamData dialyzer warnings
albertored Jun 3, 2021
b55c4c9
generate generic json if empty schema is given
albertored Jun 3, 2021
e2cfee0
handle minProperties and maxProperties for objects
albertored Jun 3, 2021
6cb0851
handle additionalProperties
albertored Jun 3, 2021
8323ca7
fix: set max_length when generating unique items arrays with limited …
albertored Jun 3, 2021
6286c37
fix: correcly merge deeply nested allOf
albertored Jun 3, 2021
6f60cf9
fix: generation of unique items arrays with enum of small size uses e…
albertored Jun 4, 2021
0ce6885
detect type of schema by checking its own specific keys when type is …
albertored Jun 4, 2021
758c870
handle not keyword
albertored Jun 4, 2021
57595f7
better error handling
albertored Jun 4, 2021
941b302
fix: undefined function pattern_properties_generator/6
albertored Jun 10, 2021
46304a4
fix: always generate additional and pattern properties
albertored Apr 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 98 additions & 66 deletions lib/json_data_faker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,39 @@ defmodule JsonDataFaker do
"""
import StreamData
require Logger

alias ExJsonSchema.Schema

alias JsonDataFaker.Generator.{Array, Misc, Number, Object, String}

defmodule InvalidSchemaError do
defexception [:message]
end

defmodule GenerationError do
defexception [:message]
end

if Mix.env() == :test do
defp unshrink(stream), do: stream
else
defp unshrink(stream), do: StreamData.unshrinkable(stream)
end

@string_keys ["pattern", "minLength", "maxLength"]
@number_keys ["multipleOf", "maximum", "exclusiveMaximum", "minimum", "exclusiveMinimum"]
@array_keys ["additionalItems", "items", "maxItems", "minItems", "uniqueItems"]
@object_keys [
"maxProperties",
"minProperties",
"required",
"additionalProperties",
"properties",
"patternProperties"
]

@misc_keys ["$ref", "oneOf", "anyOf", "allOf", "not", "enum"]

@doc """
generate fake data with given schema. It could be a raw json schema or ExJsonSchema.Schema.Root type.

Expand All @@ -19,98 +50,99 @@ defmodule JsonDataFaker do
...> "required" => ["title"],
...> "type" => "object"
...>}
iex> %{"title" => _title, "body" => _body} = JsonDataFaker.generate(schema) |> Enum.take(1) |> List.first()
iex> %{"title" => _title} = JsonDataFaker.generate!(schema) |> Enum.take(1) |> List.first()
"""
def generate(%Schema.Root{} = schema) do
generate_by_type(schema.schema)
end
def generate!(schema, opts \\ [])

def generate!(schema, opts) when is_map(schema) do
{root, schema} =
case schema do
%Schema.Root{} ->
{schema, schema.schema}

_ ->
root = Schema.resolve(schema)
{root, root.schema}
end

def generate(schema) when is_map(schema) do
generate(Schema.resolve(schema))
schema
|> generate_by_type(root, opts)
|> unshrink()
rescue
e in JsonDataFaker.InvalidSchemaError ->
reraise e, __STACKTRACE__

e ->
Logger.error("Failed to generate data. #{inspect(e)}")
nil
end
%struct{} = e

def generate(_schema), do: nil
case Module.split(struct) do
["ExJsonSchema" | _] ->
reraise JsonDataFaker.InvalidSchemaError, [message: e.message], __STACKTRACE__

# private functions
defp generate_by_type(%{"type" => "boolean"}) do
boolean()
end
["StreamData" | _] ->
reraise JsonDataFaker.GenerationError, [message: e.message], __STACKTRACE__

defp generate_by_type(%{"type" => "string"} = schema) do
generate_string(schema)
_ ->
reraise e, __STACKTRACE__
end
end

defp generate_by_type(%{"type" => "integer"} = schema) do
min = schema["minimum"] || 10
max = schema["maximum"] || 1000
integer(min..max)
def generate!(schema, _opts) do
msg = "invalid schema, it should be a map or a resolved ExJsonSchema, got #{inspect(schema)}"
raise JsonDataFaker.InvalidSchemaError, message: msg
end

defp generate_by_type(%{"type" => "array"} = schema) do
inner_schema = schema["items"]
count = Enum.random(2..5)

StreamData.list_of(generate_by_type(inner_schema), length: count)
def generate(schema, opts \\ []) do
{:ok, generate!(schema, opts)}
rescue
e -> {:error, e.message}
end

defp generate_by_type(%{"type" => "object"} = schema) do
stream_gen(fn ->
Enum.reduce(schema["properties"], %{}, fn {k, inner_schema}, acc ->
v = inner_schema |> generate_by_type() |> Enum.take(1) |> List.first()
@doc false

Map.put(acc, k, v)
end)
end)
for key <- @misc_keys do
def generate_by_type(schema, root, opts) when is_map_key(schema, unquote(key)),
do: Misc.generate(schema, root, opts)
end

defp generate_by_type(_schema), do: StreamData.constant(nil)
def generate_by_type(%{"type" => [_ | _]} = schema, root, opts),
do: Misc.generate(schema, root, opts)

defp generate_string(%{"format" => "date-time"}),
do: stream_gen(fn -> 30 |> Faker.DateTime.backward() |> DateTime.to_iso8601() end)
def generate_by_type(%{"type" => "boolean"}, _root, _opts), do: boolean()

defp generate_string(%{"format" => "uuid"}), do: stream_gen(&Faker.UUID.v4/0)
defp generate_string(%{"format" => "email"}), do: stream_gen(&Faker.Internet.email/0)
def generate_by_type(%{"type" => "string"} = schema, root, opts),
do: String.generate(schema, root, opts)

defp generate_string(%{"format" => "hostname"}),
do: stream_gen(&Faker.Internet.domain_name/0)
def generate_by_type(%{"type" => "array"} = schema, root, opts),
do: Array.generate(schema, root, opts)

defp generate_string(%{"format" => "ipv4"}), do: stream_gen(&Faker.Internet.ip_v4_address/0)
defp generate_string(%{"format" => "ipv6"}), do: stream_gen(&Faker.Internet.ip_v6_address/0)
defp generate_string(%{"format" => "uri"}), do: stream_gen(&Faker.Internet.url/0)
def generate_by_type(%{"type" => "object"} = schema, root, opts),
do: Object.generate(schema, root, opts)

defp generate_string(%{"format" => "image_uri"}) do
stream_gen(fn ->
w = Enum.random(1..4) * 400
h = Enum.random(1..4) * 400
"https://source.unsplash.com/random/#{w}x#{h}"
end)
end

defp generate_string(%{"enum" => choices}), do: StreamData.member_of(choices)
def generate_by_type(%{"type" => type} = schema, root, opts) when type in ["integer", "number"],
do: Number.generate(schema, root, opts)

defp generate_string(%{"pattern" => regex}),
do: Randex.stream(Regex.compile!(regex), mod: Randex.Generator.StreamData)
def generate_by_type(%{"type" => "null"}, _root, _opts), do: StreamData.constant(nil)

defp generate_string(schema) do
min = schema["minLength"] || 0
max = schema["maxLength"] || 1024
for key <- @string_keys do
def generate_by_type(schema, root, opts) when is_map_key(schema, unquote(key)),
do: schema |> Map.put("type", "string") |> String.generate(root, opts)
end

stream_gen(fn ->
s = Faker.Lorem.word()
for key <- @number_keys do
def generate_by_type(schema, root, opts) when is_map_key(schema, unquote(key)),
do: schema |> Map.put("type", "number") |> Number.generate(root, opts)
end

case String.length(s) do
v when v > max -> String.slice(s, 0, max - 1)
v when v < min -> String.slice(Faker.Lorem.sentence(min), 0, min)
_ -> s
end
end)
for key <- @array_keys do
def generate_by_type(schema, root, opts) when is_map_key(schema, unquote(key)),
do: schema |> Map.put("type", "array") |> Array.generate(root, opts)
end

defp stream_gen(fun) do
StreamData.map(StreamData.constant(nil), fn _ -> fun.() end)
for key <- @object_keys do
def generate_by_type(schema, root, opts) when is_map_key(schema, unquote(key)),
do: schema |> Map.put("type", "object") |> Object.generate(root, opts)
end

def generate_by_type(_schema, _root, _opts), do: JsonDataFaker.Utils.json()
end
143 changes: 143 additions & 0 deletions lib/json_data_faker/generator/array.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
defmodule JsonDataFaker.Generator.Array do
@moduledoc false

alias JsonDataFaker.Utils

def generate(
%{"additionalItems" => false, "items" => [_ | _] = items, "minItems" => min},
_root,
_opts
)
when length(items) < min do
msg = "array minItems greater than number of items choiches with 'additionalItems' false"
raise JsonDataFaker.InvalidSchemaError, message: msg
end

def generate(%{"additionalItems" => false, "items" => [_ | _] = items} = schema, root, opts) do
len = length(items)
maxItems = schema["maxItems"]
maxItems = if(not is_nil(maxItems), do: min(maxItems, len), else: len)

generate_additional_schema(
Utils.json(),
items,
schema["minItems"],
maxItems,
root,
opts
)
end

def generate(%{"additionalItems" => ai, "items" => [_ | _] = items} = schema, root, opts) do
generate_additional_schema(
if(is_boolean(ai),
do: Utils.json(),
else: JsonDataFaker.generate_by_type(ai, root, opts)
),
items,
schema["minItems"],
schema["maxItems"],
root,
opts
)
end

def generate(%{"items" => %{"$ref" => _} = inner_schema} = schema, root, opts) do
schema
|> Map.put("items", Utils.schema_resolve(inner_schema, root))
|> generate(root, opts)
end

def generate(%{"items" => %{"enum" => enum}, "uniqueItems" => true} = schema, _root, _opts)
when length(enum) < 12 do
Utils.stream_gen(fn ->
len = length(enum)

(schema["minItems"] || 1)..min(schema["maxItems"] || 5, len)
|> Enum.flat_map(&Combination.combine(enum, &1))
|> Enum.random()
end)
end

def generate(schema, root, _opts) do
inner_schema = Map.get(schema, "items", %{})

opts =
Enum.reduce(schema, [], fn
{"minItems", min}, acc -> Keyword.put(acc, :min_length, min)
{"maxItems", max}, acc -> Keyword.put(acc, :max_length, max)
_, acc -> acc
end)

opts =
case {Keyword.get(opts, :min_length), Keyword.get(opts, :max_length)} do
{nil, nil} -> Keyword.put(opts, :max_length, 5)
{minlen, nil} -> Keyword.put(opts, :max_length, minlen + 2)
_ -> opts
end

case Map.get(schema, "uniqueItems", false) do
false ->
inner_schema
|> JsonDataFaker.generate_by_type(root, opts)
|> StreamData.list_of(opts)

true ->
inner_schema
|> JsonDataFaker.generate_by_type(root, opts)
|> StreamData.scale(fn size ->
case Keyword.get(opts, :max_length, false) do
false -> size
max -> max * 3
end
end)
|> StreamData.uniq_list_of(opts)
end
end

defp generate_additional_schema(_additional_generator, _items, _min, 0, _root, _opts),
do: StreamData.constant([])

defp generate_additional_schema(_additional_generator, items, _min, max, root, opts)
when is_integer(max) and max <= length(items) do
items
|> Enum.slice(0..(max - 1))
|> Enum.map(&JsonDataFaker.generate_by_type(&1, root, opts))
|> StreamData.fixed_list()
end

defp generate_additional_schema(additional_generator, items, min, max, root, opts) do
items
|> Enum.map(&JsonDataFaker.generate_by_type(&1, root, opts))
|> StreamData.fixed_list()
|> concat_list_generators(
StreamData.list_of(
additional_generator,
list_of_opts(length(items), min, max)
)
)
end

defp list_of_opts(_items_len, nil, nil), do: [max_length: 0]
defp list_of_opts(items_len, min, nil) when min <= items_len, do: [max_length: 0]

# avoid generating too many additional items since the schema can be hard to generate
defp list_of_opts(items_len, min, nil),
do: [min_length: min - items_len, max_length: min - items_len + 2]

defp list_of_opts(items_len, nil, max), do: [max_length: max - items_len]

defp list_of_opts(items_len, min, max) when min <= items_len,
do: [max_length: max - items_len]

defp list_of_opts(items_len, min, max),
do: [min_length: min - items_len, max_length: max - items_len]

defp concat_list_generators(list1_gen, list2_gen) do
StreamData.bind(list1_gen, fn list1 ->
StreamData.bind(list2_gen, fn list2 ->
StreamData.constant(Enum.concat(list1, list2))
end)
end)
end
end
Loading