diff --git a/envschema/validators.py b/envschema/validators.py new file mode 100644 index 0000000..65afcbf --- /dev/null +++ b/envschema/validators.py @@ -0,0 +1,153 @@ +"""Type validators for environment variable values.""" + +import re +from typing import Optional + +from envschema.schema import EnvVarType + + +class ValidationError: + """Represents a validation error.""" + + def __init__(self, message: str, value: Optional[str] = None): + self.message = message + self.value = value + + def __str__(self) -> str: + if self.value is not None: + return f"{self.message} (got: {self.value!r})" + return self.message + + +class StringValidator: + """Validator for string type - always passes.""" + + @staticmethod + def validate(value: Optional[str]) -> tuple[bool, Optional[ValidationError]]: + if value is None: + return True, None + return True, None + + +class IntegerValidator: + """Validator for integer type.""" + + @staticmethod + def validate(value: Optional[str]) -> tuple[bool, Optional[ValidationError]]: + if value is None: + return True, None + try: + int(value) + return True, None + except ValueError: + return False, ValidationError( + f"Invalid integer value", + value=value + ) + + +class BooleanValidator: + """Validator for boolean type.""" + + TRUE_VALUES = {"true", "1", "yes", "on"} + FALSE_VALUES = {"false", "0", "no", "off"} + + @staticmethod + def validate(value: Optional[str]) -> tuple[bool, Optional[ValidationError]]: + if value is None: + return True, None + value_lower = value.lower().strip() + if value_lower in BooleanValidator.TRUE_VALUES: + return True, None + if value_lower in BooleanValidator.FALSE_VALUES: + return True, None + return False, ValidationError( + f"Invalid boolean value (expected: true, false, 1, 0, yes, no, on, off)", + value=value + ) + + +class ListValidator: + """Validator for list type (comma-separated values).""" + + @staticmethod + def validate(value: Optional[str]) -> tuple[bool, Optional[ValidationError]]: + if value is None: + return True, None + if "," in value: + return True, None + return False, ValidationError( + f"Invalid list value (expected comma-separated values)", + value=value + ) + + @staticmethod + def parse(value: str) -> list[str]: + """Parse a comma-separated string into a list. + + Args: + value: Comma-separated string. + + Returns: + List of values. + """ + return [item.strip() for item in value.split(",") if item.strip()] + + +class PatternValidator: + """Validator for pattern/regex validation.""" + + @staticmethod + def validate(value: Optional[str], pattern: str) -> tuple[bool, Optional[ValidationError]]: + if value is None: + return True, None + try: + if re.match(pattern, value): + return True, None + return False, ValidationError( + f"Value does not match pattern: {pattern}", + value=value + ) + except re.error: + return False, ValidationError( + f"Invalid regex pattern: {pattern}", + value=value + ) + + +def get_validator(var_type: EnvVarType): + """Get the validator class for a given type. + + Args: + var_type: The environment variable type. + + Returns: + Validator class. + """ + validators = { + EnvVarType.STRING: StringValidator, + EnvVarType.INTEGER: IntegerValidator, + EnvVarType.BOOLEAN: BooleanValidator, + EnvVarType.LIST: ListValidator, + } + return validators.get(var_type, StringValidator) + + +def validate_value(value: Optional[str], var_type: EnvVarType, pattern: Optional[str] = None) -> tuple[bool, Optional[ValidationError]]: + """Validate a value against a type and optional pattern. + + Args: + value: The value to validate. + var_type: The expected type. + pattern: Optional regex pattern. + + Returns: + Tuple of (is_valid, error). + """ + validator = get_validator(var_type) + is_valid, error = validator.validate(value) + + if is_valid and pattern and value is not None: + is_valid, error = PatternValidator.validate(value, pattern) + + return is_valid, error \ No newline at end of file