Initial upload: ConfDoc v0.1.0 - Config validation and documentation generator
This commit is contained in:
148
src/confdoc/validator/errors.py
Normal file
148
src/confdoc/validator/errors.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorSeverity(Enum):
|
||||||
|
"""Classification of validation error severity."""
|
||||||
|
CRITICAL = "critical"
|
||||||
|
ERROR = "error"
|
||||||
|
WARNING = "warning"
|
||||||
|
INFO = "info"
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError:
|
||||||
|
"""Represents a validation error with location and severity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
message: str,
|
||||||
|
path: Optional[str] = None,
|
||||||
|
line: Optional[int] = None,
|
||||||
|
column: Optional[int] = None,
|
||||||
|
severity: ErrorSeverity = ErrorSeverity.ERROR,
|
||||||
|
suggestion: Optional[str] = None,
|
||||||
|
validator: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.message = message
|
||||||
|
self.path = path
|
||||||
|
self.line = line
|
||||||
|
self.column = column
|
||||||
|
self.severity = severity
|
||||||
|
self.suggestion = suggestion
|
||||||
|
self.validator = validator
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert error to dictionary."""
|
||||||
|
return {
|
||||||
|
"message": self.message,
|
||||||
|
"path": self.path,
|
||||||
|
"line": self.line,
|
||||||
|
"column": self.column,
|
||||||
|
"severity": self.severity.value,
|
||||||
|
"suggestion": self.suggestion,
|
||||||
|
"validator": self.validator,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Format error as string."""
|
||||||
|
location = ""
|
||||||
|
if self.path:
|
||||||
|
location = f" in '{self.path}'"
|
||||||
|
elif self.line:
|
||||||
|
location = f" at line {self.line}"
|
||||||
|
if self.column:
|
||||||
|
location += f", column {self.column}"
|
||||||
|
|
||||||
|
result = f"{self.message}{location}"
|
||||||
|
if self.suggestion:
|
||||||
|
result += f". Suggestion: {self.suggestion}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"ValidationError(message='{self.message}', path='{self.path}', severity={self.severity.value})"
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorFormatter:
|
||||||
|
"""Formats validation errors for display."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_error(error: ValidationError, color: bool = True) -> str:
|
||||||
|
"""Format a single error."""
|
||||||
|
prefix = ""
|
||||||
|
if color:
|
||||||
|
if error.severity == ErrorSeverity.CRITICAL:
|
||||||
|
prefix = "[red][CRITICAL][/red] "
|
||||||
|
elif error.severity == ErrorSeverity.ERROR:
|
||||||
|
prefix = "[red]✗[/red] "
|
||||||
|
elif error.severity == ErrorSeverity.WARNING:
|
||||||
|
prefix = "[yellow]![/yellow] "
|
||||||
|
else:
|
||||||
|
prefix = "[blue]i[/blue] "
|
||||||
|
|
||||||
|
location = ""
|
||||||
|
if error.path:
|
||||||
|
location = f" [dim]({error.path})[/dim]"
|
||||||
|
elif error.line:
|
||||||
|
location = f" [dim](line {error.line})[/dim]"
|
||||||
|
|
||||||
|
suggestion = ""
|
||||||
|
if error.suggestion:
|
||||||
|
suggestion = f"\n [dim]→ {error.suggestion}[/dim]"
|
||||||
|
|
||||||
|
return f"{prefix}{error.message}{location}{suggestion}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_errors(errors: List[ValidationError], color: bool = True) -> str:
|
||||||
|
"""Format a list of errors."""
|
||||||
|
if not errors:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for i, error in enumerate(errors, 1):
|
||||||
|
lines.append(f"{i}. {ErrorFormatter.format_error(error, color)}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def classify_severity(validator_error: Dict[str, Any]) -> ErrorSeverity:
|
||||||
|
"""Classify the severity of a jsonschema error."""
|
||||||
|
validator = validator_error.get("validator", "")
|
||||||
|
|
||||||
|
if validator in ("required", "additionalProperties"):
|
||||||
|
return ErrorSeverity.CRITICAL
|
||||||
|
elif validator in ("type", "enum"):
|
||||||
|
return ErrorSeverity.ERROR
|
||||||
|
elif validator in ("minimum", "maximum", "minLength", "maxLength", "pattern"):
|
||||||
|
return ErrorSeverity.WARNING
|
||||||
|
else:
|
||||||
|
return ErrorSeverity.INFO
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_suggestion(validator_error: Dict[str, Any]) -> Optional[str]:
|
||||||
|
"""Generate a suggestion for fixing a validation error."""
|
||||||
|
validator = validator_error.get("validator", "")
|
||||||
|
validator_value = validator_error.get("validator_value", "")
|
||||||
|
|
||||||
|
if validator == "required":
|
||||||
|
missing = validator_error.get("missing_property", "")
|
||||||
|
return f"Add the required property '{missing}' to your configuration."
|
||||||
|
elif validator == "type":
|
||||||
|
expected_type = validator_error.get("expected", "")
|
||||||
|
actual_type = validator_error.get("found", "")
|
||||||
|
return f"Expected type {expected_type}, but got {actual_type}."
|
||||||
|
elif validator == "enum":
|
||||||
|
allowed = ", ".join(validator_value) if isinstance(validator_value, list) else str(validator_value)
|
||||||
|
return f"Value must be one of: {allowed}"
|
||||||
|
elif validator == "minimum":
|
||||||
|
return f"Value must be greater than or equal to {validator_value}."
|
||||||
|
elif validator == "maximum":
|
||||||
|
return f"Value must be less than or equal to {validator_value}."
|
||||||
|
elif validator == "minLength":
|
||||||
|
return f"Value must have at least {validator_value} characters."
|
||||||
|
elif validator == "maxLength":
|
||||||
|
return f"Value must have at most {validator_value} characters."
|
||||||
|
elif validator == "pattern":
|
||||||
|
return f"Value must match the pattern: {validator_value}"
|
||||||
|
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user