4b56297498
- 数据存储: ~/.local/share/todo/tasks.json 纯字符串数组 - 操作简化: 仅保留 add/list/done,无状态/优先级/归档 - 新增 scripts/validate.py 校验脚本 (格式/结构/去重) - 新增 schema/tasks.schema.json - 删除 references/setup.md (MySQL 配置指南) Co-Authored-By: Claude Code <noreply@anthropic.com>
143 lines
3.8 KiB
Python
143 lines
3.8 KiB
Python
#!/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())
|