From fc691911ba68b50940e0afb3d58c30c6ea183497 Mon Sep 17 00:00:00 2001 From: Peter Feerick Date: Thu, 21 Aug 2025 11:48:47 +1000 Subject: [PATCH] chore: validate `fw.json` syntax and target order (#6533) --- .github/workflows/actions.yml | 2 + .github/workflows/validate_fw_json.yml | 23 ++ fw.json | 6 +- tools/validate-json.py | 307 +++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/validate_fw_json.yml create mode 100755 tools/validate-json.py diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index ddffbad35..fbdce1bae 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -14,6 +14,7 @@ on: - '.github/workflows/macosx_cpn.yml' - '.github/workflows/win-cpn-32.yml' - '.github/workflows/win_cpn-64.yml' + - '.github/workflows/validate_json.yml' - 'companion/**' - '.gitpod.yml' - '.devcontainer/**' @@ -29,6 +30,7 @@ on: - '.github/workflows/macosx_cpn.yml' - '.github/workflows/win-cpn-32.yml' - '.github/workflows/win_cpn-64.yml' + - '.github/workflows/validate_fw_json.yml' - 'companion/**' - '.gitpod.yml' - '.devcontainer/**' diff --git a/.github/workflows/validate_fw_json.yml b/.github/workflows/validate_fw_json.yml new file mode 100644 index 000000000..9ee1bcbaf --- /dev/null +++ b/.github/workflows/validate_fw_json.yml @@ -0,0 +1,23 @@ +--- +name: Validate fw.json +on: + push: + paths: + - 'fw.json' + pull_request: + paths: + - 'fw.json' + workflow_dispatch: + +jobs: + json-validate: + name: Validate fw.json file + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Validate fw.json syntax and schema + run: python3 tools/validate-json.py --syntax-only + + - name: Validate fw.json alphabetical order + run: python3 tools/validate-json.py --order-only diff --git a/fw.json b/fw.json index ee77bdfdc..d4a74c149 100644 --- a/fw.json +++ b/fw.json @@ -2,12 +2,12 @@ "targets": [ ["Fatfish F16","f16-"], ["Flysky EL18", "el18-"], + ["Flysky NB4+", "nb4p-"], ["Flysky NV14", "nv14-"], ["Flysky PA01", "pa01-"], ["Flysky PL18", "pl18-"], ["Flysky PL18EV", "pl18ev-"], ["Flysky PL18U", "pl18u-"], - ["Flysky NB4+", "nb4p-"], ["Flysky ST16", "st16-"], ["FrSky Horus X10", "x10-"], ["FrSky Horus X10 Express", "x10express-"], @@ -21,6 +21,8 @@ ["HelloRadioSky V16", "v16-"], ["iFlight Commando 8", "commando8-"], ["Jumper Bumblebee", "bumblebee-"], + ["Jumper T-Pro S", "tpros-"], + ["Jumper T-Pro V2", "tprov2-"], ["Jumper T12 MAX", "t12max-"], ["Jumper T14", "t14-"], ["Jumper T15", "t15-"], @@ -28,8 +30,6 @@ ["Jumper T18", "t18-"], ["Jumper T20", "t20-"], ["Jumper T20 V2", "t20v2-"], - ["Jumper T-Pro V2", "tprov2-"], - ["Jumper T-Pro S", "tpros-"], ["RadioMaster Boxer", "boxer-"], ["RadioMaster GX12", "gx12-"], ["RadioMaster MT12","mt12-"], diff --git a/tools/validate-json.py b/tools/validate-json.py new file mode 100755 index 000000000..d96213b31 --- /dev/null +++ b/tools/validate-json.py @@ -0,0 +1,307 @@ +#!/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()