#!/usr/bin/env python3 """校验 ~/.local/share/todo/tasks.json 的格式、结构和完整性。 退出码: 0 - 校验通过 1 - 校验失败(错误信息输出到 stderr) 用法: python3 validate.py [文件路径] 默认路径: ~/.local/share/todo/tasks.json """ import json import os import sys from pathlib import Path DEFAULT_PATH = Path.home() / ".local" / "share" / "todo" / "tasks.json" SCHEMA_PATH = Path(__file__).parent.parent / "schema" / "tasks.schema.json" def load_json(path: Path) -> tuple[dict | None, str | None]: """加载并解析 JSON 文件。""" if not path.exists(): return None, f"文件不存在: {path}" try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) return data, None except json.JSONDecodeError as e: return None, f"JSON 格式错误: {e}" def load_schema() -> tuple[dict | None, str | None]: """加载 JSON Schema。""" if not SCHEMA_PATH.exists(): return None, f"Schema 文件不存在: {SCHEMA_PATH}" try: with open(SCHEMA_PATH, "r", encoding="utf-8") as f: return json.load(f), None except json.JSONDecodeError as e: return None, f"Schema 格式错误: {e}" def validate_structure(data: dict) -> list[str]: """校验数据结构。""" errors = [] if not isinstance(data, dict): errors.append("顶层必须是对象") return errors if "tasks" not in data: errors.append("缺少必需字段 'tasks'") return errors tasks = data["tasks"] if not isinstance(tasks, list): errors.append("'tasks' 必须是数组") return errors for i, item in enumerate(tasks): if not isinstance(item, str): errors.append(f"tasks[{i}] 必须是字符串,实际类型: {type(item).__name__}") elif len(item.strip()) == 0: errors.append(f"tasks[{i}] 不能为空字符串") return errors def validate_duplicates(tasks: list[str]) -> list[str]: """校验任务是否有重复(大小写不敏感)。""" errors = [] seen = {} for i, task in enumerate(tasks): key = task.strip().lower() if key in seen: errors.append(f"tasks[{i}] '{task}' 与 tasks[{seen[key]}] 重复") else: seen[key] = i return errors def validate_schema(data: dict) -> list[str]: """使用 JSON Schema 校验(如果 jsonschema 可用)。""" try: import jsonschema except ImportError: # jsonschema 未安装,跳过 schema 校验 return [] schema, err = load_schema() if err: return [f"Schema 校验跳过: {err}"] errors = [] try: jsonschema.validate(instance=data, schema=schema) except jsonschema.ValidationError as e: errors.append(f"Schema 校验失败: {e.message}") except jsonschema.SchemaError as e: errors.append(f"Schema 定义错误: {e.message}") return errors def main() -> int: path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_PATH # 1. JSON 格式校验 data, err = load_json(path) if err: print(err, file=sys.stderr) return 1 # 2. 结构校验 errors = validate_structure(data) if errors: for e in errors: print(f"结构错误: {e}", file=sys.stderr) return 1 # 3. JSON Schema 校验 schema_errors = validate_schema(data) if schema_errors: for e in schema_errors: print(f"Schema 错误: {e}", file=sys.stderr) return 1 # 4. 去重校验 dup_errors = validate_duplicates(data["tasks"]) if dup_errors: for e in dup_errors: print(f"重复错误: {e}", file=sys.stderr) return 1 print(f"校验通过: {path} ({len(data['tasks'])} 个任务)") return 0 if __name__ == "__main__": sys.exit(main())