diff --git a/lib/jake/object.ex b/lib/jake/object.ex index dee02a7..2b6e1f7 100644 --- a/lib/jake/object.ex +++ b/lib/jake/object.ex @@ -1,9 +1,41 @@ defmodule Jake.Object do alias Jake.StreamUtil alias Jake.Context + alias Jake.MapUtil def gen(%Context{child: spec} = context) do properties = Map.get(spec, "properties", %{}) + + if map_size(properties) == 0 and spec["patternProperties"] do + gen_pattern_properties(context) + else + new_child = Map.put(spec, "properties", properties) + gen_regular_object(%{context | child: new_child}) + end + end + + defp gen_pattern_properties( + %Context{child: %{"patternProperties" => patternProperties} = _spec} = context + ) do + nlist = + for {k, v} <- patternProperties, + do: build_and_verify_patterns(k, v, patternProperties, context) + + merge_patterns(nlist) + end + + defp gen_regular_object(%Context{child: %{"properties" => properties} = spec} = context) do + nproperties = check_pattern_properties(spec, properties, spec["patternProperties"]) + + properties = + if is_list(nproperties) and length(nproperties) > 0 do + Enum.reduce(nproperties, %{}, fn x, acc -> MapUtil.deep_merge(x, acc) end) + else + properties + end + + spec = Map.put(spec, "properties", properties) + context = %{context | child: spec} required = Map.get(spec, "required", []) all_properties = Map.keys(properties) optional = Enum.filter(all_properties, &(!Enum.member?(required, &1))) @@ -26,6 +58,52 @@ defmodule Jake.Object do |> StreamUtil.merge(StreamData.fixed_map(as_map(properties, required, context))) end + defp check_pattern_properties(_spec, properties, pprop) do + if pprop do + pprop_list = Map.to_list(pprop) + + Map.to_list(properties) + |> Enum.map(fn {k, v} -> + Enum.map(pprop_list, fn {key, value} -> + if Regex.match?(~r/#{key}/, k) do + Map.put(properties, k, Map.merge(v, value)) + end + end) + end) + |> List.flatten() + |> Enum.uniq() + |> List.delete(nil) + else + properties + end + end + + defp 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 + + defp build_and_verify_patterns(key, value, pprop, context) do + pprop_schema = %{"patternProperties" => pprop} + # IO.inspect(pprop_schema) + nkey = Randex.stream(~r/#{key}/, mod: Randex.Generator.StreamData) + nval = %{context | child: value} |> Jake.gen_lazy() + + 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 + defp additional(properties, _all, min.._max, _context) when min < 0 or not is_map(properties) do StreamData.constant(%{}) end diff --git a/test/jake_test.exs b/test/jake_test.exs index 2a7f8ad..93eeddf 100644 --- a/test/jake_test.exs +++ b/test/jake_test.exs @@ -85,7 +85,8 @@ defmodule JakeTest do "draft4/additionalItems.json", "draft4/maxProperties.json", "draft4/minProperties.json", - "draft4/additionalProperties.json" + "draft4/additionalProperties.json", + "draft4/patternProperties.json" ] do Path.wildcard("test_suite/tests/#{path}") |> Enum.map(fn path -> File.read!(path) |> Jason.decode!() end) diff --git a/test_suite/tests/draft4/patternProperties.json b/test_suite/tests/draft4/patternProperties.json index 5f741df..d3ee422 100644 --- a/test_suite/tests/draft4/patternProperties.json +++ b/test_suite/tests/draft4/patternProperties.json @@ -116,5 +116,16 @@ "valid": false } ] + }, + { + "description": "patternProperties with properties", + "schema": { + "type": "object", + "properties": {"foo": {"maximum": 100}, "bar": {"type":"integer"}}, + "patternProperties": { + "f.*o": {"type": "integer"}, + "ba*r": {"minimum": 20} + } + } } ]