From 4a5ca32b335defabeb58ace2e0dc0c3aeb363b91 Mon Sep 17 00:00:00 2001 From: Diogo Ribeiro Date: Thu, 19 Mar 2026 14:44:34 +0000 Subject: [PATCH 1/2] feat(cli): add --check validation-only mode --- README.md | 3 +- src/toon_format/cli.py | 11 +++++++ tests/test_cli.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 31ea483..8801e6b 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,10 @@ echo '{"x": 1}' | toon - # Stdin/stdout # Options toon data.json --encode --delimiter "\t" --length-marker toon data.toon --decode --no-strict --indent 4 +toon data.toon --check # Validate only (exit code) ``` -**Options:** `-e/--encode` `-d/--decode` `-o/--output` `--delimiter` `--indent` `--length-marker` `--no-strict` +**Options:** `-e/--encode` `-d/--decode` `-o/--output` `--delimiter` `--indent` `--length-marker` `--no-strict` `--check` ## API Reference diff --git a/src/toon_format/cli.py b/src/toon_format/cli.py index f861f33..1badbc4 100644 --- a/src/toon_format/cli.py +++ b/src/toon_format/cli.py @@ -76,6 +76,11 @@ def main() -> int: action="store_true", help="Disable strict validation when decoding", ) + parser.add_argument( + "--check", + action="store_true", + help="Validate input only and return exit status without writing output", + ) args = parser.parse_args() @@ -98,6 +103,9 @@ def main() -> int: if args.encode and args.decode: print("Error: Cannot specify both --encode and --decode", file=sys.stderr) return 1 + if args.check and args.output: + print("Error: Cannot specify both --check and --output", file=sys.stderr) + return 1 if args.encode: mode = "encode" @@ -145,6 +153,9 @@ def main() -> int: return 1 # Write output + if args.check: + return 0 + try: if args.output: output_path = Path(args.output) diff --git a/tests/test_cli.py b/tests/test_cli.py index 95b3aea..40da96f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -272,6 +272,72 @@ def test_no_strict_option(self, tmp_path): result = main() assert result == 0 + def test_check_valid_json_returns_zero_without_output(self, tmp_path): + """Check mode should validate JSON input and not emit output.""" + input_file = tmp_path / "input.json" + input_file.write_text('{"ok": true}') + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + with patch("sys.stderr", new_callable=StringIO) as mock_stderr: + with patch("sys.argv", ["toon", str(input_file), "--check"]): + result = main() + assert result == 0 + assert mock_stdout.getvalue() == "" + assert mock_stderr.getvalue() == "" + + def test_check_invalid_json_returns_error(self, tmp_path): + """Check mode should fail for invalid JSON when encoding.""" + input_file = tmp_path / "input.json" + input_file.write_text('{"broken": invalid}') + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + with patch("sys.stderr", new_callable=StringIO) as mock_stderr: + with patch("sys.argv", ["toon", str(input_file), "--check"]): + result = main() + assert result == 1 + assert mock_stdout.getvalue() == "" + assert "Error during encode" in mock_stderr.getvalue() + + def test_check_valid_toon_returns_zero_without_output(self, tmp_path): + """Check mode should validate TOON input and not emit output.""" + input_file = tmp_path / "input.toon" + input_file.write_text("name: Alice\nage: 30") + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + with patch("sys.stderr", new_callable=StringIO) as mock_stderr: + with patch("sys.argv", ["toon", str(input_file), "--check"]): + result = main() + assert result == 0 + assert mock_stdout.getvalue() == "" + assert mock_stderr.getvalue() == "" + + def test_check_invalid_toon_returns_error(self, tmp_path): + """Check mode should fail for invalid TOON when decoding.""" + input_file = tmp_path / "input.toon" + input_file.write_text("items[2]: 1") + + with patch("sys.stdout", new_callable=StringIO) as mock_stdout: + with patch("sys.stderr", new_callable=StringIO) as mock_stderr: + with patch("sys.argv", ["toon", str(input_file), "--check"]): + result = main() + assert result == 1 + assert mock_stdout.getvalue() == "" + assert "Error during decode" in mock_stderr.getvalue() + + def test_error_check_and_output_together(self, tmp_path): + """Check mode cannot be combined with output path.""" + input_file = tmp_path / "input.json" + input_file.write_text('{"test": true}') + output_file = tmp_path / "output.toon" + + with patch("sys.stderr", new_callable=StringIO) as mock_stderr: + with patch( + "sys.argv", ["toon", str(input_file), "--check", "-o", str(output_file)] + ): + result = main() + assert result == 1 + assert "Cannot specify both --check and --output" in mock_stderr.getvalue() + def test_decode_indent_option_affects_output(self, tmp_path): """Ensure --indent controls the JSON formatting.""" input_file = tmp_path / "input.toon" From af29d242a179ac3c9cb05a0390b6c3de81a0a36a Mon Sep 17 00:00:00 2001 From: Diogo Ribeiro Date: Thu, 19 Mar 2026 15:09:06 +0000 Subject: [PATCH 2/2] fix: format CLI and tests with ruff --- tests/test_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 40da96f..569b2d5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -331,9 +331,7 @@ def test_error_check_and_output_together(self, tmp_path): output_file = tmp_path / "output.toon" with patch("sys.stderr", new_callable=StringIO) as mock_stderr: - with patch( - "sys.argv", ["toon", str(input_file), "--check", "-o", str(output_file)] - ): + with patch("sys.argv", ["toon", str(input_file), "--check", "-o", str(output_file)]): result = main() assert result == 1 assert "Cannot specify both --check and --output" in mock_stderr.getvalue()