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

This commit is contained in:
2026-01-29 10:49:03 +00:00
parent 0bd3793525
commit 406c0d2246

View 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 ""