#!/usr/bin/env python3 """ EdgeTX JSON validation script This script validates JSON files with optional schema validation: 1. Generic JSON syntax validation for any JSON file 2. EdgeTX fw.json schema validation (targets array, changelog string) 3. EdgeTX fw.json alphabetical order validation Usage: python3 tools/validate-json.py [options] [file.json] Options: --fw-schema, -f Validate against EdgeTX fw.json schema --syntax-only, -s Only validate JSON syntax (and schema if --fw-schema) --order-only, -o Only validate alphabetical order (requires --fw-schema) --help, -h Show this help message Auto-detection: - If filename is 'fw.json', automatically applies --fw-schema - If no file specified and 'fw.json' exists, uses it with --fw-schema Examples: python3 validate-json.py data.json # Generic JSON validation python3 validate-json.py fw.json # Auto-detects fw.json schema python3 validate-json.py --fw-schema config.json # Force fw.json schema on other file python3 validate-json.py --syntax-only fw.json # Only syntax + fw schema python3 validate-json.py --order-only fw.json # Only order validation Exit codes: 0: All validations passed 1: Validation failed 2: File not found or other error """ import json import sys import os import argparse from pathlib import Path def validate_json_syntax(file_path, show_message=True): """Validate JSON syntax and load the data.""" if show_message: print(f'🔍 Validating JSON syntax: {file_path}') try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) if show_message: print('✅ JSON syntax is valid') return data except json.JSONDecodeError as e: print(f'❌ JSON syntax error: {e}') return None except FileNotFoundError: print(f'❌ File not found: {file_path}') return None except Exception as e: print(f'❌ Error reading file: {e}') return None def validate_fw_schema(data): """Validate the EdgeTX fw.json schema structure.""" print('🔍 Validating EdgeTX fw.json schema structure...') # Check required fields if 'targets' not in data: print('❌ Missing required field: "targets"') return False if 'changelog' not in data: print('❌ Missing required field: "changelog"') return False # Check field types if not isinstance(data['targets'], list): print('❌ Field "targets" must be an array') return False if not isinstance(data['changelog'], str): print('❌ Field "changelog" must be a string') return False # Validate each target structure for i, target in enumerate(data['targets']): target_num = i + 1 if not isinstance(target, list): print(f'❌ Target {target_num} must be an array') return False if len(target) != 2: print(f'❌ Target {target_num} must have exactly 2 elements, found {len(target)}') return False if not isinstance(target[0], str): print(f'❌ Target {target_num} first element (name) must be a string') return False if not isinstance(target[1], str): print(f'❌ Target {target_num} second element (prefix) must be a string') return False # Check for empty strings if not target[0].strip(): print(f'❌ Target {target_num} name cannot be empty') return False if not target[1].strip(): print(f'❌ Target {target_num} prefix cannot be empty') return False # Check for extra fields allowed_fields = {'targets', 'changelog'} extra_fields = set(data.keys()) - allowed_fields if extra_fields: print(f'❌ Unexpected fields found: {sorted(extra_fields)}') return False print(f'✅ EdgeTX fw.json schema validation passed - found {len(data["targets"])} targets') return True def validate_fw_alphabetical_order(data): """Validate that fw.json targets are in case-insensitive alphabetical order.""" print('🔍 Validating fw.json targets alphabetical order (case-insensitive)...') # Extract target names (first element of each target pair) target_names = [target[0] for target in data['targets']] # Sort case-insensitively sorted_names = sorted(target_names, key=str.lower) # Check if they're in order if target_names != sorted_names: print('❌ ERROR: Targets are not in alphabetical order (case-insensitive)!') print() print('Current order:') for i, name in enumerate(target_names): print(f' {i+1:2d}. {name}') print() print('Expected alphabetical order (case-insensitive):') for i, name in enumerate(sorted_names): print(f' {i+1:2d}. {name}') print() # Show misplaced targets misplaced = [] for i, (current, expected) in enumerate(zip(target_names, sorted_names)): if current != expected: misplaced.append(f'Position {i+1}: found "{current}", expected "{expected}"') if misplaced: print('Misplaced targets:') for item in misplaced[:10]: # Show first 10 print(f' - {item}') if len(misplaced) > 10: print(f' - ... and {len(misplaced) - 10} more') return False else: print('✅ All targets are in alphabetical order (case-insensitive)!') print(f'Found {len(target_names)} targets, all properly sorted.') return True def find_json_file(filename=None): """Find JSON file in current directory or repository root.""" if filename: return filename # Default to fw.json for backwards compatibility # Check current directory first if os.path.exists('fw.json'): return 'fw.json' elif os.path.exists('../fw.json'): return '../fw.json' else: # Try to find repository root current_dir = Path.cwd() for parent in [current_dir] + list(current_dir.parents): fw_json = parent / 'fw.json' if fw_json.exists(): return str(fw_json) return None def is_fw_json_file(file_path): """Check if the file is fw.json based on filename.""" return os.path.basename(file_path).lower() == 'fw.json' def main(): """Main validation function.""" parser = argparse.ArgumentParser( description='EdgeTX JSON validator', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python3 validate-json.py data.json # Generic JSON validation python3 validate-json.py fw.json # Auto-detects fw.json schema python3 validate-json.py --fw-schema config.json # Force fw.json schema validation python3 validate-json.py --syntax-only fw.json # Only syntax + fw schema python3 validate-json.py --order-only fw.json # Only order validation """ ) parser.add_argument('file', nargs='?', help='Path to JSON file (default: auto-detect fw.json)') parser.add_argument('-f', '--fw-schema', action='store_true', help='Validate against EdgeTX fw.json schema') parser.add_argument('-s', '--syntax-only', action='store_true', help='Only validate JSON syntax (and schema if --fw-schema)') parser.add_argument('-o', '--order-only', action='store_true', help='Only validate alphabetical order (requires --fw-schema)') args = parser.parse_args() # Validate argument combinations if args.order_only and not args.fw_schema: # Auto-enable fw-schema if doing order validation and file looks like fw.json file_to_check = args.file or find_json_file() if file_to_check and is_fw_json_file(file_to_check): args.fw_schema = True else: print('❌ --order-only requires --fw-schema or fw.json file') sys.exit(2) # Determine file path file_path = find_json_file(args.file) if not file_path: if args.file: file_path = args.file # Use provided file even if not found (will error later) else: print('❌ No JSON file found. Please specify the file path.') sys.exit(2) # Auto-detect fw.json schema if not args.fw_schema and is_fw_json_file(file_path): args.fw_schema = True # Determine what to validate check_syntax = not args.order_only # Always check syntax unless order-only check_schema = args.fw_schema and not args.order_only # Check schema if fw-schema and not order-only check_order = args.fw_schema and not args.syntax_only # Check order if fw-schema and not syntax-only print(f'EdgeTX JSON Validator') print(f'====================') print(f'File: {file_path}') if args.fw_schema: if args.syntax_only: print('Mode: EdgeTX fw.json syntax and schema validation only') elif args.order_only: print('Mode: EdgeTX fw.json alphabetical order validation only') else: print('Mode: Full EdgeTX fw.json validation (syntax + schema + order)') else: print('Mode: Generic JSON syntax validation') print() # Load JSON data (required for any validation) if args.order_only: # For order-only mode, silently load the JSON (we still need to parse it) print('🔍 Loading JSON for alphabetical order validation...') data = validate_json_syntax(file_path, show_message=False) if data is None: sys.exit(1) print('✅ JSON loaded successfully') else: # For syntax validation, show the full syntax validation data = validate_json_syntax(file_path, show_message=True) if data is None: sys.exit(1) validation_passed = True if check_schema: print() # Validate EdgeTX fw.json schema structure if not validate_fw_schema(data): validation_passed = False if args.syntax_only: # If syntax-only mode, exit immediately on failure sys.exit(1) # Don't continue to order validation if schema failed check_order = False if check_order: if not args.order_only: # Only add spacing if we're not in order-only mode print() # Validate alphabetical order if not validate_fw_alphabetical_order(data): validation_passed = False if not validation_passed: sys.exit(1) print() if args.fw_schema: if args.syntax_only: print('🎉 EdgeTX fw.json syntax and schema validation passed!') elif args.order_only: print('🎉 EdgeTX fw.json alphabetical order validation passed!') else: print('🎉 All EdgeTX fw.json validations passed!') else: print('🎉 JSON syntax validation passed!') if __name__ == '__main__': main()