Skip to content

Commit 0505eba

Browse files
Changes: replaces auto-generated names with :anonymous
- Changes: replaces auto-generated schema names like `zero` and `one` with `:anonymous` making it easier for a consuming project to decide what to do when a subschema does not have an explicit name. - Changes: replaces stringly-typed `type` properties for several schema types to instead use specific atoms corresponding to the set of valid JSON value type. - Adds: better error description for invalid union types.
1 parent e75c8df commit 0505eba

36 files changed

+337
-163
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ resolving a JSON schema type given an identifier via `resolve_type`.
3939

4040
Presuming we have the following two JSON schema files:
4141

42-
```elixir
42+
```json
4343
{
4444
"$schema": "http://json-schema.org/draft-07/schema#",
4545
"$id": "http://example.com/circle.json",
@@ -63,7 +63,7 @@ Presuming we have the following two JSON schema files:
6363

6464
and
6565

66-
```elixir
66+
```json
6767
{
6868
"$schema": "http://json-schema.org/draft-07/schema#",
6969
"title": "Definitions",
@@ -242,7 +242,7 @@ the schema dictionary. The `resolve_type` function expects:
242242

243243
In the example below, we resolve the reference to `color`:
244244

245-
```elixir
245+
```json
246246
"color": {
247247
"$ref": "http://example.com/definitions.json#color"
248248
}

lib/parser/const_parser.ex

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,30 @@ defmodule JsonSchema.Parser.ConstParser do
2121
2222
## Examples
2323
24-
iex> type?(%{})
25-
false
24+
iex> type?(%{})
25+
false
2626
27-
iex> type?(%{"const" => nil})
28-
true
27+
iex> type?(%{"const" => nil})
28+
true
2929
30-
iex> type?(%{"const" => false})
31-
true
30+
iex> type?(%{"const" => false})
31+
true
3232
33-
iex> type?(%{"const" => "23.4"})
34-
true
33+
iex> type?(%{"const" => "23.4"})
34+
true
3535
36-
iex> type?(%{"const" => "This is a constant"})
37-
true
36+
iex> type?(%{"const" => "This is a constant"})
37+
true
3838
39-
iex> type?(%{"const" => %{"foo" => 42}})
40-
true
39+
iex> type?(%{"const" => %{"foo" => 42}})
40+
true
4141
"""
4242
@impl JsonSchema.Parser.ParserBehaviour
4343
@spec type?(Types.schemaNode()) :: boolean
4444
def type?(%{"const" => const})
45-
when is_nil(const) or is_boolean(const) or is_number(const) or
46-
is_binary(const) or is_list(const) or is_map(const),
45+
when is_nil(const) or is_boolean(const) or is_integer(const) or
46+
is_number(const) or is_binary(const) or is_list(const) or
47+
is_map(const),
4748
do: true
4849

4950
def type?(_schema_node), do: false
@@ -67,12 +68,25 @@ defmodule JsonSchema.Parser.ConstParser do
6768
name: name,
6869
description: description,
6970
path: path,
70-
type: type,
71+
type: value_type_from_string(type),
7172
const: const
7273
}
7374

7475
const_type
7576
|> Util.create_type_dict(path, id)
7677
|> ParserResult.new()
7778
end
79+
80+
@spec value_type_from_string(String.t()) :: ConstType.value_type()
81+
defp value_type_from_string(type) do
82+
case type do
83+
"null" -> :null
84+
"boolean" -> :boolean
85+
"integer" -> :integer
86+
"number" -> :number
87+
"string" -> :string
88+
"object" -> :object
89+
"array" -> :array
90+
end
91+
end
7892
end

lib/parser/enum_parser.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ defmodule JsonSchema.Parser.EnumParser do
4949
) :: ParserResult.t()
5050
def parse(%{"enum" => values} = schema_node, _parent_id, id, path, name) do
5151
description = Map.get(schema_node, "description")
52-
type = Map.get(schema_node, "type")
52+
type = schema_node |> Map.get("type") |> parse_enum_type()
5353

5454
enum_type = %EnumType{
5555
name: name,
@@ -63,4 +63,13 @@ defmodule JsonSchema.Parser.EnumParser do
6363
|> Util.create_type_dict(path, id)
6464
|> ParserResult.new()
6565
end
66+
67+
@spec parse_enum_type(String.t()) :: EnumType.value_type()
68+
defp parse_enum_type(raw_type) do
69+
case raw_type do
70+
"string" -> :string
71+
"integer" -> :integer
72+
"number" -> :number
73+
end
74+
end
6675
end

lib/parser/error_util.ex

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,25 @@ defmodule JsonSchema.Parser.ErrorUtil do
204204
ParserError.new(identifier, :unexpected_type, error_msg)
205205
end
206206

207+
@spec unknown_union_type(Types.typeIdentifier(), String.t()) ::
208+
ParserError.t()
209+
def unknown_union_type(identifier, type_name) do
210+
printed_path = to_string(identifier)
211+
212+
error_msg = """
213+
214+
Encountered unknown union type at `#{printed_path}`
215+
216+
"type": [#{type_name}]
217+
#{error_markings(type_name)}
218+
219+
Hint: See the specification section 6. "Validation Keywords"
220+
<https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01#section-6.1.1>
221+
"""
222+
223+
ParserError.new(identifier, :unknown_union_type, error_msg)
224+
end
225+
207226
@spec unknown_enum_type(String.t()) :: ParserError.t()
208227
def unknown_enum_type(type_name) do
209228
error_msg = "Unknown or unsupported enum type: '#{type_name}'"
@@ -218,14 +237,18 @@ defmodule JsonSchema.Parser.ErrorUtil do
218237

219238
@spec unknown_node_type(
220239
Types.typeIdentifier(),
221-
String.t(),
240+
String.t() | :anonymous,
222241
Types.schemaNode()
223242
) :: ParserError.t()
224243
def unknown_node_type(identifier, name, schema_node) do
225244
full_identifier =
226-
identifier
227-
|> Util.add_fragment_child(name)
228-
|> to_string()
245+
if name == :anonymous do
246+
identifier |> to_string()
247+
else
248+
identifier
249+
|> Util.add_fragment_child(name)
250+
|> to_string()
251+
end
229252

230253
stringified_value = sanitize_value(schema_node["type"])
231254

lib/parser/parser_result_types.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule JsonSchema.Parser.ParserError do
1313
| :unknown_type
1414
| :unexpected_type
1515
| :unknown_enum_type
16+
| :unknown_union_type
1617
| :unknown_primitive_type
1718
| :unknown_node_type
1819

lib/parser/primitive_parser.ex

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ defmodule JsonSchema.Parser.PrimitiveParser do
3838
@spec type?(map) :: boolean
3939
def type?(schema_node) do
4040
type = schema_node["type"]
41-
type in ["null", "boolean", "string", "number", "integer"]
41+
type in ["null", "boolean", "integer", "number", "string"]
4242
end
4343

4444
@doc """
@@ -54,11 +54,22 @@ defmodule JsonSchema.Parser.PrimitiveParser do
5454
name: name,
5555
description: description,
5656
path: path,
57-
type: type
57+
type: value_type_from_string(type)
5858
}
5959

6060
primitive_type
6161
|> Util.create_type_dict(path, id)
6262
|> ParserResult.new()
6363
end
64+
65+
@spec value_type_from_string(String.t()) :: PrimitiveType.value_type()
66+
defp value_type_from_string(type) do
67+
case type do
68+
"null" -> :null
69+
"boolean" -> :boolean
70+
"integer" -> :integer
71+
"number" -> :number
72+
"string" -> :string
73+
end
74+
end
6475
end

lib/parser/union_parser.ex

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule JsonSchema.Parser.UnionParser do
1212

1313
require Logger
1414
alias JsonSchema.{Parser, Types}
15-
alias Parser.{ParserResult, Util}
15+
alias Parser.{ErrorUtil, ParserResult, Util}
1616
alias Types.UnionType
1717

1818
@doc """
@@ -41,15 +41,47 @@ defmodule JsonSchema.Parser.UnionParser do
4141
def parse(%{"type" => types} = schema_node, _parent_id, id, path, name) do
4242
description = Map.get(schema_node, "description")
4343

44+
unknown_type =
45+
types
46+
|> Enum.find(fn type ->
47+
type not in [
48+
"null",
49+
"boolean",
50+
"number",
51+
"integer",
52+
"string",
53+
"array",
54+
"object"
55+
]
56+
end)
57+
58+
errors =
59+
if unknown_type do
60+
[ErrorUtil.unknown_union_type(path, unknown_type)]
61+
else
62+
[]
63+
end
64+
4465
union_type = %UnionType{
4566
name: name,
4667
description: description,
4768
path: path,
48-
types: types
69+
types: types |> Enum.map(&value_type_from_string/1)
4970
}
5071

5172
union_type
5273
|> Util.create_type_dict(path, id)
53-
|> ParserResult.new()
74+
|> ParserResult.new([], errors)
75+
end
76+
77+
@spec value_type_from_string(String.t()) :: UnionType.value_type()
78+
defp value_type_from_string(type) do
79+
case type do
80+
"null" -> :null
81+
"boolean" -> :boolean
82+
"integer" -> :integer
83+
"number" -> :number
84+
"string" -> :string
85+
end
5486
end
5587
end

lib/parser/util.ex

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,43 @@ defmodule JsonSchema.Parser.Util do
6464
]
6565
def create_types_list(type_dict, path) do
6666
type_dict
67-
|> Enum.reduce(%{}, fn {child_abs_path, child_type}, reference_dict ->
67+
|> Enum.reduce(MapSet.new(), fn {child_abs_path, child_type}, acc_set ->
6868
normalized_child_name = child_type.name
69-
child_type_path = add_fragment_child(path, normalized_child_name)
7069

71-
if child_type_path == URI.parse(child_abs_path) do
72-
Map.merge(reference_dict, %{normalized_child_name => child_type_path})
70+
if normalized_child_name == :anonymous do
71+
child_abs_path_parts =
72+
child_abs_path
73+
|> String.split("/")
74+
75+
child_prefix_path =
76+
child_abs_path_parts
77+
|> Enum.drop(-1)
78+
|> Enum.join("/")
79+
|> URI.parse()
80+
81+
shares_prefix = child_prefix_path == path
82+
83+
last_path_part_is_number =
84+
child_abs_path_parts
85+
|> List.last()
86+
|> Integer.parse() != :error
87+
88+
if shares_prefix and last_path_part_is_number do
89+
MapSet.put(acc_set, URI.parse(child_abs_path))
90+
else
91+
acc_set
92+
end
7393
else
74-
reference_dict
94+
child_type_path = add_fragment_child(path, normalized_child_name)
95+
96+
if child_type_path == URI.parse(child_abs_path) do
97+
MapSet.put(acc_set, child_type_path)
98+
else
99+
acc_set
100+
end
75101
end
76102
end)
77-
|> Map.values()
103+
|> MapSet.to_list()
78104
end
79105

80106
@doc """
@@ -87,8 +113,14 @@ defmodule JsonSchema.Parser.Util do
87113
when is_list(child_nodes) do
88114
child_nodes
89115
|> Enum.reduce({ParserResult.new(), 0}, fn child_node, {result, idx} ->
90-
child_name = to_string(idx)
91-
child_result = parse_type(child_node, parent_id, path, child_name)
116+
child_result =
117+
parse_type(
118+
child_node,
119+
parent_id,
120+
add_fragment_child(path, to_string(idx)),
121+
:anonymous
122+
)
123+
92124
{ParserResult.merge(result, child_result), idx + 1}
93125
end)
94126
|> elem(0)
@@ -97,7 +129,13 @@ defmodule JsonSchema.Parser.Util do
97129
@doc """
98130
Parses a node type.
99131
"""
100-
@spec parse_type(Types.schemaNode(), URI.t(), URI.t(), String.t(), boolean) ::
132+
@spec parse_type(
133+
Types.schemaNode(),
134+
URI.t(),
135+
URI.t(),
136+
String.t() | :anonymous,
137+
boolean
138+
) ::
101139
ParserResult.t()
102140
def parse_type(schema_node, parent_id, path, name, name_is_regex \\ false) do
103141
definitions_result =
@@ -234,7 +272,11 @@ defmodule JsonSchema.Parser.Util do
234272
}
235273
236274
"""
237-
@spec add_fragment_child(URI.t(), String.t()) :: URI.t()
275+
@spec add_fragment_child(URI.t(), String.t() | :anonymous) :: URI.t()
276+
def add_fragment_child(uri, :anonymous) do
277+
uri
278+
end
279+
238280
def add_fragment_child(uri, child) do
239281
old_fragment = uri.fragment
240282

lib/types/all_of_type.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule JsonSchema.Types.AllOfType do
2-
@moduledoc """
2+
@moduledoc ~S"""
33
Represents a custom `allOf` type definition in a JSON schema.
44
55
JSON Schema:
@@ -58,7 +58,7 @@ defmodule JsonSchema.Types.AllOfType do
5858
use TypedStruct
5959

6060
typedstruct do
61-
field :name, String.t(), enforce: true
61+
field :name, String.t() | :anonymous, enforce: true
6262
field :description, String.t() | nil, default: nil
6363
field :path, URI.t(), enforce: true
6464
field :types, [URI.t()], enforce: true

lib/types/any_of_type.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule JsonSchema.Types.AnyOfType do
2-
@moduledoc """
2+
@moduledoc ~S"""
33
Represents a custom `anyOf` type definition in a JSON schema.
44
55
JSON Schema:
@@ -58,7 +58,7 @@ defmodule JsonSchema.Types.AnyOfType do
5858
use TypedStruct
5959

6060
typedstruct do
61-
field :name, String.t(), enforce: true
61+
field :name, String.t() | :anonymous, enforce: true
6262
field :description, String.t() | nil, default: nil
6363
field :path, URI.t(), enforce: true
6464
field :types, [URI.t()], enforce: true

0 commit comments

Comments
 (0)