Source code for sec_interp.core.validation.field_validator
from __future__ import annotations
"""Validation logic for QGIS layer fields and attributes."""
from qgis.core import QgsVectorLayer
from sec_interp.core.types import FieldType
[docs]
def validate_numeric_input(
value: str,
min_val: float | None = None,
max_val: float | None = None,
field_name: str = "Value",
allow_empty: bool = False,
) -> tuple[bool, str, float | None]:
"""Validate a numeric input string from a text field.
Args:
value: The string value to validate.
min_val: Optional minimum value allowed.
max_val: Optional maximum value allowed.
field_name: Name of the field for error messages.
allow_empty: Whether to allow an empty string.
Returns:
tuple: (is_valid, error_message, float_value)
- is_valid: True if validation passed.
- error_message: Error details if validation failed.
- float_value: The parsed numeric value if valid, else None.
"""
if not value or value.strip() == "":
if allow_empty:
return True, "", None
return False, f"{field_name} is required", None
try:
num_value = float(value)
except (ValueError, TypeError):
return False, f"{field_name} must be a valid number", None
if min_val is not None and num_value < min_val:
return False, f"{field_name} must be at least {min_val}", None
if max_val is not None and num_value > max_val:
return False, f"{field_name} must be at most {max_val}", None
return True, "", num_value
[docs]
def validate_integer_input(
value: str,
min_val: int | None = None,
max_val: int | None = None,
field_name: str = "Value",
allow_empty: bool = False,
) -> tuple[bool, str, int | None]:
"""Validate an integer input string from a text field.
Args:
value: The string value to validate.
min_val: Optional minimum value allowed.
max_val: Optional maximum value allowed.
field_name: Name of the field for error messages.
allow_empty: Whether to allow an empty string.
Returns:
tuple: (is_valid, error_message, int_value)
- is_valid: True if validation passed.
- error_message: Error details if validation failed.
- int_value: The parsed integer value if valid, else None.
"""
if not value or value.strip() == "":
if allow_empty:
return True, "", None
return False, f"{field_name} is required", None
try:
int_value = int(value)
except (ValueError, TypeError):
return False, f"{field_name} must be a valid integer", None
if min_val is not None and int_value < min_val:
return False, f"{field_name} must be at least {min_val}", None
if max_val is not None and int_value > max_val:
return False, f"{field_name} must be at most {max_val}", None
return True, "", int_value
[docs]
def validate_angle_range(
value: float, field_name: str, min_angle: float = 0.0, max_angle: float = 360.0
) -> tuple[bool, str]:
"""Validate that an angle value is within the expected range.
Args:
value: The angle value to validate.
field_name: Name of the field for error messages.
min_angle: Minimum allowed angle (default 0.0).
max_angle: Maximum allowed angle (default 360.0).
Returns:
tuple: (is_valid, error_message)
- is_valid: True if validation passed.
- error_message: Error details if validation failed.
"""
if value < min_angle or value > max_angle:
return (
False,
f"{field_name} must be between {min_angle} and {max_angle} degrees",
)
return True, ""
[docs]
def validate_field_exists(layer: QgsVectorLayer, field_name: str | None) -> tuple[bool, str]:
"""Validate that a specific field exists in a vector layer.
Args:
layer: The QGIS vector layer to check.
field_name: The name of the field to search for.
Returns:
tuple: (is_valid, error_message)
- is_valid: True if validation passed.
- error_message: Error details if validation failed.
"""
if not layer:
return False, "Layer is None"
if not field_name:
return False, "Field name is required"
if not isinstance(layer, QgsVectorLayer):
return (
False,
f"Layer '{layer.name() if hasattr(layer, 'name') else 'Unknown'}' is not a vector layer",
)
field_names = [field.name() for field in layer.fields()]
if field_name not in field_names:
return False, (
f"Field '{field_name}' not found in layer '{layer.name()}'. "
f"Available fields: {', '.join(field_names[:5])}"
f"{', ...' if len(field_names) > 5 else ''}"
)
return True, ""
[docs]
def validate_field_type(
layer: QgsVectorLayer, field_name: str, expected_types: list[FieldType]
) -> tuple[bool, str]:
"""Validate that a field in a layer has one of the expected data types.
Args:
layer: The QGIS vector layer containing the field.
field_name: The name of the field to check.
expected_types: List of allowed FieldType values.
Returns:
tuple: (is_valid, error_message)
- is_valid: True if validation passed.
- error_message: Error details if validation failed.
"""
if not layer:
return False, "Layer is None"
if not isinstance(layer, QgsVectorLayer):
return (
False,
f"Layer '{layer.name() if hasattr(layer, 'name') else 'Unknown'}' is not a vector layer",
)
field = layer.fields().field(field_name)
if not field:
return False, f"Field '{field_name}' not found in layer '{layer.name()}'"
if field.type() not in expected_types:
type_names = {
FieldType.INT: "Integer",
FieldType.DOUBLE: "Double",
FieldType.STRING: "String",
FieldType.LONG_LONG: "Long Integer",
FieldType.DATE: "Date",
FieldType.DATE_TIME: "DateTime",
}
expected_names = [type_names.get(t, str(t)) for t in expected_types]
actual_name = type_names.get(field.type(), f"Type ID {field.type()}")
return False, (
"Invalid data type for field '{field_name}' in layer '{layer.name()}'. "
f"Found: {actual_name}. Expected one of: {', '.join(expected_names)}. "
f"Please check your attribute table."
)
return True, ""