308 lines
11 KiB
Python
Executable File
308 lines
11 KiB
Python
Executable File
#!/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()
|