142 lines
4.1 KiB
Python
142 lines
4.1 KiB
Python
"""Validate applications.json against schema and check for common issues."""
|
|
|
|
import json
|
|
import sys
|
|
from collections import defaultdict
|
|
from typing import Any
|
|
|
|
from constants import VARIANTS
|
|
from utils import should_include_app
|
|
|
|
# Required fields for each app
|
|
REQUIRED_FIELDS = {"id", "url", "author", "name"}
|
|
|
|
# Valid meta keys
|
|
VALID_META_KEYS = {
|
|
"excludeFromExport",
|
|
"excludeFromTable",
|
|
"nameOverride",
|
|
"urlOverride",
|
|
"includeInStandard",
|
|
"includeInDualScreen",
|
|
}
|
|
|
|
|
|
def validate_app(app: dict[str, Any], index: int) -> list[str]:
|
|
"""Validate a single app entry and return list of errors."""
|
|
errors = []
|
|
app_name = app.get("name", f"app[{index}]")
|
|
|
|
# Check required fields
|
|
for field in REQUIRED_FIELDS:
|
|
if field not in app:
|
|
errors.append(f"{app_name}: missing required field '{field}'")
|
|
|
|
# Validate meta keys if present
|
|
meta = app.get("meta", {})
|
|
for key in meta.keys():
|
|
if key not in VALID_META_KEYS:
|
|
errors.append(f"{app_name}: unknown meta key '{key}' (typo?)")
|
|
|
|
# Check for common typos
|
|
typo_checks = {
|
|
"exludeFromExport": "excludeFromExport",
|
|
"exludeFromTable": "excludeFromTable",
|
|
"nameOveride": "nameOverride",
|
|
"urlOveride": "urlOverride",
|
|
}
|
|
for typo, correct in typo_checks.items():
|
|
if typo in meta:
|
|
errors.append(f"{app_name}: typo in meta key '{typo}', should be '{correct}'")
|
|
|
|
# Validate additionalSettings is valid inner JSON
|
|
additional_settings = app.get("additionalSettings")
|
|
if additional_settings is not None:
|
|
if not isinstance(additional_settings, str):
|
|
errors.append(f"{app_name}: 'additionalSettings' should be a JSON string")
|
|
else:
|
|
try:
|
|
json.loads(additional_settings)
|
|
except json.JSONDecodeError as e:
|
|
errors.append(f"{app_name}: 'additionalSettings' contains invalid JSON: {e}")
|
|
|
|
# Validate categories is a list
|
|
categories = app.get("categories")
|
|
if categories is not None and not isinstance(categories, list):
|
|
errors.append(f"{app_name}: 'categories' should be a list")
|
|
|
|
return errors
|
|
|
|
|
|
def check_duplicate_ids(apps: list[dict[str, Any]], variant: str) -> list[str]:
|
|
"""Check for duplicate app IDs within a variant."""
|
|
errors = []
|
|
ids_seen: dict[str, str] = {}
|
|
|
|
for app in apps:
|
|
if not should_include_app(app, variant):
|
|
continue
|
|
|
|
app_id = app.get("id", "")
|
|
app_name = app.get("name", "unknown")
|
|
|
|
if not app_id:
|
|
continue # Already caught by required field check
|
|
|
|
if app_id in ids_seen:
|
|
errors.append(
|
|
f"Duplicate ID '{app_id}' in {variant} variant: "
|
|
f"'{ids_seen[app_id]}' and '{app_name}'"
|
|
)
|
|
else:
|
|
ids_seen[app_id] = app_name
|
|
|
|
return errors
|
|
|
|
|
|
def validate_json(input_file: str) -> int:
|
|
"""Validate applications.json and return exit code (0=success, 1=errors)."""
|
|
try:
|
|
with open(input_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
print(f"Invalid JSON: {e}")
|
|
return 1
|
|
except FileNotFoundError:
|
|
print(f"File not found: {input_file}")
|
|
return 1
|
|
|
|
if "apps" not in data:
|
|
print("Missing 'apps' key in JSON")
|
|
return 1
|
|
|
|
apps = data["apps"]
|
|
all_errors = []
|
|
|
|
# Validate each app
|
|
for i, app in enumerate(apps):
|
|
errors = validate_app(app, i)
|
|
all_errors.extend(errors)
|
|
|
|
# Check for duplicate IDs per variant
|
|
for variant in VARIANTS:
|
|
errors = check_duplicate_ids(apps, variant)
|
|
all_errors.extend(errors)
|
|
|
|
if all_errors:
|
|
print(f"Validation failed with {len(all_errors)} error(s):\n")
|
|
for error in all_errors:
|
|
print(f" - {error}")
|
|
return 1
|
|
|
|
print(f"Validation passed: {len(apps)} apps checked")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) != 2:
|
|
print("Usage: python validate-json.py <json_file>")
|
|
sys.exit(1)
|
|
|
|
sys.exit(validate_json(sys.argv[1]))
|