| |
| """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_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description'] |
| REQUIRED_FIELDS_MODEL = [] |
|
|
| |
| 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: |
| |
| 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]: |
| 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 |
|
|
| |
| if file_url.endswith('.py') or file_url.endswith('.js'): |
| continue |
|
|
| |
| if 'github.com' in file_url: |
| |
| if file_url.endswith('.git'): |
| errors.append(f"Git repo URL must NOT end with .git: {file_url}") |
| continue |
|
|
| |
| 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 = [] |
|
|
| |
| errors.extend(validate_required_fields(entry, entry_index, required_fields)) |
|
|
| |
| 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 = [] |
|
|
| |
| path = Path(file_path) |
| if not path.exists(): |
| return False, [f"File not found: {file_path}"] |
|
|
| |
| 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}"] |
|
|
| |
| required_fields = [] |
|
|
| |
| entries_to_validate = [] |
|
|
| if isinstance(data, list): |
| |
| entries_to_validate = data |
| elif isinstance(data, dict): |
| |
| |
| 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: |
| |
| return True, [] |
| else: |
| return False, ["JSON root must be either an array or an object containing arrays"] |
|
|
| |
| for idx, entry in enumerate(entries_to_validate, start=1): |
| if not isinstance(entry, dict): |
| |
| 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: |
| |
| 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 <json-file>") |
| 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:") |
| |
| error_count = sum(1 for e in errors if e.strip().startswith('-')) |
| for error in errors: |
| |
| if error.strip().startswith('❌'): |
| print(error) |
| else: |
| print(error) |
| print(f"\nTotal errors: {error_count}") |
| sys.exit(1) |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|