#!/usr/bin/env python3 """JSON Entry Validator Validates JSON entries based on content structure. Validation rules based on JSON content: - {"custom_nodes": [...]}: Validates required fields (author, title, reference, files, install_type, description) - {"models": [...]}: Validates JSON syntax only (no required fields) - Other JSON structures: Validates JSON syntax only Git repository URL validation (for custom_nodes): 1. URLs must NOT end with .git 2. URLs must follow format: https://github.com/{author}/{reponame} 3. .py and .js files are exempt from this check Supported formats: - Array format: [{...}, {...}] - Object format: {"custom_nodes": [...]} or {"models": [...]} """ import json import re import sys from pathlib import Path from typing import Dict, List, Tuple # Required fields for each entry type REQUIRED_FIELDS_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description'] REQUIRED_FIELDS_MODEL = [] # model-list.json doesn't require field validation # Pattern for valid GitHub repository URL (without .git suffix) GITHUB_REPO_PATTERN = re.compile(r'^https://github\.com/[^/]+/[^/]+$') def get_entry_context(entry: Dict) -> str: """Get identifying information from entry for error messages Args: entry: JSON entry Returns: String with author and reference info """ parts = [] if 'author' in entry: parts.append(f"author={entry['author']}") if 'reference' in entry: parts.append(f"ref={entry['reference']}") if 'title' in entry: parts.append(f"title={entry['title']}") if parts: return " | ".join(parts) else: # No identifying info - show actual entry content (truncated) import json entry_str = json.dumps(entry, ensure_ascii=False) if len(entry_str) > 100: entry_str = entry_str[:100] + "..." return f"content={entry_str}" def validate_required_fields(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]: """Validate that all required fields are present Args: entry: JSON entry to validate entry_index: Index of entry in array (for error reporting) required_fields: List of required field names Returns: List of error descriptions (without entry prefix/context) """ errors = [] for field in required_fields: if field not in entry: errors.append(f"Missing required field '{field}'") elif entry[field] is None: errors.append(f"Field '{field}' is null") elif isinstance(entry[field], str) and not entry[field].strip(): errors.append(f"Field '{field}' is empty") elif field == 'files' and not entry[field]: # Empty array errors.append("Field 'files' is empty array") return errors def validate_git_repo_urls(entry: Dict, entry_index: int) -> List[str]: """Validate git repository URLs in 'files' array Requirements: - Git repo URLs must NOT end with .git - Must follow format: https://github.com/{author}/{reponame} - .py and .js files are exempt Args: entry: JSON entry to validate entry_index: Index of entry in array (for error reporting) Returns: List of error descriptions (without entry prefix/context) """ errors = [] if 'files' not in entry or not isinstance(entry['files'], list): return errors for file_url in entry['files']: if not isinstance(file_url, str): continue # Skip .py and .js files - they're exempt from git repo validation if file_url.endswith('.py') or file_url.endswith('.js'): continue # Check if it's a GitHub URL (likely a git repo) if 'github.com' in file_url: # Error if URL ends with .git if file_url.endswith('.git'): errors.append(f"Git repo URL must NOT end with .git: {file_url}") continue # Validate format: https://github.com/{author}/{reponame} if not GITHUB_REPO_PATTERN.match(file_url): errors.append(f"Invalid git repo URL format (expected https://github.com/author/reponame): {file_url}") return errors def validate_entry(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]: """Validate a single JSON entry Args: entry: JSON entry to validate entry_index: Index of entry in array (for error reporting) required_fields: List of required field names Returns: List of error messages (empty if valid) """ errors = [] # Check required fields errors.extend(validate_required_fields(entry, entry_index, required_fields)) # Check git repository URLs errors.extend(validate_git_repo_urls(entry, entry_index)) return errors def validate_json_file(file_path: str) -> Tuple[bool, List[str]]: """Validate JSON file containing entries Args: file_path: Path to JSON file Returns: Tuple of (is_valid, error_messages) """ errors = [] # Check file exists path = Path(file_path) if not path.exists(): return False, [f"File not found: {file_path}"] # Load JSON try: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) except json.JSONDecodeError as e: return False, [f"Invalid JSON: {e}"] except Exception as e: return False, [f"Error reading file: {e}"] # Determine required fields based on JSON content required_fields = [] # Validate structure - support both array and object formats entries_to_validate = [] if isinstance(data, list): # Direct array format: [{...}, {...}] entries_to_validate = data elif isinstance(data, dict): # Object format: {"custom_nodes": [...]} or {"models": [...]} # Determine validation based on keys if 'custom_nodes' in data and isinstance(data['custom_nodes'], list): required_fields = REQUIRED_FIELDS_CUSTOM_NODE entries_to_validate = data['custom_nodes'] elif 'models' in data and isinstance(data['models'], list): required_fields = REQUIRED_FIELDS_MODEL entries_to_validate = data['models'] else: # Other JSON structures (extension-node-map.json, etc.) - just validate JSON syntax return True, [] else: return False, ["JSON root must be either an array or an object containing arrays"] # Validate each entry for idx, entry in enumerate(entries_to_validate, start=1): if not isinstance(entry, dict): # Show actual value for type errors entry_str = json.dumps(entry, ensure_ascii=False) if not isinstance(entry, str) else repr(entry) if len(entry_str) > 150: entry_str = entry_str[:150] + "..." errors.append(f"\n❌ Entry #{idx}: Must be an object, got {type(entry).__name__}") errors.append(f" Actual value: {entry_str}") continue entry_errors = validate_entry(entry, idx, required_fields) if entry_errors: # Group errors by entry with context context = get_entry_context(entry) errors.append(f"\n❌ Entry #{idx} ({context}):") for error in entry_errors: errors.append(f" - {error}") is_valid = len(errors) == 0 return is_valid, errors def main(): """Main entry point""" if len(sys.argv) < 2: print("Usage: python json-checker.py ") print("\nValidates JSON entries based on content:") print(" - {\"custom_nodes\": [...]}: Validates required fields (author, title, reference, files, install_type, description)") print(" - {\"models\": [...]}: Validates JSON syntax only (no required fields)") print(" - Other JSON structures: Validates JSON syntax only") print("\nGit repo URL validation (for custom_nodes):") print(" - URLs must NOT end with .git") print(" - URLs must follow: https://github.com/{author}/{reponame}") sys.exit(1) file_path = sys.argv[1] is_valid, errors = validate_json_file(file_path) if is_valid: print(f"✅ {file_path}: Validation passed") sys.exit(0) else: print(f"Validating: {file_path}") print("=" * 60) print("❌ Validation failed!\n") print("Errors:") # Count actual errors (lines starting with " -") error_count = sum(1 for e in errors if e.strip().startswith('-')) for error in errors: # Don't add ❌ prefix to grouped entries (they already have it) if error.strip().startswith('❌'): print(error) else: print(error) print(f"\nTotal errors: {error_count}") sys.exit(1) if __name__ == '__main__': main()