Skip to content

Commit 71801ac

Browse files
committed
Add support for 'array' input from JSON data.
1 parent dbddd43 commit 71801ac

File tree

5 files changed

+107
-11
lines changed

5 files changed

+107
-11
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,13 @@ JSON data input format.
255255

256256
#### Options
257257

258-
This format does not support any options.
258+
* `array-name`: accepts a single string (e.g. `array-name=foo`), which
259+
must be a valid Python identifier and not a Python keyword. If this
260+
option is specified, and the JSON data provided is an `array`
261+
(sequence, list), the specified name will be used to make the data
262+
available to the Jinja2 template. Errors will be generated if
263+
`array` data is provided and this option is not specified, or if
264+
this option is specified and the data provided is an `object`.
259265

260266
#### Usage
261267

changelog.d/15.adding.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for 'array' data in JSON-format input.

src/jinjanator/formats.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,28 @@ def as_dict(self) -> Mapping[str, Any]:
5959
class JSONFormat:
6060
name = "json"
6161
suffixes: Iterable[str] | None = (".json",)
62-
option_names: Iterable[str] | None = ()
62+
option_names: Iterable[str] | None = "array-name"
6363

6464
def __init__(self, options: Iterable[str] | None) -> None:
65-
pass
65+
self.array_name: str | None = None
66+
if options:
67+
for option in options:
68+
try:
69+
opt, val = option.split("=")
70+
except ValueError as exc:
71+
raise FormatOptionValueError(
72+
self, option, "", "contains more than one '='"
73+
) from exc
74+
75+
if not val.isidentifier():
76+
raise FormatOptionValueError(
77+
self, opt, val, "is not a valid Python identifier"
78+
)
79+
80+
if keyword.iskeyword(val):
81+
raise FormatOptionValueError(self, opt, val, "is a Python keyword")
82+
83+
self.array_name = val
6684

6785
def parse(self, data_string: str) -> Mapping[str, Any]:
6886
"""JSON data input format.
@@ -85,13 +103,28 @@ def parse(self, data_string: str) -> Mapping[str, Any]:
85103
$ cat data.json | j2 --format=ini config.j2
86104
"""
87105

88-
context = json.loads(data_string)
106+
try:
107+
context = json.loads(data_string)
108+
except json.decoder.JSONDecodeError as exc:
109+
msg = "JSON input is neither an object nor an array"
110+
raise TypeError(msg) from exc
111+
112+
if isinstance(context, dict):
113+
if self.array_name:
114+
raise FormatOptionUnsupportedError(
115+
self,
116+
"array-name",
117+
"cannot be used with object (dictionary) input",
118+
)
119+
120+
return context
89121

90-
if not isinstance(context, dict):
91-
msg = "JSON input does not contain an object (dictionary)"
92-
raise TypeError(msg)
122+
if not self.array_name:
123+
raise FormatOptionUnsupportedError(
124+
self, "array-name", "must be specified for array (list) input"
125+
)
93126

94-
return context
127+
return {self.array_name: context}
95128

96129

97130
class YAMLFormat:

tests/test_invalid_data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010

1111

1212
def test_invalid_json(make_file_pair: FilePairFactory) -> None:
13-
files = make_file_pair("", '["one", "two"]', "json")
13+
files = make_file_pair("", "midge", "json")
1414

15-
with pytest.raises(TypeError, match="JSON input does not contain an object"):
15+
with pytest.raises(TypeError, match="JSON input is neither an object nor an array"):
1616
assert render_file(files, [])
1717

1818

1919
def test_invalid_yaml(make_file_pair: FilePairFactory) -> None:
20-
files = make_file_pair("", "-one\n-two", "yaml")
20+
files = make_file_pair("", "midge", "yaml")
2121

2222
with pytest.raises(TypeError, match="YAML input is neither a mapping nor a sequence"):
2323
assert render_file(files, [])

tests/test_json_input.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from __future__ import annotations
2+
3+
import jinjanator_plugins
4+
import pytest
5+
6+
from . import (
7+
FilePairFactory,
8+
render_file,
9+
)
10+
11+
12+
def test_mapping_normal(make_file_pair: FilePairFactory) -> None:
13+
files = make_file_pair("{{ foo }}", '{"foo": "bar"}', "json")
14+
15+
assert "bar" == render_file(files, [])
16+
17+
18+
def test_mapping_with_array_name_option(make_file_pair: FilePairFactory) -> None:
19+
files = make_file_pair("{{ foo }}", '{"foo": "bar"}', "json")
20+
21+
with pytest.raises(jinjanator_plugins.FormatOptionUnsupportedError):
22+
assert render_file(files, ["--format-option", "array-name=seq"])
23+
24+
25+
def test_array_normal(make_file_pair: FilePairFactory) -> None:
26+
files = make_file_pair("{{ seq[0] }}", "[1,2,3]", "json")
27+
28+
assert "1" == render_file(files, ["--format-option", "array-name=seq"])
29+
30+
31+
def test_array_without_array_name_option(make_file_pair: FilePairFactory) -> None:
32+
files = make_file_pair("{{ seq[0] }}", "[1,2,3]", "json")
33+
34+
with pytest.raises(jinjanator_plugins.FormatOptionUnsupportedError):
35+
assert render_file(files, [])
36+
37+
38+
def test_array_invalid_name(make_file_pair: FilePairFactory) -> None:
39+
files = make_file_pair("{{ seq[0] }}", "[1,2,3]", "json")
40+
41+
with pytest.raises(jinjanator_plugins.FormatOptionValueError):
42+
render_file(files, ["--format-option", "array-name=334seq"])
43+
44+
45+
def test_array_invalid_value(make_file_pair: FilePairFactory) -> None:
46+
files = make_file_pair("{{ seq[0] }}", "[1,2,3]", "json")
47+
48+
with pytest.raises(jinjanator_plugins.FormatOptionValueError):
49+
render_file(files, ["--format-option", "array-name=abc=def"])
50+
51+
52+
def test_array_keyword_name(make_file_pair: FilePairFactory) -> None:
53+
files = make_file_pair("{{ seq[0] }}", "[1,2,3]", "json")
54+
55+
with pytest.raises(jinjanator_plugins.FormatOptionValueError):
56+
render_file(files, ["--format-option", "array-name=raise"])

0 commit comments

Comments
 (0)