diff --git a/lib/jake.ex b/lib/jake.ex index a8554ae..2d4ca35 100644 --- a/lib/jake.ex +++ b/lib/jake.ex @@ -9,31 +9,83 @@ defmodule Jake do "string" ] - def gen(%{"anyOf" => options} = spec) when is_list(options) do - Enum.map(options, fn option -> - gen(Map.merge(Map.drop(spec, ["anyOf"]), option)) + def generator(jschema) do + IO.puts(jschema) + map = jschema |> Poison.decode!() + + StreamData.sized(fn size -> + Map.put(%{}, "map", map) |> Map.put("omap", map) |> Map.put("size", 2 * size) |> gen_init() end) - |> StreamData.one_of() end - def gen(%{"type" => type} = spec) when is_binary(type) do - module = String.to_existing_atom("Elixir.Jake.#{String.capitalize(type)}") - apply(module, :gen, [spec]) + def gen_init(schema) do + StreamData.bind( + get_lazy_streamkey(schema), + fn {nmap, nsize} -> + nschema = Map.put(schema, "map", nmap) |> Map.put("size", nsize) + + if nmap["allOf"] || nmap["oneOf"] || nmap["anyOf"] || nmap["not"] do + Jake.Mixed.gen_mixed(nmap, nschema) + else + gen_all(nschema, nmap["enum"], nmap["type"]) + end + |> StreamData.resize(nsize) + end + ) end - def gen(%{"type" => types} = spec) when is_list(types) do - Enum.map(types, fn type -> - gen(%{spec | "type" => type}) - end) - |> StreamData.one_of() + def get_lazy_streamkey(schema) do + {map, _} = + get_in(schema, ["map", "$ref"]) |> Jake.Ref.expand_ref(schema["map"], schema["omap"]) + + StreamData.constant({map, trunc(schema["size"] / 2)}) end - # type not present - def gen(spec) do - Enum.map(@types, fn type -> - Map.put(spec, "type", type) - |> gen() - end) + def gen_all(schema, enum, _type) when enum != nil, do: gen_enum(schema["map"], enum) + + def gen_all(schema, _enum, type) when is_list(type) do + list = for n <- type, do: %{"type" => n} + nmap = schema["map"] |> Map.drop(["type"]) + + for(n <- list, is_map(n), do: Map.put(schema, "map", Map.merge(n, nmap)) |> Jake.gen_init()) |> StreamData.one_of() end + + def gen_all(schema, _enum, type) when type in @types, + do: gen_type(type, schema) + + def gen_all(schema, _enum, type) when type == nil do + Jake.Notype.gen_notype(type, schema) + end + + def gen_type(type, schema) when type == "string" do + map = schema["map"] + Jake.String.gen_string(map, map["pattern"]) + end + + def gen_type(type, schema) when type in ["integer", "number"] do + Jake.Number.gen_number(schema["map"], type) + end + + def gen_type(type, schema) when type == "boolean" do + StreamData.boolean() + end + + def gen_type(type, schema) when type == "null" do + StreamData.constant(nil) + end + + def gen_type(type, schema) when type == "array" do + Jake.Array.gen_array(schema["map"], schema) + end + + def gen_type(type, schema) when type == "object" do + map = schema["map"] + Jake.Object.gen_object(map, map["properties"], schema) + end + + def gen_enum(map, list) do + Enum.filter(list, fn x -> ExJsonSchema.Validator.valid?(map, x) end) + |> StreamData.member_of() + end end diff --git a/lib/jake/array.ex b/lib/jake/array.ex index f369a72..a78083f 100644 --- a/lib/jake/array.ex +++ b/lib/jake/array.ex @@ -1,5 +1,108 @@ defmodule Jake.Array do - def gen(_) do - StreamData.constant([]) + @type_list [ + %{"type" => "integer"}, + %{"type" => "number"}, + %{"type" => "boolean"}, + %{"type" => "string"}, + %{"type" => "null"}, + nil + ] + + @min_items 0 + + @max_items 1000 + + def gen_array(%{"items" => items} = map, schema) do + case items do + item when is_map(item) and map_size(item) == 0 -> + StreamData.constant([]) + + item when is_map(item) -> + gen_list(map, item, schema) + + item when is_list(item) -> + gen_tuple(map, item, schema) + + _ -> + raise "Invalid items in array" + end + end + + def gen_array(map, schema), do: arraytype(map, map["items"], schema) + + def arraytype(map, items, schema) when is_nil(items) do + item = get_one_of(schema) + {min, max} = get_min_max(map) + decide_min_max(map, item, min, max) + end + + def gen_tuple(map, items, schema) do + list = for n <- items, is_map(n), do: Map.put(schema, "map", n) |> Jake.gen_init() + + {min, max} = get_min_max(map) + + case map["additionalItems"] do + x when is_map(x) -> + add_additional_items(list, Jake.gen_init(Map.put(schema, "map", x)), max, min) + + x when (is_boolean(x) and x) or is_nil(x) -> + add_additional_items(list, get_one_of(schema), max, min) + + x when is_boolean(x) and not x and length(list) in min..max -> + StreamData.fixed_list(list) + + _ -> + raise "Invalid items or length of list exceeds specified bounds" + end + end + + def gen_list(map, items, schema) do + {min, max} = get_min_max(map) + item = Map.put(schema, "map", items) |> Jake.gen_init() + decide_min_max(map, item, min, max) + end + + def get_min_max(map) do + min = Map.get(map, "minItems", @min_items) + max = Map.get(map, "maxItems", @max_items) + {min, max} + end + + def decide_min_max(map, item, min, max) + when is_integer(min) and is_integer(max) and min < max do + if map["uniqueItems"] do + StreamData.uniq_list_of(item, min_length: min, max_length: max) + else + StreamData.list_of(item, min_length: min, max_length: max) + end + end + + def decide_min_max(map, item, min, max) do + raise "Bounds of items not well defined" + end + + def get_one_of(schema) do + for(n <- @type_list, is_map(n), do: Map.put(schema, "map", n) |> Jake.gen_init()) + |> StreamData.one_of() + end + + def add_additional_items(olist, additional, max, min) do + StreamData.bind(StreamData.fixed_list(olist), fn list -> + StreamData.bind_filter( + StreamData.list_of(additional), + fn + nlist + when (length(list) + length(nlist)) in min..max -> + {:cont, StreamData.constant(list ++ nlist)} + + nlist + when length(list) in min..max -> + {:cont, StreamData.constant(list)} + + _ -> + :skip + end + ) + end) end end diff --git a/lib/jake/boolean.ex b/lib/jake/boolean.ex deleted file mode 100644 index 3f6fba8..0000000 --- a/lib/jake/boolean.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Jake.Boolean do - def gen(_) do - StreamData.boolean() - end -end diff --git a/lib/jake/integer.ex b/lib/jake/integer.ex deleted file mode 100644 index b6e98be..0000000 --- a/lib/jake/integer.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Jake.Integer do - def gen(spec) do - min = Map.get(spec, "minimum", -9_007_199_254_740_991) - max = Map.get(spec, "maximum", 9_007_199_254_740_991) - StreamData.integer(min..max) - end -end diff --git a/lib/jake/map_util.ex b/lib/jake/map_util.ex new file mode 100644 index 0000000..39bb606 --- /dev/null +++ b/lib/jake/map_util.ex @@ -0,0 +1,33 @@ +# Ref: https://stackoverflow.com/questions/38864001/elixir-how-to-deep-merge-maps +# Ref: https://github.com/activesphere/jake/blob/master/lib/jake/map_util.ex + +defmodule Jake.MapUtil do + def deep_merge(left, right) do + Map.merge(left, right, &deep_resolve/3) + end + + defp deep_resolve(_key, left, nil) do + left + end + + defp deep_resolve(_key, nil, right) do + right + end + + defp deep_resolve(key, left, right) when key == "type" do + case {is_list(left), is_list(right)} do + {x, y} when x and y -> left ++ right + {x, y} when x and not y -> left ++ [right] + {x, y} when not x and y -> [left] ++ right + {x, y} when not x and not y -> [left, right] + end + end + + defp deep_resolve(_key, left, right) when is_map(left) do + Map.merge(left, right) + end + + defp deep_resolve(_key, left, right) when is_list(left) do + left ++ right + end +end diff --git a/lib/jake/mixed.ex b/lib/jake/mixed.ex new file mode 100644 index 0000000..aeda46c --- /dev/null +++ b/lib/jake/mixed.ex @@ -0,0 +1,99 @@ +defmodule Jake.Mixed do + @types [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + + def gen_mixed(%{"anyOf" => options} = map, schema) when is_list(options) do + nmap = Map.drop(map, ["anyOf"]) + + nlist = for(n <- options, is_map(n), do: Map.merge(nmap, n)) + for(n <- nlist, do: Map.put(schema, "map", n) |> Jake.gen_init()) |> StreamData.one_of() + end + + def gen_mixed(%{"oneOf" => options} = map, schema) when is_list(options) do + nmap = Map.drop(map, ["oneOf"]) + + tail_schema = fn tail -> + Enum.reduce(tail, %{}, fn x, acc -> Jake.MapUtil.deep_merge(acc, x) end) + end + + nlist = + for {n, counter} <- Enum.with_index(options) do + hd = Map.put(schema, "map", Map.merge(nmap, n)) |> Jake.gen_init() + tail = List.delete_at(options, counter) |> tail_schema.() + {hd, tail} + end + + try_one_of(nlist, 0) + end + + def gen_mixed(%{"allOf" => options} = map, schema) when is_list(options) do + nmap = Map.drop(map, ["allOf"]) + + map = + Enum.reduce(options, %{}, fn x, acc -> Jake.MapUtil.deep_merge(acc, x) end) + |> Jake.MapUtil.deep_merge(nmap) + + Map.put(schema, "map", map) |> Jake.gen_init() + end + + def gen_mixed(%{"not" => not_schema} = map, schema) when is_map(not_schema) do + nmap = Map.drop(map, ["not"]) + nmap_type = nmap["type"] + + type_val = + if not_schema["type"] do + not_schema["type"] + else + Jake.Notype.gen_notype("return type", schema) + end + + type = if type_val == nil, do: "null", else: type_val + nlist = if is_list(type), do: @types -- type, else: @types -- [type] + + data = + if nmap_type || (is_map(nmap) && map_size(nmap) > 0) do + Map.put(schema, "map", nmap) |> Jake.gen_init() + else + for(n <- nlist, do: Map.put(schema, "map", %{"type" => n}) |> Jake.gen_init()) + |> StreamData.one_of() + end + + StreamData.filter( + data, + fn x -> + if type == "null" do + true + else + not ExJsonSchema.Validator.valid?(not_schema, x) + end + end + ) + end + + def try_one_of(nlist, index) do + data = filter_mutually_exclusive(nlist, index) + + try do + Enum.take(data, 25) + data + rescue + _ -> filter_mutually_exclusive(nlist, index + 1) + end + end + + def filter_mutually_exclusive(nlist, index) do + if index < length(nlist) do + {head, tail_schema} = Enum.at(nlist, index) + StreamData.filter(head, fn hd -> not ExJsonSchema.Validator.valid?(tail_schema, hd) end) + else + raise "oneOf combination not possible" + end + end +end diff --git a/lib/jake/notype.ex b/lib/jake/notype.ex new file mode 100644 index 0000000..44c97c3 --- /dev/null +++ b/lib/jake/notype.ex @@ -0,0 +1,45 @@ +defmodule Jake.Notype do + @prop %{ + "minLength" => "string", + "maxLength" => "string", + "pattern" => "string", + "multipleOf" => "number", + "minimum" => "number", + "maximum" => "number", + "exclusiveMinimum" => "number", + "exclusiveMaximum" => "number", + "items" => "array", + "additionalItems" => "array", + "minItems" => "array", + "maxItems" => "array", + "uniqueItems" => "array", + "properties" => "object", + "patternProperties" => "object", + "additionalProperties" => "object", + "dependencies" => "object", + "required" => "object", + "minProperties" => "object", + "maxProperties" => "object" + } + + def gen_notype(type, schema) do + map = schema["map"] + nmap = for {k, v} <- map, into: %{}, do: {k, v} + nlist = for {k, v} <- map, into: [], do: @prop[k] + + types = + Enum.reduce(nlist, nil, fn + x, acc when not is_nil(x) -> x + x, acc when is_nil(x) -> acc + end) + + if type == nil do + nmap = if not is_nil(types), do: Map.put(nmap, "type", types), else: nmap + + if nmap["type"] || (nmap["$ref"] && nmap["$ref"] != "#"), + do: Map.put(schema, "map", nmap) |> Jake.gen_init() + else + types + end + end +end diff --git a/lib/jake/null.ex b/lib/jake/null.ex deleted file mode 100644 index 9c9ff2c..0000000 --- a/lib/jake/null.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Jake.Null do - def gen(_) do - StreamData.constant(nil) - end -end diff --git a/lib/jake/number.ex b/lib/jake/number.ex index f282e4e..4601dbb 100644 --- a/lib/jake/number.ex +++ b/lib/jake/number.ex @@ -1,5 +1,148 @@ defmodule Jake.Number do - def gen(_spec) do - StreamData.integer() + @num_min -9_007_199_254_740_991 + + @num_max 9_007_199_254_740_991 + + @max_mult 100 + + def get_min_max(map) do + min = Map.get(map, "minimum", @num_min) + max = Map.get(map, "maximum", @num_max) + {min, max} + end + + def gen_number(map, type) do + {min_i, max_i} = get_min_max(map) + {step_left, step_right} = find_step(map, min_i, max_i) + min = findmin(map, @num_min, step_left, type) + max = findmax(map, @num_max, step_right, type) + random_number_gen(map["multipleOf"], type, min, max) + end + + def find_step(map, low, high) when is_number(low) and is_number(high) and low <= high do + mult = map["multipleOf"] + + if is_number(mult) do + step_left = mult * (trunc(low / mult) + 1) - low + step_right = high - mult * trunc(high / mult) + + case {step_left, step_right} do + {x, y} when x == 0 and y == 0 -> + {(mult * (low / mult + 1) - low) / 2, (high - mult * (high / mult - 1)) / 2} + + {x, y} when x == 0 -> + {(mult * (low / mult + 1) - low) / 2, y} + + {x, y} when y == 0 -> + {x, (high - mult * (high / mult - 1)) / 2} + + _ -> + {step_left, step_right} + end + else + {(high - low) / 1000, (high - low) / 1000} + end + end + + def find_step(map, low, high) when true, do: {0.001, 0.001} + + def random_number_gen(mult, type, min, max) when type == "integer" do + new_min = + case min do + x when is_float(x) -> + new_min = trunc(min) + 1 + + x when is_integer(x) -> + new_min = min + end + + new_max = + case max do + x when is_float(x) -> + new_max = trunc(x) + + x when is_integer(x) -> + new_max = max + end + + random_number_int(mult, new_min, new_max) + end + + def random_number_gen(mult, type, min, max) when type == "number" do + case {mult, min, max} do + {m, x, y} when is_integer(x) and is_integer(y) -> + random_number_int(m, x, y) + + {m, x, y} when is_number(m) -> + random_number_float(m * 1.0, x, y) + + _ -> + random_number_float(mult, min, max) + end + end + + def random_number_int(mult, min, max) when is_number(mult) and round(mult) == mult do + getmultipleof(round(mult), min, max) + end + + def random_number_int(mult, min, max) when is_number(mult) and round(mult) != mult do + getmultipleof(mult, min, max) + end + + def random_number_int(mult, min, max) when is_nil(mult) do + StreamData.integer(min..max) + end + + def random_number_float(mult, min, max) when is_float(mult) do + getmultipleof(mult, min, max) + end + + def random_number_float(mult, min, max) when mult == nil do + StreamData.float([{:min, min}, {:max, max}]) + end + + def findmax(map, max, _, type) when type == "integer" do + case {map["maximum"], map["exclusiveMaximum"]} do + {x, y} when is_integer(x) and y -> x - 1 + {x, y} when is_float(x) and y -> trunc(x) + {x, _} when is_number(x) -> x + _ -> max + end + end + + def findmax(map, max, step_right, type) when type == "number" do + case {map["maximum"], map["exclusiveMaximum"]} do + {x, y} when is_number(x) and y -> x - step_right + {x, _} when is_number(x) -> x + _ -> max + end + end + + def findmin(map, min, _, type) when type == "integer" do + case {map["minimum"], map["exclusiveMinimum"]} do + {x, y} when is_integer(x) and y -> x + 1 + {x, y} when is_float(x) and y -> trunc(x) + {x, _} when is_number(x) -> x + _ -> min + end + end + + def findmin(map, min, step_left, type) when type == "number" do + case {map["minimum"], map["exclusiveMinimum"]} do + {x, y} when is_number(x) and y -> x + step_left + {x, _} when is_number(x) -> x + _ -> min + end + end + + def getmultipleof(mult, min, max) when is_integer(mult) do + fn_mod = fn x -> rem(x, mult) == 0 end + StreamData.filter(StreamData.integer(min..max), fn_mod, 100) + end + + def getmultipleof(mult, min, max) when is_float(mult) do + fn_check = fn x, y -> x * y >= min and x * y <= max end + lo = trunc(min / mult) + for(n <- lo..(lo + @max_mult), fn_check.(n, mult), do: n * mult) |> StreamData.member_of() end end diff --git a/lib/jake/object.ex b/lib/jake/object.ex index 9df648f..1db564c 100644 --- a/lib/jake/object.ex +++ b/lib/jake/object.ex @@ -1,13 +1,337 @@ defmodule Jake.Object do - def gen(%{"properties" => properties}) do - Enum.map(properties, fn {name, spec} -> - {name, Jake.gen(spec)} + @min_properties 0 + + @max_properties 1000 + + def get_min_max(map) do + min = Map.get(map, "minProperties", @min_properties) + max = Map.get(map, "maxProperties", @max_properties) + {min, max} + end + + def gen_object(map, properties, schema) when is_nil(properties) do + {min, max} = get_min_max(map) + + if map["patternProperties"] do + nlist = + for {k, v} <- map["patternProperties"], + do: build_and_verify_patterns(k, v, map["patternProperties"], schema) + + merge_patterns(nlist) + else + if map["dependencies"] do + decide_dep_and_properties(map, schema) + else + decide_min_max( + map, + Jake.gen_init(Map.put(schema, "map", %{"type" => "string"})), + StreamData.term(), + min, + max, + schema + ) + end + end + end + + def gen_object(map, properties, schema) when is_map(properties) do + nproperties = check_pattern_properties(map, properties, map["patternProperties"]) + + pmap = + if nproperties != nil and is_list(nproperties) do + nlist = for n <- nproperties, length(n) > 0, do: Enum.fetch!(n, 0) + Enum.reduce(nlist, %{}, fn x, acc -> Map.merge(x, acc) end) + else + properties + end + + fn_not_check = fn k, v -> + if v["not"] != nil and is_map(v["not"]) and map_size(v["not"]) == 0, + do: {"null", "null"}, + else: {k, Jake.gen_init(Map.put(schema, "map", v))} + end + + map = Map.put(map, "properties", pmap) + new_prop = for {k, v} <- pmap, into: %{}, do: fn_not_check.(k, v) + new_prop = if new_prop["null"] == "null", do: Map.drop(new_prop, ["null"]), else: new_prop + + req = + if map["required"] do + for n <- map["required"], into: %{}, do: {n, Map.get(new_prop, n)} + end + + non_req = + if is_map(req) and map_size(req) > 0 do + for {k, v} <- new_prop, req[k] == nil, into: %{}, do: {k, v} + end + + if is_nil(req) or map_size(req) == 0 do + check_additional_properties(map, 0, req, non_req, new_prop, schema) + else + check_additional_properties(map, Map.size(req), req, non_req, new_prop, schema) + end + end + + def merge_patterns(nlist) do + merge_maps = fn list -> Enum.reduce(list, %{}, fn x, acc -> Map.merge(acc, x) end) end + + StreamData.bind(StreamData.fixed_list(nlist), fn list -> + StreamData.constant(merge_maps.(list)) + end) + end + + def build_and_verify_patterns(key, value, pprop, schema) do + pprop_schema = %{"patternProperties" => pprop} + # IO.inspect(pprop_schema) + nkey = Randex.stream(~r/#{key}/, mod: Randex.Generator.StreamData) + nval = Map.put(schema, "map", value) |> Jake.gen_init() + + StreamData.bind(nkey, fn k -> + StreamData.bind_filter( + nval, + fn v -> + result = ExJsonSchema.Validator.valid?(pprop_schema, %{k => v}) + if result, do: {:cont, StreamData.constant(%{k => v})}, else: :skip + end, + 100 + ) + end) + end + + def gen_with_no_prop(map, schema) do + {min, max} = get_min_max(map) + + Map.put(schema, "map", %{"type" => "string"}) + |> Jake.gen_init() + |> StreamData.map_of(StreamData.term(), min_length: min, max_length: max) + end + + def decide_dep_and_properties(map, schema) do + dep = map["dependencies"] + + list_with_map = + for {k, v} <- dep do + # IO.inspect(v) + + if is_list(v) do + item = %{k => StreamData.term()} + nmap = for i <- v, into: %{}, do: {i, StreamData.term()} + Map.merge(item, nmap) + else + prop_list = for {kl, vl} <- v["properties"], do: kl + {k, prop_list, v["properties"]} + end + end + + resolve_dep(map, list_with_map, schema) + end + + def resolve_dep(map, list_with_map, schema) do + if is_map(List.first(list_with_map)) do + properties = Enum.reduce(list_with_map, %{}, fn x, acc -> Map.merge(acc, x) end) + check_additional_properties(map, 0, nil, nil, properties, schema) + else + dependencies = + for({k, prop_list, prop_map} <- list_with_map, do: %{k => prop_list}) + |> Enum.reduce(%{}, fn x, acc -> Map.merge(x, acc) end) + + # IO.inspect(dependencies) + + properties = + for({k, prop_list, prop_map} <- list_with_map, do: prop_map) + |> Enum.reduce(%{}, fn x, acc -> Map.merge(x, acc) end) + + map = Map.put(map, "properties", properties) |> Map.put("dependencies", dependencies) + # IO.inspect(map) + gen_object(map, properties, schema) + end + end + + def check_pattern_properties(map, properties, pprop) do + if pprop do + for {k, v} <- properties do + for {key, value} <- pprop, + Regex.match?(~r/#{key}/, k), + do: Map.put(properties, k, Map.merge(v, value)) + end + else + properties + end + end + + def bind_function(new_prop, additional, y, z) do + prop = + if is_list(new_prop) do + StreamData.one_of(new_prop) + else + StreamData.optional_map(new_prop) + end + + # IO.inspect(prop) + + StreamData.bind(prop, fn mapn -> + StreamData.bind_filter( + additional, + fn + nmap + when (map_size(mapn) + map_size(nmap)) in y..z -> + {:cont, StreamData.constant(Map.merge(mapn, nmap))} + + nmap when map_size(mapn) in y..z -> + {:cont, StreamData.constant(mapn)} + + nmap when is_map(nmap) -> + :skip + end + ) end) - |> Enum.into(%{}) - |> StreamData.fixed_map() end - def gen(_) do - StreamData.fixed_map(%{}) + def create_dep_list_map(new_prop, dep) do + dep_list = for {k, v} <- dep, do: k + {dep_map, non_dep_map} = Map.split(new_prop, dep_list) + + list_with_map = + for {k, v} <- dep do + item = %{k => get_in(new_prop, [k])} + nmap = for i <- v, into: %{}, do: {i, get_in(new_prop, [i])} + StreamData.fixed_map(Map.merge(item, nmap)) + end + + list_with_map ++ [StreamData.optional_map(non_dep_map)] + end + + def bind_function_req(non_req, req, y, z) + when is_map(non_req) or is_nil(non_req) or is_list(non_req) do + prop = + if is_list(non_req) do + StreamData.one_of(non_req) + else + StreamData.optional_map(non_req) + end + + StreamData.bind_filter( + StreamData.fixed_map(req), + fn + mapn when is_map(non_req) or is_list(non_req) -> + {:cont, + StreamData.bind_filter(prop, fn + nmap + when (map_size(mapn) + map_size(nmap)) in y..z -> + {:cont, StreamData.constant(Map.merge(mapn, nmap))} + + nmap + when map_size(mapn) in y..z -> + {:cont, StreamData.constant(mapn)} + + _ -> + :skip + end)} + + mapn + when is_nil(non_req) and map_size(mapn) in y..z -> + {:cont, StreamData.constant(mapn)} + + mapn when is_nil(non_req) -> + :skip + end + ) + end + + def bind_function_req(non_req, req, y, z, add) when not is_nil(non_req) do + StreamData.bind( + StreamData.fixed_map(req), + fn + mapn when is_map(non_req) -> + StreamData.bind_filter(non_req, fn + nmap + when (map_size(mapn) + map_size(nmap)) in y..z -> + {:cont, StreamData.constant(Map.merge(mapn, nmap))} + + nmap + when map_size(mapn) in y..z -> + {:cont, StreamData.constant(mapn)} + + _ -> + :skip + end) + end + ) + end + + def check_additional_properties(map, req_size, req, _non_req, new_prop, schema) + when is_nil(req) or req_size == 0 do + {min, max} = get_min_max(map) + + case {map["additionalProperties"], min, max} do + {x, y, z} when is_nil(x) or (is_boolean(x) and x) -> + additional = gen_with_no_prop(map, schema) + + check_dependencies(map, new_prop) + |> bind_function(additional, y, z) + + {x, y, z} when is_boolean(x) and not x -> + val = check_dependencies(map, new_prop) + + prop = + if is_list(val) do + StreamData.one_of(val) + else + StreamData.optional_map(val) + end + + StreamData.filter(prop, fn nmap -> map_size(nmap) in y..z end) + + {x, y, z} when is_map(x) -> + obj = Map.put(schema, "map", x) |> Jake.gen_init() + key = Map.put(schema, "map", %{"type" => "string"}) |> Jake.gen_init() + + check_dependencies(map, new_prop) + |> bind_function(StreamData.map_of(key, obj), y, z) + end + end + + def check_additional_properties(map, req_size, req, non_req, new_prop, schema) + when req_size > 0 do + {min, max} = get_min_max(map) + + case {map["additionalProperties"], min, max} do + {x, y, z} when is_nil(x) or (is_boolean(x) and x) -> + additional = gen_with_no_prop(map, schema) + + check_dependencies(map, non_req) + |> bind_function(additional, y, z) + |> bind_function_req(req, y, z, "additional") + + {x, y, z} when is_boolean(x) and not x -> + check_dependencies(map, non_req) + |> bind_function_req(req, y, z) + + {x, y, z} when is_map(x) -> + obj = Map.put(schema, "map", x) |> Jake.gen_init() + key = Map.put(schema, "map", %{"type" => "string"}) |> Jake.gen_init() + val1 = StreamData.map_of(key, obj, min_length: y, max_length: z) + + check_dependencies(map, non_req) + |> bind_function(val1, y, z) + |> bind_function_req(req, y, z, "additional") + end + end + + def check_dependencies(map, non_req) do + if map["dependencies"] do + create_dep_list_map(non_req, map["dependencies"]) + else + non_req + end + end + + def decide_min_max(map, key, value, min, max, schema) + when is_integer(min) and is_integer(max) and min <= max do + if map["additionalProperties"] != nil do + gen_object(map, %{}, schema) + else + StreamData.map_of(key, value, min_length: min, max_length: max) + end end end diff --git a/lib/jake/ref.ex b/lib/jake/ref.ex new file mode 100644 index 0000000..eba075e --- /dev/null +++ b/lib/jake/ref.ex @@ -0,0 +1,97 @@ +defmodule Jake.Ref do + def expand_ref(ref, map, _omap) + when is_nil(ref) or is_map(ref) do + {map, false} + end + + def expand_ref(ref, map, omap) + when ref == "#" do + nmap = Map.drop(map, ["$ref"]) |> Map.merge(omap) + {nmap, true} + end + + def expand_ref(ref, map, omap) when is_binary(ref) do + nmap = Map.drop(map, ["$ref"]) + uri = URI.decode(ref) + + ref_map = + if String.starts_with?(uri, "http") do + process_http_path(uri) + else + process_local_path(uri) |> get_head_list_path(omap) + end + + nmap = + if ref_map != nil do + Map.merge(nmap, ref_map) + else + nmap + end + + {nmap, true} + end + + def get_head_list_path(path_list, omap) do + {head, tail} = Enum.split(path_list, -1) + + head_path = + if length(head) > 0 do + get_in(omap, head) + else + get_in(omap, path_list) + end + + tail = + if is_list(head_path) do + Enum.fetch!(tail, 0) + else + nil + end + + if tail != nil and is_numeric(tail) do + {index, ""} = Integer.parse(tail) + Enum.fetch!(head_path, index) + else + get_in(omap, path_list) + end + end + + def process_http_path(url) do + [url, local] = + if String.contains?(url, "#/") do + String.split(url, "#/") + else + [url, nil] + end + + IO.inspect({url, local}) + {:ok, {{_, 200, _}, _, schema}} = :httpc.request(:get, {to_charlist(url), []}, [], []) + jschema = Poison.decode!(schema) + + if is_nil(local) do + jschema + else + process_local_path(local) |> get_head_list_path(jschema) + end + end + + def process_local_path(path) do + str = + String.replace(path, "~0", "~") + |> String.replace("#/", "", global: false) + + if String.contains?(str, "~1") do + strlist = String.split(str, "/") + for n <- strlist, do: String.replace(n, "~1", "/") + else + String.split(str, "/") + end + end + + def is_numeric(str) do + case Integer.parse(str) do + {_num, ""} -> true + _ -> false + end + end +end diff --git a/lib/jake/string.ex b/lib/jake/string.ex index 68eb689..58bdbf8 100644 --- a/lib/jake/string.ex +++ b/lib/jake/string.ex @@ -1,21 +1,25 @@ defmodule Jake.String do - def gen(spec) do - options = [] + @strlen_min 1 - options = - if min_length = Map.get(spec, "minLength") do - Keyword.put(options, :min_length, min_length) - else - options - end + @strlen_max 100 - options = - if max_length = Map.get(spec, "maxLength") do - Keyword.put(options, :max_length, max_length) - else - options - end + def find_min_max(map) do + min = Map.get(map, "minLength", @strlen_min) + max = Map.get(map, "maxLength", @strlen_max) + {min, max} + end + + def gen_string(map, pattern) when is_nil(pattern) do + {min, max} = find_min_max(map) + StreamData.string(:alphanumeric, [{:max_length, max}, {:min_length, min}]) + end + + def gen_string(map, pattern) when is_binary(pattern) do + {min, max} = find_min_max(map) + pat = Randex.stream(~r/#{pattern}/, mod: Randex.Generator.StreamData) - StreamData.string(:ascii, options) + if min <= max do + StreamData.filter(pat, fn x -> String.length(x) in min..max end) + end end end diff --git a/mix.exs b/mix.exs index 4c4c93e..f30ad15 100644 --- a/mix.exs +++ b/mix.exs @@ -20,8 +20,10 @@ defmodule Jake.MixProject do defp deps do [ {:stream_data, "~> 0.4"}, - {:ex_json_schema, "~> 0.5"}, - {:jason, "~> 1.1", only: :test} + {:jason, "~> 1.1", only: :test}, + {:randex, git: "https://github.com/ananthakumaran/randex", tag: "v0.4.0"}, + {:poison, "~>3.1"}, + {:ex_json_schema, "~> 0.5.4"} ] end end diff --git a/mix.lock b/mix.lock index ef9688e..da58a0e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,7 @@ %{ "ex_json_schema": {:hex, :ex_json_schema, "0.5.7", "14a1bcd716e432badb19e5f54fd0f3f0be47b34d1b5957944702be90d66a6cf6", [:mix], [], "hexpm"}, "jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "randex": {:git, "https://github.com/ananthakumaran/randex", "9725cb0d5d7d0f7a1e951c8abdf27d70a3a5f38b", [tag: "v0.4.0"]}, "stream_data": {:hex, :stream_data, "0.4.2", "fa86b78c88ec4eaa482c0891350fcc23f19a79059a687760ddcf8680aac2799b", [:mix], [], "hexpm"}, } diff --git a/test/jake_test.exs b/test/jake_test.exs index a81268a..482c9e0 100644 --- a/test/jake_test.exs +++ b/test/jake_test.exs @@ -1,49 +1,277 @@ defmodule JakeTest do - alias ExJsonSchema.Validator - use ExUnit.Case doctest Jake - @schemas [ - %{ - "type" => "object", - "properties" => %{ - "foo" => %{ - "type" => "string" - } - } - } - ] - - test "valid" do - Enum.each(@schemas, &verify/1) - end - + @tag timeout: 300_000 test "suite" do for path <- [ "draft4/type.json", - "draft4/anyOf.json" + "draft4/anyOf.json", + "draft4/required.json", + "draft4/allOf.json", + "draft4/enum.json", + "draft4/minimum.json", + "draft4/maximum.json", + "draft4/items.json", + "draft4/minItems.json", + "draft4/maxItems.json", + "draft4/uniqItems.json", + "draft4/pattern.json", + "draft4/minLength.json", + "draft4/maxLength.json", + "draft4/maxProperties.json", + "draft4/minProperties.json", + "draft4/additionalItems.json", + "draft4/additionalProperties.json", + "draft4/multipleOf.json", + "draft4/properties.json", + "draft4/patternProperties.json", + "draft4/dependencies.json", + "draft4/default.json", + "draft4/oneOf.json", + "draft4/not.json" ] do Path.wildcard("test_suite/tests/#{path}") - |> Enum.map(fn path -> File.read!(path) |> Jason.decode!() end) + |> Enum.map(fn path -> File.read!(path) |> Poison.decode!() end) |> Enum.concat() |> Enum.map(fn %{"schema" => schema} -> verify(schema) end) end end def verify(schema) do - Jake.gen(schema) - |> Enum.take(100) - |> Enum.each(fn value -> - result = Validator.validate(schema, value) + Poison.encode!(schema) |> test_generator() + end + + def test_generator(jschema) do + gen = Jake.generator(jschema) + schema = Poison.decode!(jschema) + IO.inspect(Enum.take(gen, 3)) - if result != :ok do + Enum.take(gen, 100) + |> Enum.each(fn val -> + result = ExJsonSchema.Validator.valid?(schema, val) + + if result == false do flunk( - "Invalid data: \nschema: #{inspect(schema)}\ngenerated: #{inspect(value)}\nerror: #{ + "Invalid data: \nschema: #{inspect(schema)}\ngenerated: #{inspect(val)}\nerror: #{ inspect(result) }" ) end end) end + + test "test anyOf" do + jschema = ~s({"anyOf": [{"type": "object"}, {"type": "array"}]}) + assert test_generator(jschema) + end + + test "test allOf" do + jschema = ~s({"allOf": [{"type": "integer"}, {"maximum": 255}]}) + assert test_generator(jschema) + end + + test "test not" do + jschema = ~s({"not": {"type": "integer"}}) + assert test_generator(jschema) + end + + test "test oneOf" do + jschema = ~s({"type": "integer", "minimum": 29, "oneOf": [{"maximum": 255}, {"minimum": 25}]}) + assert test_generator(jschema) + end + + test "test type both integer string" do + jschema = ~s({"type": ["string", "integer"], "maxLength": 5, "minLength": 1, "maximum": 29}) + assert test_generator(jschema) + end + + test "test object with no properties" do + jschema = ~s({"type": "object"}) + assert test_generator(jschema) + end + + test "test object with properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "required":["name", "age"]}) + + assert test_generator(jschema) + end + + test "test object with required properties and dependencies" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}}) + + assert test_generator(jschema) + end + + test "test object with required properties, dependencies and no additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":false}) + + assert test_generator(jschema) + end + + test "test object with required properties, dependencies and map of additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":{"type":"boolean"}}) + + assert test_generator(jschema) + end + + test "test object with dependencies and map of additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":{"type":"boolean"}}) + + assert test_generator(jschema) + end + + test "test object with dependencies and no additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":false}) + + assert test_generator(jschema) + end + + test "test object with dependencies and additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":true}) + + assert test_generator(jschema) + end + + test "test object with properties minmax" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": {"type": "integer"}, "minProperties":1, "maxProperties": 50}) + + assert test_generator(jschema) + end + + test "test object with properties required" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": false, "minProperties":1, "maxProperties": 5, "required": ["age", "name"]}) + + assert test_generator(jschema) + end + + test "test object with additional properties and required" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": {"type": "integer"}, "minProperties":2, "maxProperties": 5, "required": ["age"]}) + + assert test_generator(jschema) + end + + test "test array items" do + jschema = + ~s({"type": "array", "items" : [{"type": "integer"}, {"type": "string", "maxLength": 10}, {"type": "boolean"}], "additionalItems": {"type": "boolean"} }) + + assert test_generator(jschema) + end + + test "test array item with bounds" do + jschema = + ~s({"type": "array", "items" : {"type": "string", "maxLength": 10, "minLength":5}, "minItems": 1, "maxItems": 100 }) + + assert test_generator(jschema) + end + + test "test array single item" do + jschema = + ~s({"type": "array", "items" : {"type": "string", "maxLength": 10}, "additionalItems":true}) + + assert test_generator(jschema) + end + + test "test string" do + jschema = ~s({"type": "string", "maxLength": 5, "minLength": 1}) + assert test_generator(jschema) + end + + test "test string regex" do + jschema = ~s({"type": "string", "pattern": "[a-zA-Z0-9_]{5,10}@abc[.]\(org|com|in\)"}) + assert test_generator(jschema) + end + + test "test string regex with length" do + jschema = + ~s({"type": "string", "pattern": "[a-zA-Z0-9_]{5,10}@abc[.]\(org|com|in\)", "minLength": 5, "maxLength": 20}) + + assert test_generator(jschema) + end + + test "test integer" do + jschema = ~s({"type": "integer", "maximum": 111, "minimum": -87, "multipleOf": 9}) + assert test_generator(jschema) + end + + test "test integer excl" do + jschema = + ~s({"type": "integer", "maximum": 120, "minimum": -87, "multipleOf": 6, "exclusiveMaximum": true}) + + assert test_generator(jschema) + end + + test "test number" do + jschema = ~s({"type": "number", "maximum": 7.5, "minimum": 3.6}) + assert test_generator(jschema) + end + + test "test number multiple" do + jschema = ~s({"type": "number", "maximum": 9.7, "minimum": 3.2, "multipleOf": 1.5}) + assert test_generator(jschema) + end + + test "test number multiple again" do + jschema = ~s({"type": "number", "maximum": 9.8, "minimum": -3.6, "multipleOf": 2}) + assert test_generator(jschema) + end + + test "test fraction" do + jschema = ~s({"type": "number", "maximum": 9.7, "minimum": 9.65, "multipleOf": 0.04}) + assert test_generator(jschema) + end + + test "test fraction excl" do + jschema = + ~s({"type": "number", "maximum": 8.1, "minimum": 7.79, "multipleOf": 0.3, "exclusiveMaximum": true, "exclusiveMinimum": true}) + + assert test_generator(jschema) + end + + test "test number negative" do + jschema = ~s({"type": "number", "maximum": -3, "minimum": -9}) + assert test_generator(jschema) + end + + test "test integer enum" do + jschema = ~s({"type": "integer", "enum": [30, -11, 18, 75, 99, -65, null, "abc"]}) + assert test_generator(jschema) + end + + test "test only enum" do + jschema = ~s({"enum": [1, 2, "hello", -3, "world"]}) + assert test_generator(jschema) + end + + test "test only enum with mix types" do + jschema = + ~s({"type": ["integer", "string"], "enum": [1, 2, "hello", -3, "world", null, true]}) + + assert test_generator(jschema) + end + + test "test boolean" do + jschema = ~s({"type": "boolean"}) + assert test_generator(jschema) + end + + test "test null" do + jschema = ~s({"type": "null"}) + assert test_generator(jschema) + end + + test "test notype" do + jschema = ~s({"maxLength": 20, "minLength": 10, "minItems": 3}) + assert test_generator(jschema) + end end diff --git a/test/jake_test_property.exs b/test/jake_test_property.exs new file mode 100644 index 0000000..817ebc8 --- /dev/null +++ b/test/jake_test_property.exs @@ -0,0 +1,288 @@ +defmodule JakeTestProperty do + use ExUnitProperties + use ExUnit.Case + doctest Jake + + @tag timeout: 300_000 + property "suite" do + for path <- [ + "draft4/type.json", + "draft4/anyOf.json", + "draft4/required.json", + "draft4/allOf.json", + "draft4/enum.json", + "draft4/minimum.json", + "draft4/maximum.json", + "draft4/items.json", + "draft4/minItems.json", + "draft4/maxItems.json", + "draft4/uniqItems.json", + "draft4/pattern.json", + "draft4/minLength.json", + "draft4/maxLength.json", + "draft4/maxProperties.json", + "draft4/minProperties.json", + "draft4/additionalItems.json", + "draft4/additionalProperties.json", + "draft4/multipleOf.json", + "draft4/properties.json", + "draft4/patternProperties.json", + "draft4/dependencies.json", + "draft4/default.json", + "draft4/oneOf.json", + "draft4/not.json" + ] do + Path.wildcard("test_suite/tests/#{path}") + |> Enum.map(fn path -> File.read!(path) |> Poison.decode!() end) + |> Enum.concat() + |> Enum.map(fn %{"schema" => schema} -> verify(schema) end) + end + end + + def verify(schema) do + Poison.encode!(schema) |> test_generator_property() + end + + def test_generator_property(jschema) do + gen = Jake.generator(jschema) + schema = Poison.decode!(jschema) + IO.inspect(Enum.take(gen, 3)) + + check all a <- gen do + assert ExJsonSchema.Validator.valid?(schema, a) + end + end + + property "test anyOf" do + jschema = ~s({"anyOf": [{"type": "object"}, {"type": "array"}]}) + test_generator_property(jschema) + end + + property "test allOf" do + jschema = ~s({"allOf": [{"type": "integer"}, {"maximum": 255}]}) + test_generator_property(jschema) + end + + property "test not" do + jschema = ~s({"not": {"type": "integer"}}) + test_generator_property(jschema) + end + + property "test not string foo" do + jschema = + ~s({"type": "object", "properties": {"foo":{"not": {"type":"string"}}, "bar": {"type":"integer"}}}) + + test_generator_property(jschema) + end + + property "test forbidden foo" do + jschema = ~s({"type": "object", "properties": {"foo":{"not": {}}, "bar": {"type":"integer"}}}) + test_generator_property(jschema) + end + + property "test oneOf" do + jschema = ~s({"type": "integer", "minimum": 29, "oneOf": [{"maximum": 255}, {"minimum": 25}]}) + test_generator_property(jschema) + end + + property "test type both integer string" do + jschema = ~s({"type": ["string", "integer"], "maxLength": 5, "minLength": 1, "maximum": 29}) + test_generator_property(jschema) + end + + property "test object with no properties" do + jschema = ~s({"type": "object"}) + test_generator_property(jschema) + end + + property "test object with properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "required":["name", "age"]}) + + test_generator_property(jschema) + end + + property "test object with required properties and dependencies" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}}) + + test_generator_property(jschema) + end + + property "test object with required properties, dependencies and no additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":false}) + + test_generator_property(jschema) + end + + property "test object with required properties, dependencies and map of additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "required":["name"], "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":{"type":"boolean"}}) + + test_generator_property(jschema) + end + + property "test object with dependencies and map of additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":{"type":"boolean"}}) + + test_generator_property(jschema) + end + + property "test object with dependencies and no additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":false}) + + test_generator_property(jschema) + end + + property "test object with dependencies and additional properties" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}, "dt":{"type":"string", "pattern":"[0][1-9]|[1-2][0-9]|[3][0-1]"}, "address": {"type":"string"}}, "dependencies":{"dt":["age"], "age":["dt"]}, "additionalProperties":true}) + + test_generator_property(jschema) + end + + property "test object with properties minmax" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": {"type": "integer"}, "minProperties":1, "maxProperties": 50}) + + test_generator_property(jschema) + end + + property "test object with properties required" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": false, "minProperties":1, "maxProperties": 5, "required": ["age", "name"]}) + + test_generator_property(jschema) + end + + property "test object with additional properties and required" do + jschema = + ~s({"type": "object", "properties": {"name":{"type":"string", "maxLength": 10}, "age":{"type": "integer", "minimum": 1, "maximum": 125}}, "additionalProperties": {"type": "integer"}, "minProperties":2, "maxProperties": 5, "required": ["age"]}) + + test_generator_property(jschema) + end + + property "test array items" do + jschema = + ~s({"type": "array", "items" : [{"type": "integer"}, {"type": "string", "maxLength": 10}, {"type": "boolean"}], "additionalItems": {"type": "boolean"} }) + + test_generator_property(jschema) + end + + property "test array item with bounds" do + jschema = + ~s({"type": "array", "items" : {"type": "string", "maxLength": 10, "minLength":5}, "minItems": 1, "maxItems": 100 }) + + test_generator_property(jschema) + end + + property "test array single item" do + jschema = + ~s({"type": "array", "items" : {"type": "string", "maxLength": 10}, "additionalItems":true}) + + test_generator_property(jschema) + end + + property "test string" do + jschema = ~s({"type": "string", "maxLength": 5, "minLength": 1}) + test_generator_property(jschema) + end + + property "test string regex" do + jschema = ~s({"type": "string", "pattern": "[a-zA-Z0-9_]{5,10}@abc[.]\(org|com|in\)"}) + test_generator_property(jschema) + end + + property "test string regex with length" do + jschema = + ~s({"type": "string", "pattern": "[a-zA-Z0-9_]{5,10}@abc[.]\(org|com|in\)", "minLength": 5, "maxLength": 20}) + + test_generator_property(jschema) + end + + property "test integer" do + jschema = ~s({"type": "integer", "maximum": 111, "minimum": -87, "multipleOf": 9}) + test_generator_property(jschema) + end + + property "test integer excl" do + jschema = + ~s({"type": "integer", "maximum": 120, "minimum": -87, "multipleOf": 6, "exclusiveMaximum": true}) + + test_generator_property(jschema) + end + + property "test number" do + jschema = ~s({"type": "number", "maximum": 7.5, "minimum": 3.6}) + test_generator_property(jschema) + end + + property "test number multiple" do + jschema = ~s({"type": "number", "maximum": 9.7, "minimum": 3.2, "multipleOf": 1.5}) + test_generator_property(jschema) + end + + property "test number multiple again" do + jschema = ~s({"type": "number", "maximum": 9.8, "minimum": -3.6, "multipleOf": 2}) + test_generator_property(jschema) + end + + property "test fraction" do + jschema = ~s({"type": "number", "maximum": 9.7, "minimum": 9.65, "multipleOf": 0.04}) + test_generator_property(jschema) + end + + property "test fraction excl" do + jschema = + ~s({"type": "number", "maximum": 8.1, "minimum": 7.79, "multipleOf": 0.3, "exclusiveMaximum": true, "exclusiveMinimum": true}) + + test_generator_property(jschema) + end + + property "test number negative" do + jschema = ~s({"type": "number", "maximum": -3, "minimum": -9}) + test_generator_property(jschema) + end + + property "test integer enum" do + jschema = ~s({"type": "integer", "enum": [30, -11, 18, 75, 99, -65, null, "abc"]}) + test_generator_property(jschema) + end + + property "test only enum" do + jschema = ~s({"enum": [1, 2, "hello", -3, "world"]}) + test_generator_property(jschema) + end + + property "test only enum with mix types" do + jschema = + ~s({"type": ["integer", "string"], "enum": [1, 2, "hello", -3, "world", null, true]}) + + test_generator_property(jschema) + end + + property "test enum with constraints" do + jschema = + ~s({"type": ["integer", "string"], "enum": [1, 2, "four", "hello", -3, "worlds", null, true], "minimum": -1, "minLength": 5}) + + test_generator_property(jschema) + end + + property "test boolean" do + jschema = ~s({"type": "boolean"}) + test_generator_property(jschema) + end + + property "test null" do + jschema = ~s({"type": "null"}) + test_generator_property(jschema) + end + + property "test notype" do + jschema = ~s({"maxLength": 20, "minLength": 10, "minItems": 3}) + test_generator_property(jschema) + end +end diff --git a/test/jake_test_ref.ex b/test/jake_test_ref.ex new file mode 100644 index 0000000..d58c317 --- /dev/null +++ b/test/jake_test_ref.ex @@ -0,0 +1,163 @@ +defmodule JakeTestRef do + use ExUnitProperties + use ExUnit.Case + doctest Jake + + def test_generator_property(jschema) do + gen = Jake.generator(jschema) + schema = Poison.decode!(jschema) + IO.inspect(Enum.take(gen, 3)) + + check all a <- gen do + assert ExJsonSchema.Validator.valid?(schema, a) + end + end + + property "test ref simple" do + jschema = ~s({"properties": { + "foo": {"type": "integer"}, + "bar": {"$ref": "#/properties/foo"} + }}) + test_generator_property(jschema) + end + + property "test ref escape pointer" do + jschema = ~s({"tilda~field": {"type": "integer"}, + "slash/field": {"type": "integer"}, + "percent%field": {"type": "integer"}, + "properties": { + "tilda": {"$ref": "#/tilda~0field"}, + "slash": {"$ref": "#/slash~1field"}, + "percent": {"$ref": "#/percent%25field"} + }}) + test_generator_property(jschema) + end + + property "test ref nested schema" do + jschema = ~s({"definitions": { + "a": {"type": "integer"}, + "b": {"$ref": "#/definitions/a"}, + "c": {"$ref": "#/definitions/b"} + }, + "$ref": "#/definitions/c"}) + test_generator_property(jschema) + end + + property "test ref overrides any sibling keywords" do + jschema = ~s({"definitions": { + "reffed": { + "type": "array" + } + }, + "properties": { + "foo": { + "$ref": "#/definitions/reffed", + "maxItems": 2 + } + }}) + test_generator_property(jschema) + end + + property "test ref not reference" do + jschema = ~s({"properties": { + "$ref": {"type": "string"} + }}) + test_generator_property(jschema) + end + + property "test ref array index" do + jschema = ~s({"items": [ + {"type": "integer"}, + {"$ref": "#/items/0"} + ]}) + test_generator_property(jschema) + end + + property "test ref root" do + jschema = ~s({"properties": { + "bar" : {"type":"integer"}, + "foo": {"$ref": "#"} + }, + "additionalProperties": false}) + test_generator_property(jschema) + end + + property "test ref simple recursive" do + jschema = ~s({"properties": { + "foo_bar" : {"type":"integer"}, + "bar" : {"$ref": "#/properties"}, + "foo": {"$ref": "#/properties/bar"} + }, + "additionalProperties": false}) + test_generator_property(jschema) + end + + property "test ref complex recursive" do + jschema = ~s({"definitions": { + "person": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "children": { + "type": "array", + "items": { "$ref": "#/definitions/person" } + + } + }, "required": ["name"], "additionalProperties": false + } + }, + "type": "object", + "properties": { + "person": { "$ref": "#/definitions/person" } + }, "required": ["person"], "additionalProperties": false + }) + test_generator_property(jschema) + end + + property "test ref complex recursive no required" do + jschema = ~s({"definitions": { + "person": { + "type": "object", + "properties": { + "name": { "type": "string", "minLength": 5 }, + "children": { + "type": "array", + "items": { "$ref": "#/definitions/person" } + + } + }, "additionalProperties": false + } + }, + "type": "object", + "properties": { + "person": { "$ref": "#/definitions/person" } + }, "additionalProperties": false + }) + test_generator_property(jschema) + end + + property "test complex ref" do + jschema = ~s({ "definitions": { + "address": { + "type": "object", + "additionalProperties": false, + "properties": { + "street_address": { "type": "string" }, + "city": { "type": "string" }, + "state": { "type": "string" } + }, + "required": ["street_address", "city", "state"] + } + }, + + "type": "object", + + "properties": { + "billing_address": { "$ref": "#/definitions/address" }, + "shipping_address": { "$ref": "#/definitions/address" } + }, + "additionalProperties": false + }) + test_generator_property(jschema) + end +end