diff --git a/src/launch_manager_daemon/config/config_schema/README.rst b/src/launch_manager_daemon/config/config_schema/README.rst new file mode 100755 index 00000000..52e1857b --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/README.rst @@ -0,0 +1,86 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + + +Launch Manager Configuration Schema +################################### + +This folder contains the Launch Manager configuration JSON Schema. The schema defines and validates the structure of Launch Manager configuration files. + +Overview +******** + +This project manages the Launch Manager configuration schema as a single, self-contained file. When you need to modify or extend the schema, you should directly edit `s-core_launch_manager.schema.json`. + +**Project Structure:** + +:: + + +-- s-core_launch_manager.schema.json # The Launch Manager schema. + +-- examples/ # Illustrative example configuration files for the schema. + +-- scripts/ # Utility scripts, including a validation tool. + +Quick Start +*********** + +For Users & Developers +====================== + +Whether you're validating a Launch Manager configuration against the schema, or actively developing and modifying the schema itself, here's how to interact with this project: + +1. **Locate the Schema:** The complete schema definition resides in ``s-core_launch_manager.schema.json``. +2. **Explore Examples:** The ``examples/`` folder provides various sample Launch Manager configuration files. These are invaluable for understanding how the schema applies in practice and how to structure your own configurations. +3. **Validate Your Configuration:** Use the provided validation script to check if your configuration file conforms to the schema: + + .. code-block:: bash + + scripts/validate.py --schema s-core_launch_manager.schema.json --instance your_config.json + +Examples +******** + +The ``examples`` folder contains a set of sample Launch Manager configuration files. Each example demonstrates valid configurations according to the ``s-core_launch_manager.schema.json``. + +**Recommendation:** If you are new to Launch Manager configurations, **start by reviewing these examples**. They offer practical insight into the expected structure, available properties, and common use cases defined by the schema. + +Scripts +******* + +The ``scripts`` folder houses utility scripts designed to assist with schema development. + +validate.py +=========== + +The ``validate.py`` script is a crucial tool for verifying that any given Launch Manager configuration instance adheres to the rules defined in `s-core_launch_manager.schema.json`. + +**Usage:** + +To validate a configuration file (e.g., `example_conf.json` from the `examples` folder) against the schema: + +.. code-block:: bash + + scripts/validate.py --schema s-core_launch_manager.schema.json --instance examples/example_conf.json + Success --> examples/example_conf.json: valid + +**When to use:** +* **During Development:** Run this script frequently whenever you're creating or modifying a Launch Manager configuration file. It provides immediate feedback on whether your changes are valid according to the schema. +* **Schema Development:** If you are making changes to `s-core_launch_manager.schema.json` itself, always run `validate.py` against the examples to ensure your schema changes haven't inadvertently broken existing, valid configurations. + +Typical Workflow +**************** + +For schema developers or those creating new configurations: + +1. **Modify** the ``s-core_launch_manager.schema.json`` file (if you're updating the schema definition) or your Launch Manager configuration file. +2. **Validate** your changes using the `scripts/validate.py` script against relevant example files or your new configuration. This iterative process helps ensure compliance and catch errors early. diff --git a/src/launch_manager_daemon/config/config_schema/examples/example_conf.json b/src/launch_manager_daemon/config/config_schema/examples/example_conf.json new file mode 100755 index 00000000..ddb3bc7a --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/examples/example_conf.json @@ -0,0 +1,170 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0, + "delay_before_restart": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [], + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": [], + "process_arguments": [], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "depends_on": [], + "transition_timeout": 5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2.0, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + }, + "components": { + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "Native" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Minimal"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Minimal" + } + } + }, + "Off": { + "description": "Nothing is running", + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "initial_run_target": "Minimal", + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} diff --git a/src/launch_manager_daemon/config/config_schema/s-core_launch_manager.schema.json b/src/launch_manager_daemon/config/config_schema/s-core_launch_manager.schema.json new file mode 100644 index 00000000..371630d7 --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/s-core_launch_manager.schema.json @@ -0,0 +1,491 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Configuration schema for the S-CORE Launch Manager", + "description": "Defines the structure and valid values for the Launch Manager configuration file, which specifies managed components, run targets, and recovery behaviors.", + "$defs": { + "component_properties": { + "type": "object", + "description": "Defines a reusable type that captures essential characteristics of a software component.", + "properties": { + "binary_name": { + "type": "string", + "description": "Specifies the relative path of the executable file inside the directory defined by 'deployment_config.bin_dir'. The final executable path will be resolved as '{deployment_config.bin_dir}/{binary_name}'. Example values include simple filenames (e.g., 'test_app1') or subdirectory paths (e.g., 'bin/test_app1')." + }, + "application_profile": { + "type": "object", + "description": "Defines the application profile that specifies the runtime behavior and capabilities of this component.", + "properties": { + "application_type": { + "type": "string", + "enum": [ + "Native", + "Reporting", + "Reporting_And_Supervised", + "State_Manager" + ], + "description": "Specifies the level of integration between the component and the Launch Manager. 'Native': no integration with Launch Manager. 'Reporting': uses Launch Manager lifecycle APIs. 'Reporting_And_Supervised': uses lifecycle APIs and sends alive notifications. 'State_Manager': uses lifecycle APIs, sends alive notifications, and has permission to change the active Run Target." + }, + "is_self_terminating": { + "type": "boolean", + "description": "Indicates whether the component is designed to terminate automatically once its planned tasks are completed (true), or remain running until explicitly requested to terminate by the Launch Manager (false)." + }, + "alive_supervision": { + "type": "object", + "description": "Defines the configuration parameters used for alive monitoring of the component.", + "properties": { + "reporting_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the duration, in seconds (e.g., '0.5' for 500 milliseconds), of the time interval used to verify that the component sends alive notifications within the expected time frame." + }, + "failed_cycles_tolerance": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of consecutive reporting cycle failures (see 'reporting_cycle'). Once the number of failed cycles exceeds this maximum, the Launch Manager will trigger the configured recovery action." + }, + "min_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the minimum number of checkpoints that must be reported within each configured 'reporting_cycle'." + }, + "max_indications": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of checkpoints that may be reported within each configured 'reporting_cycle'." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components that this component depends on. Each dependency must be initialized and reach its ready state before the Launch Manager will start this component.", + "items": { + "type": "string", + "description": "Specifies the name of a component on which this component depends." + } + }, + "process_arguments": { + "type": "array", + "description": "Specifies an ordered list of command-line arguments passed to the component at startup.", + "items": { + "type": "string", + "description": "Specifies a single command-line argument token as a UTF-8 string; order is preserved." + } + }, + "ready_condition": { + "type": "object", + "description": "Defines the set of conditions that determine when the component completes its initializing state and enters the ready state.", + "properties": { + "process_state": { + "type": "string", + "enum": [ + "Running", + "Terminated" + ], + "description": "Specifies the required state of the component's POSIX process. 'Running': the process has started and reached its running state. 'Terminated': the process has started, reached its running state, and then terminated successfully." + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "recovery_action": { + "type": "object", + "description": "Defines a reusable type that specifies recovery actions to execute when an error or failure occurs.", + "properties": { + "restart": { + "type": "object", + "description": "Defines a recovery action that restarts the POSIX process associated with this component.", + "properties": { + "number_of_attempts": { + "type": "integer", + "minimum": 0, + "description": "Specifies the maximum number of restart attempts before the Launch Manager concludes that recovery cannot succeed." + }, + "delay_before_restart": { + "type": "number", + "minimum": 0, + "description": "Specifies the delay duration, in seconds (e.g., '0.25' for 250 milliseconds), that the Launch Manager waits before initiating a restart attempt." + } + }, + "required": [], + "additionalProperties": false + }, + "switch_run_target": { + "type": "object", + "description": "Defines a recovery action that switches to a Run Target. This can be a different Run Target or the same one to retry activation of the current Run Target.", + "properties": { + "run_target": { + "type": "string", + "description": "Specifies the name of the Run Target that the Launch Manager should switch to." + } + }, + "required": [], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "restart" + ] + }, + { + "required": [ + "switch_run_target" + ] + } + ], + "additionalProperties": false + }, + "deployment_config": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters that are specific to a particular deployment environment or system setup.", + "properties": { + "ready_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time, in seconds (e.g., '0.25' for 250 milliseconds), allowed for the component to reach its ready state. The timeout is measured from when the component process is created until the ready conditions specified in 'component_properties.ready_condition' are met." + }, + "shutdown_timeout": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the maximum time, in seconds (e.g., '0.75' for 750 milliseconds), allowed for the component to terminate after it receives a SIGTERM signal from the Launch Manager. The timeout is measured from when the Launch Manager sends the SIGTERM signal until the Operating System notifies the Launch Manager that the child process has terminated." + }, + "environmental_variables": { + "type": "object", + "description": "Defines the set of environment variables passed to the component at startup.", + "additionalProperties": { + "type": "string", + "description": "Specifies the environment variable's value as a string. An empty string is allowed and represents an intentionally empty environment variable." + } + }, + "bin_dir": { + "type": "string", + "description": "Specifies the absolute filesystem path to the directory where the component is installed." + }, + "working_dir": { + "type": "string", + "description": "Specifies the directory used as the working directory for the component during execution." + }, + "ready_recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "restart": true + }, + "required": [ + "restart" + ], + "not": { + "required": [ + "switch_run_target" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component fails to reach its ready state within the configured timeout." + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when the component malfunctions after reaching its ready state." + }, + "sandbox": { + "type": "object", + "description": "Defines the sandbox configuration parameters that isolate and constrain the component's runtime execution.", + "properties": { + "uid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the POSIX user ID (UID) under which this component executes." + }, + "gid": { + "type": "integer", + "minimum": 0, + "description": "Specifies the primary POSIX group ID (GID) under which this component executes." + }, + "supplementary_group_ids": { + "type": "array", + "description": "Specifies the list of supplementary POSIX group IDs (GIDs) assigned to this component.", + "items": { + "type": "integer", + "minimum": 0, + "description": "Specifies a single supplementary POSIX group ID (GID)." + } + }, + "security_policy": { + "type": "string", + "description": "Specifies the security policy or confinement profile name (such as an SELinux or AppArmor profile) assigned to the component." + }, + "scheduling_policy": { + "type": "string", + "description": "Specifies the scheduling policy applied to the component's initial thread. Supported values correspond to OS-defined policies (e.g., FIFO, RR, OTHER).", + "anyOf": [ + { + "enum": [ + "SCHED_FIFO", + "SCHED_RR", + "SCHED_OTHER" + ] + }, + { + "type": "string" + } + ] + }, + "scheduling_priority": { + "type": "integer", + "description": "Specifies the scheduling priority applied to the component's initial thread." + }, + "max_memory_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum amount of memory, in bytes, that the component is permitted to use during runtime." + }, + "max_cpu_usage": { + "type": "integer", + "exclusiveMinimum": 0, + "description": "Specifies the maximum CPU usage limit for the component, expressed as a percentage (%) of total CPU capacity." + } + }, + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_target": { + "type": "object", + "description": "Defines a reusable type that specifies configuration parameters for a Run Target.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a user-defined description of the Run Target." + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit, in seconds (e.g., '1.5' for 1500 milliseconds), for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + }, + "recovery_action": { + "allOf": [ + { + "$ref": "#/$defs/recovery_action" + }, + { + "properties": { + "switch_run_target": true + }, + "required": [ + "switch_run_target" + ], + "not": { + "required": [ + "restart" + ] + } + } + ], + "description": "Specifies the recovery action to execute when a component assigned to this Run Target fails." + } + }, + "required": [], + "additionalProperties": false + }, + "alive_supervision": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for alive supervision.", + "properties": { + "evaluation_cycle": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Specifies the length, in seconds (e.g., '0.5' for 500 milliseconds), of the time window used to assess incoming alive supervision reports." + } + }, + "required": [], + "additionalProperties": false + }, + "watchdog": { + "type": "object", + "description": "Defines a reusable type that contains configuration parameters for the external watchdog.", + "properties": { + "device_file_path": { + "type": "string", + "description": "Specifies the path to the external watchdog device file (e.g., /dev/watchdog)." + }, + "max_timeout": { + "type": "number", + "minimum": 0, + "description": "Specifies the maximum timeout value, in seconds (e.g., '0.5' for 500 milliseconds), that the Launch Manager configures on the external watchdog during startup. The external watchdog uses this timeout as the deadline for receiving periodic alive reports from the Launch Manager." + }, + "deactivate_on_shutdown": { + "type": "boolean", + "description": "Specifies whether the Launch Manager disables the external watchdog during shutdown. When set to true, the watchdog is deactivated; when false, it remains active." + }, + "require_magic_close": { + "type": "boolean", + "description": "Specifies whether the Launch Manager performs a defined shutdown sequence to inform the external watchdog that the shutdown is intentional and to prevent a watchdog-initiated reset. When true, the magic close sequence is performed; when false, it is not." + } + }, + "required": [], + "additionalProperties": false + } + }, + "properties": { + "schema_version": { + "type": "integer", + "description": "Specifies the schema version number that the Launch Manager uses to determine how to parse and validate this configuration file.", + "enum": [ + 1 + ] + }, + "defaults": { + "type": "object", + "description": "Defines default configuration values that components and Run Targets inherit unless they provide their own overriding values.", + "properties": { + "component_properties": { + "description": "Defines default component property values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Defines default deployment configuration values applied to all components unless overridden in individual component definitions.", + "$ref": "#/$defs/deployment_config" + }, + "run_target": { + "description": "Defines default Run Target configuration values applied to all Run Targets unless overridden in individual Run Target definitions.", + "$ref": "#/$defs/run_target" + }, + "alive_supervision": { + "description": "Defines default alive supervision configuration values used unless a global 'alive_supervision' configuration is specified at the root level.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Defines default watchdog configuration values applied to all watchdogs unless overridden in individual watchdog definitions.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [], + "additionalProperties": false + }, + "components": { + "type": "object", + "description": "Defines software components managed by the Launch Manager, where each property name is a unique component identifier and its value contains the component's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "type": "object", + "description": "Defines an individual component's configuration properties and deployment settings.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the component's purpose." + }, + "component_properties": { + "description": "Defines component properties for this component; any properties not specified here are inherited from 'defaults.component_properties'.", + "$ref": "#/$defs/component_properties" + }, + "deployment_config": { + "description": "Defines deployment configuration for this component; any properties not specified here are inherited from 'defaults.deployment_config'.", + "$ref": "#/$defs/deployment_config" + } + }, + "required": [], + "additionalProperties": false + } + }, + "required": [], + "additionalProperties": false + }, + "run_targets": { + "type": "object", + "description": "Defines Run Targets representing different operational modes of the system, where each property name is a unique Run Target identifier and its value contains the Run Target's configuration.", + "patternProperties": { + "^[a-zA-Z0-9_-]+$": { + "$ref": "#/$defs/run_target" + } + }, + "required": [], + "additionalProperties": false + }, + "initial_run_target": { + "type": "string", + "description": "Specifies the name of the initial Run Target that the Launch Manager activates during the startup sequence. This name must match a Run Target defined in 'run_targets'." + }, + "fallback_run_target": { + "type": "object", + "description": "Defines the fallback Run Target configuration that the Launch Manager activates when all recovery attempts have been exhausted. This Run Target does not include a recovery_action property.", + "properties": { + "description": { + "type": "string", + "description": "Specifies a human-readable description of the fallback Run Target." + }, + "depends_on": { + "type": "array", + "description": "Specifies the names of components and Run Targets that must be activated when this Run Target is activated.", + "items": { + "type": "string", + "description": "Specifies the name of a component or Run Target that this Run Target depends on." + } + }, + "transition_timeout": { + "type": "number", + "description": "Specifies the time limit, in seconds (e.g., '1.5' for 1500 milliseconds), for the Run Target transition. If this limit is exceeded, the transition is considered failed.", + "exclusiveMinimum": 0 + } + }, + "required": [ + "depends_on" + ], + "additionalProperties": false + }, + "alive_supervision": { + "description": "Defines the alive supervision configuration parameters used to monitor component health. This configuration overrides 'defaults.alive_supervision' if specified.", + "$ref": "#/$defs/alive_supervision" + }, + "watchdog": { + "description": "Defines the external watchdog device configuration used by the Launch Manager. This configuration overrides 'defaults.watchdog' if specified.", + "$ref": "#/$defs/watchdog" + } + }, + "required": [ + "schema_version", + "initial_run_target" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/launch_manager_daemon/config/config_schema/scripts/validate.py b/src/launch_manager_daemon/config/config_schema/scripts/validate.py new file mode 100755 index 00000000..f34418be --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/scripts/validate.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" +Validate JSON instance(s) against a multi-file JSON Schema with relative $ref paths. + +Usage examples: + Validate a single file: + python validate_config.py --schema ./schema/s-core_launch_manager.schema.json --instance ./examples/config.json + + Validate all JSON files in a directory (recursively): + python validate_config.py --schema ./schema/s-core_launch_manager.schema.json --instances-dir ./examples + +Exit codes: + 0 -> all instances are valid + 1 -> at least one instance failed validation or there was an error +""" + +import argparse +import json +import sys +from pathlib import Path + +try: + from jsonschema import validators, RefResolver, FormatChecker + from jsonschema.exceptions import RefResolutionError, SchemaError, ValidationError +except ImportError: + print( + "This script requires the 'jsonschema' package. Install with:\n pip install jsonschema", + file=sys.stderr, + ) + sys.exit(1) + + +def load_json(path: Path): + try: + with path.open("r", encoding="utf-8") as f: + return json.load(f) + except json.JSONDecodeError as e: + raise ValueError(f"Failed to parse JSON file '{path}': {e}") from e + except OSError as e: + raise ValueError(f"Failed to read file '{path}': {e}") from e + + +def json_pointer_path(parts): + """ + Convert an error path into a friendly string like: + $.topLevel.items[2].name + """ + if not parts: + return "$" + s = "$" + for p in parts: + if isinstance(p, int): + s += f"[{p}]" + else: + s += f".{p}" + return s + + +def build_validator(schema_path: Path): + schema = load_json(schema_path) + + # Choose validator based on $schema automatically (Draft-07 / 2019-09 / 2020-12, etc.) + ValidatorClass = validators.validator_for(schema) + # Validate the schema itself (optional but helpful) + try: + ValidatorClass.check_schema(schema) + except SchemaError as e: + raise SchemaError( + f"Your schema appears invalid: {e.message}\nAt: {'/'.join(map(str, e.path))}" + ) from e + + # Base URI for resolving relative $refs like "./types/*.schema.json" + base_uri = schema_path.resolve().parent.as_uri() + "/" + + # RefResolver is deprecated upstream but still widely supported and reliable for local file resolution. + resolver = RefResolver(base_uri=base_uri, referrer=schema) + + # Enable common format checks (e.g., "uri", "email", "date-time") + format_checker = FormatChecker() + + return ValidatorClass(schema, resolver=resolver, format_checker=format_checker) + + +def validate_instance(validator, instance_path: Path): + instance = load_json(instance_path) + errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.path)) + return errors + + +def find_json_files(root: Path): + # Recurse and pick *.json files only + return [p for p in root.rglob("*.json") if p.is_file()] + + +def main(): + parser = argparse.ArgumentParser( + description="Validate JSON instance(s) against a multi-file JSON Schema." + ) + parser.add_argument( + "--schema", + required=True, + type=Path, + help="Path to the top-level schema (e.g., ./schema/s-core_launch_manager.schema.json)", + ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "--instance", type=Path, help="Path to a single JSON instance to validate" + ) + group.add_argument( + "--instances-dir", + type=Path, + help="Path to a directory containing JSON instances (recursively)", + ) + parser.add_argument( + "--stop-on-first", + action="store_true", + help="Stop after the first instance with errors", + ) + args = parser.parse_args() + + try: + validator = build_validator(args.schema) + except (ValueError, SchemaError) as e: + print(f"[Schema Error] {e}", file=sys.stderr) + sys.exit(1) + + instance_paths = [] + if args.instance: + instance_paths = [args.instance] + else: + if not args.instances_dir.exists(): + print( + f"[Error] Instances directory not found: {args.instances_dir}", + file=sys.stderr, + ) + sys.exit(1) + instance_paths = find_json_files(args.instances_dir) + if not instance_paths: + print(f"[Info] No JSON files found under: {args.instances_dir}") + sys.exit(0) + + any_failed = False + for path in instance_paths: + try: + errors = validate_instance(validator, path) + except ValueError as e: + print(f"Error --> {path}: {e}", file=sys.stderr) + any_failed = True + if args.stop_on_first: + break + continue + except RefResolutionError as e: + print(f"Error --> {path}: Failed to resolve a $ref - {e}", file=sys.stderr) + print(" Tips:") + print( + " * Ensure $ref paths like './types/...' are correct relative to the top-level schema file." + ) + print(" * Make sure referenced files exist and are valid JSON schemas.") + any_failed = True + if args.stop_on_first: + break + continue + + if not errors: + print(f"Success --> {path}: valid") + else: + any_failed = True + print(f"Error --> {path}: {len(errors)} error(s)") + for i, err in enumerate(errors, 1): + instance_loc = json_pointer_path(err.path) + schema_loc = ( + "/".join(map(str, err.schema_path)) if err.schema_path else "(root)" + ) + print(f" [{i}] at {instance_loc}") + print(f" --> {err.message}") + print(f" schema path: {schema_loc}") + if args.stop_on_first: + break + + sys.exit(1 if any_failed else 0) + + +if __name__ == "__main__": + main()