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