Add commands and formatters modules
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.8) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build-package (push) Has been cancelled
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.8) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / build-package (push) Has been cancelled
This commit is contained in:
157
configforge/commands/validate.py
Normal file
157
configforge/commands/validate.py
Normal file
@@ -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 ""
|
||||
Reference in New Issue
Block a user