Skip to content

Commit d9b72d4

Browse files
committed
Represent refs as structs
1 parent 3a72454 commit d9b72d4

4 files changed

Lines changed: 107 additions & 119 deletions

File tree

lib/ex_json_schema/schema.ex

Lines changed: 29 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule ExJsonSchema.Schema do
2424
alias ExJsonSchema.Schema.Draft7
2525
alias ExJsonSchema.Schema.Root
2626
alias ExJsonSchema.Validator
27+
alias ExJsonSchema.Schema.Ref
2728

2829
@type ref_path :: [:root | String.t()]
2930
@type resolved :: ExJsonSchema.data() | %{String.t() => (Root.t() -> {Root.t(), resolved}) | ref_path} | true | false
@@ -61,24 +62,18 @@ defmodule ExJsonSchema.Schema do
6162

6263
@spec get_fragment(Root.t(), ref_path | ExJsonSchema.json_path()) ::
6364
{:ok, resolved} | invalid_reference_error | no_return
64-
def get_fragment(root = %Root{}, path) when is_binary(path) do
65-
case resolve_ref(root, path) do
66-
{:ok, {_root, ref}} -> get_fragment(root, ref)
67-
error -> error
68-
end
65+
def get_fragment(root = %Root{}, ref) when is_binary(ref) do
66+
get_fragment(root, Ref.from_string(ref, root))
6967
end
7068

71-
def get_fragment(%Root{schema: schema, refs: refs}, [:root | path] = ref) do
72-
case Map.get(refs, ref_to_string(ref)) do
73-
nil -> do_get_fragment(schema, path, ref)
74-
schema -> {:ok, schema}
75-
end
76-
end
69+
def get_fragment(%Root{schema: schema, refs: refs}, %Ref{location: location, fragment: fragment} = ref) do
70+
case Map.get(refs, to_string(ref)) do
71+
nil ->
72+
schema = if Ref.local?(ref), do: schema, else: refs[location]
73+
do_get_fragment(schema, fragment, ref)
7774

78-
def get_fragment(%Root{refs: refs}, [url | path] = ref) when is_binary(url) do
79-
case Map.get(refs, ref_to_string(ref)) do
80-
nil -> do_get_fragment(refs[url], path, ref)
81-
schema -> {:ok, schema}
75+
schema ->
76+
{:ok, schema}
8277
end
8378
end
8479

@@ -91,8 +86,8 @@ defmodule ExJsonSchema.Schema do
9186
end
9287

9388
@spec get_ref_schema(Root.t(), [:root | String.t()]) :: ExJsonSchema.data() | no_return
94-
def get_ref_schema(%Root{schema: schema}, [:root | path] = ref) do
95-
case get_ref_schema_with_schema(schema, path, ref) do
89+
def get_ref_schema(%Root{schema: schema}, %Ref{location: :root, fragment: fragment} = ref) do
90+
case get_ref_schema_with_schema(schema, fragment, ref) do
9691
{:error, error} ->
9792
raise InvalidSchemaError, message: error
9893

@@ -101,8 +96,8 @@ defmodule ExJsonSchema.Schema do
10196
end
10297
end
10398

104-
def get_ref_schema(%Root{refs: refs}, [url | path] = ref) when is_binary(url) do
105-
case get_ref_schema_with_schema(refs[url], path, ref) do
99+
def get_ref_schema(%Root{refs: refs}, %Ref{location: url, fragment: fragment} = ref) when is_binary(url) do
100+
case get_ref_schema_with_schema(refs[url], fragment, ref) do
106101
{:error, error} ->
107102
raise InvalidSchemaError, message: error
108103

@@ -137,16 +132,16 @@ defmodule ExJsonSchema.Schema do
137132
defp resolve_refs(%Root{} = root, schema) when is_map(schema) do
138133
schema
139134
|> Enum.reduce(root, fn
140-
{"$ref", [:root | _]}, root ->
135+
{"$ref", %Ref{} = ref}, root ->
136+
root =
137+
case Ref.cached?(ref, root) do
138+
true -> root
139+
false -> resolve_and_cache_remote_schema(root, ref)
140+
end
141+
142+
get_fragment!(root, ref)
141143
root
142144

143-
{"$ref", ref}, root when is_list(ref) ->
144-
if local_ref?(root, ref) do
145-
root
146-
else
147-
resolve_and_cache_remote_schema(root, ref)
148-
end
149-
150145
{_, value}, root when is_map(value) ->
151146
resolve_refs(root, value)
152147

@@ -161,10 +156,6 @@ defmodule ExJsonSchema.Schema do
161156

162157
defp resolve_refs(%Root{} = root, _), do: root
163158

164-
defp local_ref?(%Root{refs: refs}, [url | _] = ref) do
165-
Map.has_key?(refs, url) || Map.has_key?(refs, ref_to_string(ref))
166-
end
167-
168159
defp schema_version!(schema_url) do
169160
case schema_module(schema_url, :error) do
170161
:error -> raise(UnsupportedSchemaVersionError)
@@ -253,71 +244,17 @@ defmodule ExJsonSchema.Schema do
253244

254245
scoped_ref =
255246
case URI.parse(scope) do
256-
%URI{host: nil} -> ref
257-
scope_uri -> URI.merge(scope_uri, ref_uri) |> to_string()
247+
%URI{host: nil} -> ref_uri
248+
scope_uri -> URI.merge(scope_uri, ref_uri)
258249
end
250+
|> to_string()
259251

260-
{root, path} = resolve_ref!(root, scoped_ref)
261-
{root, {"$ref", path}}
252+
{root, {"$ref", Ref.from_string(scoped_ref, root)}}
262253
end
263254

264255
defp resolve_property(root, tuple, _) when is_tuple(tuple), do: {root, tuple}
265256

266-
defp resolve_ref(%Root{location: location} = root, "#") do
267-
{:ok, {root, [location]}}
268-
end
269-
270-
defp resolve_ref(root, ref) do
271-
[url | anchor] = String.split(ref, "#")
272-
ref_path = validate_ref_path(anchor, ref)
273-
{root, path} = root_and_path_for_url(root, ref_path, url)
274-
275-
{:ok, {root, path}}
276-
# case get_fragment(root, path) do
277-
# {:ok, _schema} -> {:ok, {root, path}}
278-
# error -> error
279-
# end
280-
end
281-
282-
defp resolve_ref!(root, ref) do
283-
case resolve_ref(root, ref) do
284-
{:ok, result} -> result
285-
{:error, :invalid_reference} -> raise_invalid_reference_error(ref)
286-
end
287-
end
288-
289-
defp validate_ref_path([], _), do: nil
290-
defp validate_ref_path([""], _), do: nil
291-
defp validate_ref_path([fragment], _) when is_binary(fragment), do: fragment
292-
defp validate_ref_path(_, ref), do: raise_invalid_reference_error(ref)
293-
294-
defp root_and_path_for_url(%Root{location: location} = root, fragment, "") do
295-
{root, [location | relative_path(fragment)]}
296-
end
297-
298-
defp root_and_path_for_url(root, fragment, url) do
299-
# root = resolve_and_cache_remote_schema(root, url)
300-
{root, [url | relative_path(fragment)]}
301-
end
302-
303-
defp relative_path(nil), do: []
304-
defp relative_path(fragment), do: relative_ref_path(fragment)
305-
306-
defp relative_ref_path(ref) do
307-
keys = unescaped_ref_segments(ref)
308-
309-
Enum.map(keys, fn key ->
310-
case key =~ ~r/^\d+$/ do
311-
true ->
312-
String.to_integer(key)
313-
314-
false ->
315-
key
316-
end
317-
end)
318-
end
319-
320-
defp resolve_and_cache_remote_schema(root, [url | _]) do
257+
defp resolve_and_cache_remote_schema(root, %Ref{location: url}) do
321258
remote_schema = remote_schema(url)
322259
resolve_remote_schema(root, url, remote_schema)
323260
end
@@ -388,18 +325,6 @@ defmodule ExJsonSchema.Schema do
388325
not Map.has_key?(schema, "additionalItems")
389326
end
390327

391-
defp unescaped_ref_segments(ref) do
392-
ref
393-
|> String.split("/")
394-
|> Enum.reject(&(&1 == ""))
395-
|> Enum.map(fn segment ->
396-
segment
397-
|> String.replace("~0", "~")
398-
|> String.replace("~1", "/")
399-
|> URI.decode()
400-
end)
401-
end
402-
403328
defp meta_schema?(%{"id" => "http://json-schema.org/" <> _}), do: true
404329
defp meta_schema?(%{"$id" => "http://json-schema.org/" <> _}), do: true
405330
defp meta_schema?(_), do: false
@@ -420,7 +345,7 @@ defmodule ExJsonSchema.Schema do
420345
end
421346

422347
defp get_ref_schema_with_schema(nil, _, ref) do
423-
{:error, "reference #{ref_to_string(ref)} could not be resolved"}
348+
{:error, "reference #{to_string(ref)} could not be resolved"}
424349
end
425350

426351
defp get_ref_schema_with_schema(schema, [], _) do
@@ -439,13 +364,10 @@ defmodule ExJsonSchema.Schema do
439364
|> get_ref_schema_with_schema(path, ref)
440365
end
441366

442-
defp ref_to_string([:root | path]), do: "##{path}"
443-
defp ref_to_string([url | path]), do: url <> "#" <> Enum.join(path, "/")
444-
445367
@spec raise_invalid_reference_error(any) :: no_return
446368
def raise_invalid_reference_error(ref) when is_binary(ref),
447369
do: raise(InvalidReferenceError, message: "invalid reference #{ref}")
448370

449371
def raise_invalid_reference_error(ref),
450-
do: ref |> ref_to_string |> raise_invalid_reference_error
372+
do: ref |> to_string() |> raise_invalid_reference_error
451373
end

lib/ex_json_schema/schema/ref.ex

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
defmodule ExJsonSchema.Schema.Ref do
2+
defstruct location: nil, fragment: nil, fragment_pointer?: false
3+
4+
alias ExJsonSchema.Schema.Root
5+
6+
def local?(%__MODULE__{location: :root}), do: true
7+
def local?(%__MODULE__{}), do: false
8+
9+
def cached?(%__MODULE__{location: url} = ref, %Root{refs: refs}) do
10+
local?(ref) || Map.has_key?(refs, url) || Map.has_key?(refs, to_string(ref))
11+
end
12+
13+
def from_string(ref, root) do
14+
from_uri(URI.parse(ref), root)
15+
end
16+
17+
defp from_uri(%URI{host: nil, path: nil, fragment: fragment}, %Root{location: location}) do
18+
%__MODULE__{location: location} |> parse_fragment(fragment)
19+
end
20+
21+
defp from_uri(%URI{fragment: fragment} = uri, _root) do
22+
location = %URI{uri | fragment: nil} |> to_string()
23+
%__MODULE__{location: location} |> parse_fragment(fragment)
24+
end
25+
26+
defp parse_fragment(ref, fragment) when fragment in [nil, ""], do: %__MODULE__{ref | fragment: []}
27+
28+
defp parse_fragment(ref, "/" <> _ = pointer) do
29+
keys = unescaped_ref_segments(pointer)
30+
31+
pointer =
32+
Enum.map(keys, fn key ->
33+
case key =~ ~r/^\d+$/ do
34+
true -> String.to_integer(key)
35+
false -> key
36+
end
37+
end)
38+
39+
%__MODULE__{ref | fragment: pointer, fragment_pointer?: true}
40+
end
41+
42+
defp parse_fragment(ref, id), do: %__MODULE__{ref | fragment: [id]}
43+
44+
defp unescaped_ref_segments(ref) do
45+
ref
46+
|> String.split("/")
47+
|> Enum.reject(&(&1 == ""))
48+
|> Enum.map(fn segment ->
49+
segment
50+
|> String.replace("~0", "~")
51+
|> String.replace("~1", "/")
52+
|> URI.decode()
53+
end)
54+
end
55+
end
56+
57+
defimpl String.Chars, for: ExJsonSchema.Schema.Ref do
58+
alias ExJsonSchema.Schema.Ref
59+
60+
def to_string(%ExJsonSchema.Schema.Ref{location: :root} = ref), do: fragment_to_string(ref)
61+
def to_string(%ExJsonSchema.Schema.Ref{location: url} = ref), do: url <> fragment_to_string(ref)
62+
63+
defp fragment_to_string(%Ref{fragment: []}), do: ""
64+
defp fragment_to_string(%Ref{fragment: fragment, fragment_pointer?: true}), do: "#/" <> Enum.join(fragment, "/")
65+
defp fragment_to_string(%Ref{fragment: [fragment], fragment_pointer?: false}), do: "#" <> fragment
66+
end

lib/ex_json_schema/validator/ref.ex

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,11 @@ defmodule ExJsonSchema.Validator.Ref do
1313

1414
@impl ExJsonSchema.Validator
1515
def validate(root, _, {"$ref", ref}, data, path) do
16-
do_validate(root, ref, data, path)
17-
end
18-
19-
def validate(_, _, _, _, _) do
20-
[]
21-
end
22-
23-
defp do_validate(root, ref, data, path) when is_bitstring(ref) or is_list(ref) do
2416
schema = Schema.get_fragment!(root, ref)
2517
Validator.validation_errors(root, schema, data, path)
2618
end
2719

28-
defp do_validate(root, ref, data, path) when is_map(ref) do
29-
Validator.validation_errors(root, ref, data, path)
20+
def validate(_, _, _, _, _) do
21+
[]
3022
end
3123
end

test/ex_json_schema/schema_test.exs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,23 @@ defmodule ExJsonSchema.SchemaTest do
136136
})
137137

138138
assert get_fragment!(schema, "#/properties/foo") == %{
139-
"$ref" => ["http://localhost:1234/subschema.json", "foo"]
139+
"$ref" => %ExJsonSchema.Schema.Ref{
140+
location: "http://localhost:1234/subschema.json",
141+
fragment: ["foo"],
142+
fragment_pointer?: true
143+
}
140144
}
141145
end
142146

143147
test "fetching a ref schema with a URL" do
144148
schema = resolve(%{"$ref" => "http://localhost:1234/subschema.json#/foo"})
145149

146150
assert get_fragment!(schema, "http://localhost:1234/subschema.json#/foo") == %{
147-
"$ref" => ["http://localhost:1234/subsubschema.json", "foo"]
151+
"$ref" => %ExJsonSchema.Schema.Ref{
152+
location: "http://localhost:1234/subsubschema.json",
153+
fragment: ["foo"],
154+
fragment_pointer?: true
155+
}
148156
}
149157
end
150158
end

0 commit comments

Comments
 (0)