mirror of
https://forge.fsky.io/oneflux/omegafox.git
synced 2026-02-10 14:02:04 -08:00
262 lines
9 KiB
Python
262 lines
9 KiB
Python
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Union
|
|
|
|
from .exceptions import InvalidPropertyType
|
|
from .strings import string_validator
|
|
|
|
TYPE_NAMES = {'array', 'tuple', 'str', 'int', 'double', 'bool', 'any', 'nil', 'tuple'}
|
|
|
|
|
|
class Type(ABC):
|
|
@abstractmethod
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, 'Type']) -> None:
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class BaseType(Type):
|
|
"""Base class for all types"""
|
|
|
|
name: str
|
|
conditions: Optional[str] = None
|
|
|
|
def __post_init__(self):
|
|
# Raise error early
|
|
if not self.name.startswith('@') and self.name not in TYPE_NAMES:
|
|
raise InvalidPropertyType(f'Unknown base type {self.name}')
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if self.name in type_registry:
|
|
type_registry[self.name].validate(value, path, type_registry)
|
|
else:
|
|
raise RuntimeError(f'Unknown base type {self.name}')
|
|
|
|
|
|
@dataclass
|
|
class NilType(Type):
|
|
"""Represents a nil/null type"""
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if value is not None:
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected nil, got {value}"
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return "nil"
|
|
|
|
|
|
@dataclass
|
|
class StringType(Type):
|
|
pattern: Optional[str] = None
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if not isinstance(value, str):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected string, got {type(value).__name__}"
|
|
)
|
|
|
|
if self.pattern:
|
|
if not string_validator(value, self.pattern):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: {value} does not match pattern '{self.pattern}'"
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"str[{self.pattern}]" if self.pattern else "str"
|
|
|
|
|
|
@dataclass
|
|
class NumericalType(Type):
|
|
conditions: Optional[str] = None
|
|
numeric_type: Type = float # Default to float
|
|
type_name: str = "number" # For error messages
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
allowed_types = (int, float) if self.numeric_type is float else (int,)
|
|
if not isinstance(value, allowed_types):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected {self.type_name}, got {type(value).__name__}"
|
|
)
|
|
if self.conditions and not self._check_conditions(self.numeric_type(value)):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: {value} does not match conditions '{self.conditions}'"
|
|
)
|
|
|
|
def _check_conditions(self, value: Union[int, float]) -> bool:
|
|
if not self.conditions:
|
|
return True
|
|
|
|
# Split by comma and handle each condition
|
|
conditions = [c.strip() for c in self.conditions.split(',')]
|
|
|
|
for condition in conditions:
|
|
try:
|
|
# Handle comparisons
|
|
if '>=' in condition:
|
|
if value >= self.numeric_type(condition.replace('>=', '')):
|
|
return True
|
|
elif '<=' in condition:
|
|
if value <= self.numeric_type(condition.replace('<=', '')):
|
|
return True
|
|
elif '>' in condition:
|
|
if value > self.numeric_type(condition.replace('>', '')):
|
|
return True
|
|
elif '<' in condition:
|
|
if value < self.numeric_type(condition.replace('<', '')):
|
|
return True
|
|
# Handle ranges (e.g., "1.5-5.5")
|
|
elif '-' in condition[1:]:
|
|
# split by the -, ignoring the first character
|
|
range_s, range_e = condition[1:].split('-', 1)
|
|
range_s = self.numeric_type(condition[0] + range_s)
|
|
range_e = self.numeric_type(range_e)
|
|
if range_s <= value <= range_e:
|
|
return True
|
|
# Handle single values
|
|
else:
|
|
if value == self.numeric_type(condition):
|
|
return True
|
|
except ValueError:
|
|
continue
|
|
|
|
return False
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.type_name}[{self.conditions}]" if self.conditions else self.type_name
|
|
|
|
|
|
@dataclass
|
|
class IntType(NumericalType):
|
|
def __init__(self, conditions: Optional[str] = None):
|
|
super().__init__(conditions=conditions, numeric_type=int, type_name="int")
|
|
|
|
|
|
@dataclass
|
|
class DoubleType(NumericalType):
|
|
def __init__(self, conditions: Optional[str] = None):
|
|
super().__init__(conditions=conditions, numeric_type=float, type_name="double")
|
|
|
|
|
|
@dataclass
|
|
class AnyType(Type):
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
# Any type accepts all values
|
|
pass
|
|
|
|
def __str__(self) -> str:
|
|
return "any"
|
|
|
|
|
|
@dataclass
|
|
class BoolType(Type):
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if not isinstance(value, bool):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected bool, got {type(value).__name__}"
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class ArrayType(Type):
|
|
element_type: Type
|
|
length_conditions: Optional[str] = None
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if not isinstance(value, list):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected array, got {type(value).__name__}"
|
|
)
|
|
|
|
if self.length_conditions:
|
|
array_len = len(value)
|
|
length_validator = IntType(self.length_conditions)
|
|
try:
|
|
length_validator._check_conditions(array_len)
|
|
except Exception:
|
|
raise InvalidPropertyType(
|
|
f"Invalid array length at {'.'.join(path)}: got length {array_len}"
|
|
)
|
|
|
|
for i, item in enumerate(value):
|
|
self.element_type.validate(item, path + [str(i)], type_registry)
|
|
|
|
|
|
@dataclass
|
|
class TupleType(Type):
|
|
element_types: List[Type]
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
if not isinstance(value, (list, tuple)):
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: expected tuple, got {type(value).__name__}"
|
|
)
|
|
|
|
if len(value) != len(self.element_types):
|
|
raise InvalidPropertyType(
|
|
f"Invalid tuple length at {'.'.join(path)}: expected {len(self.element_types)}, got {len(value)}"
|
|
)
|
|
|
|
for i, (item, expected_type) in enumerate(zip(value, self.element_types)):
|
|
expected_type.validate(item, path + [str(i)], type_registry)
|
|
|
|
|
|
@dataclass
|
|
class UnionType(Type):
|
|
types: List[Type]
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
errors = []
|
|
for t in self.types:
|
|
try:
|
|
t.validate(value, path, type_registry)
|
|
return # If any type validates successfully, we're done
|
|
except InvalidPropertyType as e:
|
|
errors.append(str(e))
|
|
|
|
# If we get here, none of the types validated
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {'.'.join(path)}: {value} does not match any of the allowed types"
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"({' | '.join(str(t) for t in self.types)})"
|
|
|
|
|
|
@dataclass
|
|
class SubtractionType(Type):
|
|
base_type: Type
|
|
subtracted_type: Type
|
|
|
|
def validate(self, value: Any, path: List[str], type_registry: Dict[str, Type]) -> None:
|
|
path_str = '.'.join(path)
|
|
|
|
# First check if value matches base type
|
|
matches_base = True
|
|
try:
|
|
self.base_type.validate(value, path, type_registry)
|
|
except InvalidPropertyType:
|
|
matches_base = False
|
|
raise
|
|
|
|
# Then check if value matches subtracted type
|
|
matches_subtracted = True
|
|
try:
|
|
self.subtracted_type.validate(value, path, type_registry)
|
|
matches_subtracted = True
|
|
except InvalidPropertyType:
|
|
matches_subtracted = False
|
|
|
|
# Final validation decision
|
|
if matches_base and matches_subtracted:
|
|
raise InvalidPropertyType(f"Invalid value at {path_str}: {value} matches excluded type")
|
|
elif matches_base and not matches_subtracted:
|
|
return
|
|
else:
|
|
raise InvalidPropertyType(
|
|
f"Invalid value at {path_str}: {value} does not match base type"
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return f"({self.base_type} - {self.subtracted_type})"
|