Skip to content

Commit ea92cf4

Browse files
Tuple types (#46)
Adds support for tuple typing for the 'items' property, see https://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.5. Specifically, the commit contains: * A tuple type definition * A tuple type parser + tests * A tuple type printer + tests This commit closes issue #3
1 parent cbe9d6e commit ea92cf4

File tree

17 files changed

+570
-13
lines changed

17 files changed

+570
-13
lines changed

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import Json.Decode as Decode
7575
, map
7676
, maybe
7777
, field
78+
, index
7879
, at
7980
, andThen
8081
, oneOf
@@ -92,6 +93,7 @@ import Json.Encode as Encode
9293
exposing
9394
( Value
9495
, object
96+
, list
9597
)
9698

9799

@@ -206,6 +208,7 @@ import Json.Decode as Decode
206208
, map
207209
, maybe
208210
, field
211+
, index
209212
, at
210213
, andThen
211214
, oneOf
@@ -223,6 +226,7 @@ import Json.Encode as Encode
223226
exposing
224227
( Value
225228
, object
229+
, list
226230
)
227231
import Domain.Definitions
228232

@@ -259,7 +263,10 @@ encodeCircle circle =
259263
radius =
260264
[ ( "radius", Encode.float circle.radius ) ]
261265
in
262-
object <| center ++ color ++ radius
266+
object <|
267+
center
268+
++ color
269+
++ radius
263270
```
264271

265272
## Tests

examples/example-output-elm-code/Domain/Circle.elm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Json.Decode as Decode
99
, map
1010
, maybe
1111
, field
12+
, index
1213
, at
1314
, andThen
1415
, oneOf
@@ -26,6 +27,7 @@ import Json.Encode as Encode
2627
exposing
2728
( Value
2829
, object
30+
, list
2931
)
3032
import Domain.Definitions
3133

examples/example-output-elm-code/Domain/Definitions.elm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Json.Decode as Decode
99
, map
1010
, maybe
1111
, field
12+
, index
1213
, at
1314
, andThen
1415
, oneOf
@@ -26,6 +27,7 @@ import Json.Encode as Encode
2627
exposing
2728
( Value
2829
, object
30+
, list
2931
)
3032

3133

lib/parser.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule JS2E.Parser do
77
require Logger
88
alias JS2E.Parsers.{ArrayParser, ObjectParser, EnumParser, PrimitiveParser,
99
DefinitionsParser, AllOfParser, AnyOfParser, OneOfParser,
10-
UnionParser, TypeReferenceParser}
10+
UnionParser, TupleParser, TypeReferenceParser}
1111
alias JS2E.{TypePath, Types, Predicates}
1212
alias JS2E.Types.SchemaDefinition
1313

@@ -100,6 +100,10 @@ defmodule JS2E.Parser do
100100
schema_root_node
101101
|> parse_type(schema_id, [], name)
102102

103+
Predicates.tuple_type?(schema_root_node) ->
104+
schema_root_node
105+
|> parse_type(schema_id, [], name)
106+
103107
Predicates.array_type?(schema_root_node) ->
104108
schema_root_node
105109
|> parse_type(schema_id, [], name)
@@ -169,6 +173,7 @@ defmodule JS2E.Parser do
169173
{&Predicates.one_of_type?/1, &OneOfParser.parse/5},
170174
{&Predicates.object_type?/1, &ObjectParser.parse/5},
171175
{&Predicates.array_type?/1, &ArrayParser.parse/5},
176+
{&Predicates.tuple_type?/1, &TupleParser.parse/5},
172177
{&Predicates.primitive_type?/1, &PrimitiveParser.parse/5},
173178
{&Predicates.definitions?/1, &DefinitionsParser.parse/5}
174179
]

lib/parsers/tuple_parser.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule JS2E.Parsers.TupleParser do
2+
@behaviour JS2E.Parsers.ParserBehaviour
3+
@moduledoc ~S"""
4+
Parses a JSON schema array type:
5+
6+
{
7+
"type": "array",
8+
"items": [
9+
{ "$ref": "#/rectangle" },
10+
{ "$ref": "#/circle" }
11+
]
12+
}
13+
14+
Into a `JS2E.Types.TupleType`.
15+
"""
16+
17+
require Logger
18+
import JS2E.Parsers.Util
19+
alias JS2E.{TypePath, Types}
20+
alias JS2E.Types.TupleType
21+
22+
@doc ~S"""
23+
Parses a JSON schema array type into an `JS2E.Types.TupleType`.
24+
"""
25+
@impl JS2E.Parsers.ParserBehaviour
26+
@spec parse(map, URI.t, URI.t | nil, TypePath.t, String.t)
27+
:: Types.typeDictionary
28+
def parse(schema_node, parent_id, id, path, name) do
29+
Logger.debug "Parsing '#{inspect path}' as TupleType"
30+
31+
descendants_types_dict =
32+
schema_node
33+
|> Map.get("items")
34+
|> create_descendants_type_dict(parent_id, path)
35+
Logger.debug "Descendants types dict: #{inspect descendants_types_dict}"
36+
37+
tuple_types =
38+
descendants_types_dict
39+
|> create_types_list(path)
40+
Logger.debug "Tuple types: #{inspect tuple_types}"
41+
42+
tuple_type = TupleType.new(name, path, tuple_types)
43+
Logger.debug "Parsed tuple type: #{inspect tuple_type}"
44+
45+
tuple_type
46+
|> create_type_dict(path, id)
47+
|> Map.merge(descendants_types_dict)
48+
end
49+
50+
end

lib/predicates.ex

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,28 @@ defmodule JS2E.Predicates do
209209
type == "array" && is_map(items)
210210
end
211211

212+
@doc ~S"""
213+
Returns true if the json subschema represents a tuple type.
214+
215+
## Examples
216+
217+
iex> JS2E.Predicates.tuple_type?(%{})
218+
false
219+
220+
iex> JS2E.Predicates.tuple_type?(%{"type" => "array"})
221+
false
222+
223+
iex> aTuple = %{"type" => "array",
224+
...> "items" => [%{"$ref" => "#foo"}, %{"$ref" => "#bar"}]}
225+
iex> JS2E.Predicates.tuple_type?(aTuple)
226+
true
227+
228+
"""
229+
@spec tuple_type?(map) :: boolean
230+
def tuple_type?(schema_node) do
231+
type = schema_node["type"]
232+
items = schema_node["items"]
233+
type == "array" && is_list(items)
234+
end
235+
212236
end

lib/printer.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ defmodule JS2E.Printer do
177177
) :: {Types.typeDefinition, SchemaDefinition.t}
178178
def resolve_type!(identifier, schema_def, schema_dict) do
179179
Logger.debug "Looking up '#{inspect identifier}' in #{inspect schema_def}"
180-
type_dict = schema_def.types
181180

182181
{resolved_type, resolved_schema_def} = cond do
183182

lib/printers/tuple_printer.ex

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
defmodule JS2E.Printers.TuplePrinter do
2+
@behaviour JS2E.Printers.PrinterBehaviour
3+
@moduledoc """
4+
A printer for printing a 'tuple' type decoder.
5+
"""
6+
7+
@templates_location Application.get_env(:js2e, :templates_location)
8+
@type_location Path.join(@templates_location, "tuple/type.elm.eex")
9+
@decoder_location Path.join(@templates_location, "tuple/decoder.elm.eex")
10+
@encoder_location Path.join(@templates_location, "tuple/encoder.elm.eex")
11+
12+
require Elixir.{EEx, Logger}
13+
import JS2E.Printers.Util
14+
alias JS2E.{Printer, TypePath, Types}
15+
alias JS2E.Types.{TupleType, SchemaDefinition}
16+
17+
EEx.function_from_file(:defp, :type_template, @type_location,
18+
[:type_name, :type_fields])
19+
20+
EEx.function_from_file(:defp, :decoder_template, @decoder_location,
21+
[:decoder_name, :type_name, :clauses])
22+
23+
EEx.function_from_file(:defp, :encoder_template, @encoder_location,
24+
[:encoder_name, :type_name, :properties])
25+
26+
@impl JS2E.Printers.PrinterBehaviour
27+
@spec print_type(Types.typeDefinition, SchemaDefinition.t,
28+
Types.schemaDictionary) :: String.t
29+
def print_type(%TupleType{name: name,
30+
path: _path,
31+
items: types}, schema_def, schema_dict) do
32+
33+
type_name = upcase_first name
34+
type_fields = create_type_fields(types, schema_def, schema_dict)
35+
36+
type_template(type_name, type_fields)
37+
end
38+
39+
@spec create_type_fields([TypePath.t], SchemaDefinition.t,
40+
Types.schemaDictionary) :: [String.t]
41+
defp create_type_fields(types, schema_def, schema_dict) do
42+
types |> Enum.map(&(create_type_field(&1, schema_def, schema_dict)))
43+
end
44+
45+
@spec create_type_field(TypePath.t, SchemaDefinition.t,
46+
Types.schemaDictionary) :: String.t
47+
defp create_type_field(type_path, schema_def, schema_dict) do
48+
type_path
49+
|> Printer.resolve_type!(schema_def, schema_dict)
50+
|> create_type_name(schema_def)
51+
end
52+
53+
@impl JS2E.Printers.PrinterBehaviour
54+
@spec print_decoder(Types.typeDefinition, SchemaDefinition.t,
55+
Types.schemaDictionary) :: String.t
56+
def print_decoder(%TupleType{name: name,
57+
path: _path,
58+
items: type_paths},
59+
schema_def, schema_dict) do
60+
61+
decoder_name = "#{name}Decoder"
62+
type_name = upcase_first name
63+
clauses = create_decoder_clauses(type_paths, schema_def, schema_dict)
64+
65+
decoder_template(decoder_name, type_name, clauses)
66+
end
67+
68+
@spec create_decoder_clauses(
69+
[TypePath.t],
70+
SchemaDefinition.t,
71+
Types.schemaDictionary
72+
) :: [map]
73+
defp create_decoder_clauses(type_paths, schema_def, schema_dict) do
74+
75+
type_paths
76+
|> Enum.map(fn type_path ->
77+
create_decoder_clause(type_path, schema_def, schema_dict)
78+
end)
79+
end
80+
81+
@spec create_decoder_clause(
82+
TypePath.t,
83+
SchemaDefinition.t,
84+
Types.schemaDictionary
85+
) :: map
86+
defp create_decoder_clause(type_path, schema_def, schema_dict) do
87+
88+
{property_type, resolved_schema_def} =
89+
type_path
90+
|> Printer.resolve_type!(schema_def, schema_dict)
91+
92+
decoder_name = create_decoder_name(
93+
{property_type, resolved_schema_def}, schema_def)
94+
95+
cond do
96+
union_type?(property_type) || one_of_type?(property_type) ->
97+
create_decoder_union_clause(decoder_name)
98+
99+
enum_type?(property_type) ->
100+
property_type_decoder =
101+
property_type.type
102+
|> determine_primitive_type_decoder!()
103+
104+
create_decoder_enum_clause(property_type_decoder, decoder_name)
105+
106+
true ->
107+
create_decoder_normal_clause(decoder_name)
108+
end
109+
end
110+
111+
defp create_decoder_union_clause(decoder_name) do
112+
%{decoder_name: decoder_name}
113+
end
114+
115+
defp create_decoder_enum_clause(property_type_decoder, decoder_name) do
116+
%{property_decoder: property_type_decoder,
117+
decoder_name: decoder_name}
118+
end
119+
120+
defp create_decoder_normal_clause(decoder_name) do
121+
%{decoder_name: decoder_name}
122+
end
123+
124+
@impl JS2E.Printers.PrinterBehaviour
125+
@spec print_encoder(Types.typeDefinition, SchemaDefinition.t,
126+
Types.schemaDictionary) :: String.t
127+
def print_encoder(%TupleType{name: name,
128+
path: _path,
129+
items: type_paths},
130+
schema_def, schema_dict) do
131+
132+
type_name = upcase_first name
133+
encoder_name = "encode#{type_name}"
134+
135+
properties = create_encoder_properties(type_paths, schema_def, schema_dict)
136+
137+
template = encoder_template(encoder_name, type_name, properties)
138+
trim_newlines(template)
139+
end
140+
141+
defp create_encoder_properties(type_paths, schema_def, schema_dict) do
142+
143+
type_paths
144+
|> Enum.map(fn type_path ->
145+
Printer.resolve_type!(type_path, schema_def, schema_dict)
146+
end)
147+
|> Enum.reduce([], fn ({resolved_property, resolved_schema}, properties) ->
148+
encoder_name = create_encoder_name(
149+
{resolved_property, resolved_schema}, schema_def)
150+
updated_property = Map.put(resolved_property, :encoder_name, encoder_name)
151+
properties ++ [updated_property]
152+
end)
153+
end
154+
155+
end

lib/types/array_type.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@ defmodule JS2E.Types.ArrayType do
22
@moduledoc ~S"""
33
Represents a custom 'array' type definition in a JSON schema.
44
5-
Limitations:
6-
7-
While the standard states
8-
9-
The value of "items" MUST be either a schema or array of schemas.
10-
11-
We limit the value of "items" such that it MUST be a schema and nothing else.
12-
13-
Furthermore, the "type" keyword MUST be present and have the value "array".
14-
155
JSON Schema:
166
177
"rectangles": {

0 commit comments

Comments
 (0)