From 406c0d22462101b3f5620c20376864fdaca5805e Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 29 Jan 2026 10:49:03 +0000 Subject: [PATCH] Add commands and formatters modules --- configforge/commands/validate.py | 157 +++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 configforge/commands/validate.py diff --git a/configforge/commands/validate.py b/configforge/commands/validate.py new file mode 100644 index 0000000..5cac18e --- /dev/null +++ b/configforge/commands/validate.py @@ -0,0 +1,157 @@ +"""Validation command for ConfigForge.""" + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +import click +from jsonschema import ValidationError +from jsonschema import validate as jsonschema_validate + +from configforge.exceptions import ( + FileOperationError, + InvalidConfigFormatError, + SchemaValidationError, +) +from configforge.formatters import JSONHandler, YAMLHandler +from configforge.utils import FileParser +from configforge.utils.file_parser import detect_format + + +@click.command("validate") +@click.argument("config_file", type=click.Path(exists=True)) +@click.argument("schema_file", type=click.Path(exists=True)) +@click.option( + "--format", + type=click.Choice(["json", "yaml", "table"]), + default="table", + help="Output format for validation results", +) +@click.option( + "--dry-run", + is_flag=True, + default=False, + help="Show validation errors but always exit with 0", +) +@click.option( + "--verbose", + "-v", + is_flag=True, + default=False, + help="Show detailed error information", +) +def validate( + config_file: str, + schema_file: str, + format: str, + dry_run: bool, + verbose: bool, +) -> None: + """Validate a configuration file against a JSON Schema.""" + try: + config_data = FileParser.parse(config_file) + schema_data = _load_schema(schema_file) + + errors = _validate_against_schema(config_data, schema_data) + + if format == "json": + output = _format_json_errors(errors, verbose) + click.echo(output) + elif format == "yaml": + output = _format_yaml_errors(errors, verbose) + click.echo(output) + else: + output = _format_table_errors(errors, verbose) + if output: + click.echo(output) + + if errors and not dry_run: + click.get_current_context().exit(1) + + except (InvalidConfigFormatError, FileOperationError) as e: + click.echo(f"Error: {e.message}", err=True) + click.get_current_context().exit(1) + except Exception as e: + click.echo(f"Unexpected error: {str(e)}", err=True) + click.get_current_context().exit(1) + + +def _load_schema(schema_file: str) -> Dict[str, Any]: + """Load a JSON Schema from a file.""" + format_type = detect_format(schema_file) + + if format_type == "json": + return JSONHandler.read(schema_file) + elif format_type in ("yaml", "yml"): + return YAMLHandler.read(schema_file) + else: + raise InvalidConfigFormatError( + f"Unsupported schema format: {format_type}. Use JSON or YAML." + ) + + +def _validate_against_schema( + config: Dict[str, Any], schema: Dict[str, Any] +) -> List[Dict[str, Any]]: + """Validate configuration against a schema.""" + errors = [] + + try: + jsonschema_validate(instance=config, schema=schema) + except ValidationError as e: + error = _format_validation_error(e) + errors.append(error) + + return errors + + +def _format_validation_error(error: ValidationError) -> Dict[str, Any]: + """Format a jsonschema ValidationError.""" + path = list(error.absolute_path) if error.absolute_path else [] + message = error.message + + return { + "path": path, + "message": message, + "validator": error.validator, + "validator_value": error.validator_value, + "constraint": error.validator_value if error.validator in ("enum", "const") else None, + "severity": "error", + } + + +def _format_json_errors(errors: List[Dict[str, Any]], verbose: bool) -> str: + """Format validation errors as JSON.""" + if not verbose: + errors = [{"path": e["path"], "message": e["message"]} for e in errors] + return json.dumps({"valid": not errors, "errors": errors}, indent=2) + + +def _format_yaml_errors(errors: List[Dict[str, Any]], verbose: bool) -> str: + """Format validation errors as YAML.""" + output = {"valid": not errors, "errors": errors} + return YAMLHandler.dumps(output) + + +def _format_table_errors(errors: List[Dict[str, Any]], verbose: bool) -> str: + """Format validation errors as a table.""" + if not errors: + click.secho("✓ Configuration is valid", fg="green") + return "" + + click.secho(f"✗ Found {len(errors)} validation error(s):", fg="red") + click.echo("") + + for i, error in enumerate(errors, 1): + path = ".".join(str(p) for p in error.get("path", [])) or "root" + click.secho(f"{i}. Path: {path}", fg="yellow") + click.echo(f" Message: {error['message']}") + + if verbose: + if "validator" in error: + click.echo(f" Validator: {error['validator']}") + if "validator_value" in error: + click.echo(f" Value: {error['validator_value']}") + click.echo("") + + return ""