feat(cli): --json machine-readable output mode

This commit is contained in:
tack
2026-04-23 15:53:07 +08:00
parent 6825d8520a
commit c223d22981
2 changed files with 84 additions and 7 deletions
@@ -70,3 +70,36 @@ def test_cli_unreadable_json_exits_3(tmp_path, capsys):
p = _write(tmp_path, "broken.json", "{not json")
code = main([str(p)])
assert code == 3
def test_cli_json_output_structure_on_success(tmp_path, capsys):
p = _write(tmp_path, "ok.json", _valid_minimal_book())
code = main([str(p), "--json"])
out = capsys.readouterr().out
payload = json.loads(out)
assert code == 0
assert payload["summary"] == {"total": 1, "ok": 1, "fail": 0}
assert len(payload["files"]) == 1
f0 = payload["files"][0]
assert f0["status"] == "ok"
assert f0["errors"] == []
assert f0["info"]["job_id"] == "cli_test"
assert f0["info"]["page_count"] == 1
assert f0["info"]["panel_count"] == 1
def test_cli_json_output_structure_on_failure(tmp_path, capsys):
book = _valid_minimal_book()
book["pages"][0]["page_index"] = 5
p = _write(tmp_path, "bad.json", book)
code = main([str(p), "--json"])
payload = json.loads(capsys.readouterr().out)
assert code == 1
assert payload["summary"] == {"total": 1, "ok": 0, "fail": 1}
f0 = payload["files"][0]
assert f0["status"] == "fail"
assert len(f0["errors"]) >= 1
err = f0["errors"][0]
assert err["layer"] == 2
assert err["path"] == "pages[0].page_index"
assert "expected 1" in err["message"]
+51 -7
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import argparse
import json
import sys
from dataclasses import asdict
from pathlib import Path
from .core.validator import CheckError, collect_errors
@@ -55,6 +56,17 @@ def _print_file_human(path: Path, status: str, errors: list[CheckError],
f"(raise --max-errors to see all)")
def _file_result_dict(path: Path, status: str, errors: list[CheckError], info: dict | None) -> dict:
d = {
"path": str(path),
"status": status,
"errors": [asdict(e) for e in errors],
}
if info is not None:
d["info"] = info
return d
def _ensure_utf8_stdio() -> None:
"""Reconfigure stdout/stderr to UTF-8 if the current encoding can't handle it."""
import contextlib
@@ -76,33 +88,65 @@ def main(argv: list[str] | None = None) -> int:
description="Validate comic script JSON(s).",
)
parser.add_argument("files", nargs="*", help="JSON file(s) to validate")
parser.add_argument("--json", action="store_true",
help="Emit machine-readable JSON instead of human-readable text")
args = parser.parse_args(argv)
if not args.files:
parser.print_usage(sys.stderr)
return 2
results: list[dict] = []
any_fail = False
for raw in args.files:
path = Path(raw)
if not path.exists():
print(f"✗ {path} (file not found)", file=sys.stderr)
any_fail = True
return 3
if args.json:
results.append({"path": str(path), "status": "fail",
"errors": [{"layer": 0, "path": "<io>",
"message": "file not found", "hint": None}]})
any_fail = True
continue
else:
print(f"✗ {path} (file not found)", file=sys.stderr)
return 3
try:
data = _load_json(path)
except (OSError, json.JSONDecodeError) as e:
print(f"✗ {path} (I/O or JSON parse error: {e})", file=sys.stderr)
return 3
if args.json:
results.append({"path": str(path), "status": "fail",
"errors": [{"layer": 0, "path": "<io>",
"message": f"I/O or JSON parse error: {e}",
"hint": None}]})
any_fail = True
continue
else:
print(f"✗ {path} (I/O or JSON parse error: {e})", file=sys.stderr)
return 3
errors = collect_errors(data)
if errors:
any_fail = True
_print_file_human(path, "fail", errors, None)
if args.json:
results.append(_file_result_dict(path, "fail", errors, None))
else:
_print_file_human(path, "fail", errors, None)
else:
info = {"version": data.get("version"), **_summarize_file(path, data)}
_print_file_human(path, "ok", [], info)
if args.json:
results.append(_file_result_dict(path, "ok", [], info))
else:
_print_file_human(path, "ok", [], info)
if args.json:
summary = {
"total": len(results),
"ok": sum(1 for r in results if r["status"] == "ok"),
"fail": sum(1 for r in results if r["status"] == "fail"),
}
print(json.dumps({"summary": summary, "files": results},
indent=2, ensure_ascii=False))
return 1 if any_fail else 0