133 lines
3.6 KiB
Python
133 lines
3.6 KiB
Python
"""Syntax validation for config formats."""
|
|
|
|
import json
|
|
import re
|
|
from typing import Optional, Tuple
|
|
|
|
import yaml
|
|
|
|
|
|
ENV_LINE_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*\s*=(.*)$")
|
|
|
|
|
|
class ValidationError:
|
|
"""Represents a validation error with location info."""
|
|
|
|
def __init__(self, message: str, line: Optional[int] = None, column: Optional[int] = None):
|
|
self.message = message
|
|
self.line = line
|
|
self.column = column
|
|
|
|
def __str__(self) -> str:
|
|
if self.line is not None:
|
|
if self.column is not None:
|
|
return f"Line {self.line}, Column {self.column}: {self.message}"
|
|
return f"Line {self.line}: {self.message}"
|
|
return self.message
|
|
|
|
|
|
def validate_json(data: str) -> Tuple[bool, Optional[ValidationError]]:
|
|
"""Validate JSON syntax.
|
|
|
|
Args:
|
|
data: The JSON string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error)
|
|
"""
|
|
try:
|
|
json.loads(data)
|
|
return True, None
|
|
except json.JSONDecodeError as e:
|
|
return False, ValidationError(str(e), e.lineno, e.colno)
|
|
|
|
|
|
def validate_yaml(data: str) -> Tuple[bool, Optional[ValidationError]]:
|
|
"""Validate YAML syntax.
|
|
|
|
Args:
|
|
data: The YAML string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error)
|
|
"""
|
|
try:
|
|
yaml.safe_load(data)
|
|
return True, None
|
|
except yaml.YAMLError as e:
|
|
lineno = getattr(e, "lineno", None)
|
|
colno = getattr(e, "column", None)
|
|
problem = getattr(e, "problem", str(e))
|
|
if lineno is not None:
|
|
return False, ValidationError(problem, lineno, colno)
|
|
return False, ValidationError(problem)
|
|
|
|
|
|
def validate_toml(data: str) -> Tuple[bool, Optional[ValidationError]]:
|
|
"""Validate TOML syntax.
|
|
|
|
Args:
|
|
data: The TOML string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error)
|
|
"""
|
|
import sys
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
else:
|
|
import tomli as tomllib
|
|
|
|
try:
|
|
tomllib.loads(data)
|
|
return True, None
|
|
except Exception as e:
|
|
error_str = str(e)
|
|
line_match = re.search(r"line (\d+)", error_str)
|
|
line = int(line_match.group(1)) if line_match else None
|
|
return False, ValidationError(error_str, line)
|
|
|
|
|
|
def validate_env(data: str) -> Tuple[bool, Optional[ValidationError]]:
|
|
"""Validate ENV format syntax.
|
|
|
|
Args:
|
|
data: The ENV string to validate
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error)
|
|
"""
|
|
lines = data.splitlines()
|
|
for line_num, line in enumerate(lines, 1):
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
if not line.strip() == line:
|
|
return False, ValidationError("Invalid KEY=value format", line_num)
|
|
if not ENV_LINE_PATTERN.match(line):
|
|
return False, ValidationError("Invalid KEY=value format", line_num)
|
|
return True, None
|
|
|
|
|
|
def validate(data: str, format: str) -> Tuple[bool, Optional[ValidationError]]:
|
|
"""Validate syntax for any supported format.
|
|
|
|
Args:
|
|
data: The string to validate
|
|
format: The format to validate (json, yaml, toml, env)
|
|
|
|
Returns:
|
|
Tuple of (is_valid, error)
|
|
"""
|
|
format_lower = format.lower()
|
|
if format_lower == "json":
|
|
return validate_json(data)
|
|
elif format_lower == "yaml":
|
|
return validate_yaml(data)
|
|
elif format_lower == "toml":
|
|
return validate_toml(data)
|
|
elif format_lower == "env":
|
|
return validate_env(data)
|
|
else:
|
|
return False, ValidationError(f"Unsupported format: {format}")
|