Skip to content

Commit 4a5ca32

Browse files
committed
feat(cli): add --check validation-only mode
1 parent 9086144 commit 4a5ca32

3 files changed

Lines changed: 79 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ echo '{"x": 1}' | toon - # Stdin/stdout
5151
# Options
5252
toon data.json --encode --delimiter "\t" --length-marker
5353
toon data.toon --decode --no-strict --indent 4
54+
toon data.toon --check # Validate only (exit code)
5455
```
5556

56-
**Options:** `-e/--encode` `-d/--decode` `-o/--output` `--delimiter` `--indent` `--length-marker` `--no-strict`
57+
**Options:** `-e/--encode` `-d/--decode` `-o/--output` `--delimiter` `--indent` `--length-marker` `--no-strict` `--check`
5758

5859
## API Reference
5960

src/toon_format/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ def main() -> int:
7676
action="store_true",
7777
help="Disable strict validation when decoding",
7878
)
79+
parser.add_argument(
80+
"--check",
81+
action="store_true",
82+
help="Validate input only and return exit status without writing output",
83+
)
7984

8085
args = parser.parse_args()
8186

@@ -98,6 +103,9 @@ def main() -> int:
98103
if args.encode and args.decode:
99104
print("Error: Cannot specify both --encode and --decode", file=sys.stderr)
100105
return 1
106+
if args.check and args.output:
107+
print("Error: Cannot specify both --check and --output", file=sys.stderr)
108+
return 1
101109

102110
if args.encode:
103111
mode = "encode"
@@ -145,6 +153,9 @@ def main() -> int:
145153
return 1
146154

147155
# Write output
156+
if args.check:
157+
return 0
158+
148159
try:
149160
if args.output:
150161
output_path = Path(args.output)

tests/test_cli.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,72 @@ def test_no_strict_option(self, tmp_path):
272272
result = main()
273273
assert result == 0
274274

275+
def test_check_valid_json_returns_zero_without_output(self, tmp_path):
276+
"""Check mode should validate JSON input and not emit output."""
277+
input_file = tmp_path / "input.json"
278+
input_file.write_text('{"ok": true}')
279+
280+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
281+
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
282+
with patch("sys.argv", ["toon", str(input_file), "--check"]):
283+
result = main()
284+
assert result == 0
285+
assert mock_stdout.getvalue() == ""
286+
assert mock_stderr.getvalue() == ""
287+
288+
def test_check_invalid_json_returns_error(self, tmp_path):
289+
"""Check mode should fail for invalid JSON when encoding."""
290+
input_file = tmp_path / "input.json"
291+
input_file.write_text('{"broken": invalid}')
292+
293+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
294+
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
295+
with patch("sys.argv", ["toon", str(input_file), "--check"]):
296+
result = main()
297+
assert result == 1
298+
assert mock_stdout.getvalue() == ""
299+
assert "Error during encode" in mock_stderr.getvalue()
300+
301+
def test_check_valid_toon_returns_zero_without_output(self, tmp_path):
302+
"""Check mode should validate TOON input and not emit output."""
303+
input_file = tmp_path / "input.toon"
304+
input_file.write_text("name: Alice\nage: 30")
305+
306+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
307+
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
308+
with patch("sys.argv", ["toon", str(input_file), "--check"]):
309+
result = main()
310+
assert result == 0
311+
assert mock_stdout.getvalue() == ""
312+
assert mock_stderr.getvalue() == ""
313+
314+
def test_check_invalid_toon_returns_error(self, tmp_path):
315+
"""Check mode should fail for invalid TOON when decoding."""
316+
input_file = tmp_path / "input.toon"
317+
input_file.write_text("items[2]: 1")
318+
319+
with patch("sys.stdout", new_callable=StringIO) as mock_stdout:
320+
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
321+
with patch("sys.argv", ["toon", str(input_file), "--check"]):
322+
result = main()
323+
assert result == 1
324+
assert mock_stdout.getvalue() == ""
325+
assert "Error during decode" in mock_stderr.getvalue()
326+
327+
def test_error_check_and_output_together(self, tmp_path):
328+
"""Check mode cannot be combined with output path."""
329+
input_file = tmp_path / "input.json"
330+
input_file.write_text('{"test": true}')
331+
output_file = tmp_path / "output.toon"
332+
333+
with patch("sys.stderr", new_callable=StringIO) as mock_stderr:
334+
with patch(
335+
"sys.argv", ["toon", str(input_file), "--check", "-o", str(output_file)]
336+
):
337+
result = main()
338+
assert result == 1
339+
assert "Cannot specify both --check and --output" in mock_stderr.getvalue()
340+
275341
def test_decode_indent_option_affects_output(self, tmp_path):
276342
"""Ensure --indent controls the JSON formatting."""
277343
input_file = tmp_path / "input.toon"

0 commit comments

Comments
 (0)