swiftops-backend / src /app /utils /field_validation_defaults.py
kamau1's picture
feat: add smart validation for activation requirements with 6 new field types and auto-generated validation rules
dddd6c6
"""
Field Validation Defaults - Smart defaults for activation requirement types
Provides sensible validation rules, placeholders, and help text based on field type.
Managers only specify the type, system fills in the rest automatically.
"""
from typing import Dict, Any, Optional
# Smart defaults by field type
FIELD_TYPE_DEFAULTS = {
"phone": {
"min_length": 10,
"max_length": 15,
"pattern": r"^\+?[0-9]{10,15}$",
"placeholder": "+254XXXXXXXXX",
"help_text": "Enter phone number with country code"
},
"email": {
"min_length": 5,
"max_length": 100,
"pattern": r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
"placeholder": "user@example.com",
"help_text": "Enter valid email address"
},
"mac_address": {
"min_length": 17,
"max_length": 17,
"pattern": r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$",
"placeholder": "AA:BB:CC:DD:EE:FF",
"help_text": "Enter MAC address (format: AA:BB:CC:DD:EE:FF)"
},
"serial_number": {
"min_length": 6,
"max_length": 30,
"pattern": r"^[A-Z0-9\-]+$",
"placeholder": "ABC123XYZ",
"help_text": "Enter equipment serial number"
},
"ip_address": {
"min_length": 7,
"max_length": 15,
"pattern": r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
"placeholder": "192.168.1.1",
"help_text": "Enter IPv4 address"
},
"url": {
"min_length": 10,
"max_length": 500,
"pattern": r"^https?://[^\s/$.?#].[^\s]*$",
"placeholder": "https://example.com",
"help_text": "Enter valid URL starting with http:// or https://"
},
"text": {
"min_length": 1,
"max_length": 255,
"pattern": None,
"placeholder": None, # Will use field label
"help_text": None
},
"number": {
"min_length": None,
"max_length": None,
"pattern": r"^-?[0-9]+(\.[0-9]+)?$",
"placeholder": "0",
"help_text": "Enter numeric value"
},
"date": {
"min_length": None,
"max_length": None,
"pattern": r"^\d{4}-\d{2}-\d{2}$",
"placeholder": "YYYY-MM-DD",
"help_text": "Select date"
},
"boolean": {
"min_length": None,
"max_length": None,
"pattern": None,
"placeholder": None,
"help_text": None
},
"select": {
"min_length": None,
"max_length": None,
"pattern": None,
"placeholder": "Select an option",
"help_text": None
}
}
def apply_field_defaults(field_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Apply smart defaults to field configuration based on type
Args:
field_config: Field configuration dict (from ActivationRequirement)
Returns:
Enhanced field config with defaults applied (non-destructive)
"""
field_type = field_config.get("type", "text")
defaults = FIELD_TYPE_DEFAULTS.get(field_type, FIELD_TYPE_DEFAULTS["text"])
# Create enhanced config (don't modify original)
enhanced = field_config.copy()
# Apply defaults only if not already specified
if enhanced.get("min_length") is None and defaults["min_length"] is not None:
enhanced["min_length"] = defaults["min_length"]
if enhanced.get("max_length") is None and defaults["max_length"] is not None:
enhanced["max_length"] = defaults["max_length"]
# Handle pattern (check both 'pattern' and deprecated 'validation_regex')
if not enhanced.get("pattern") and not enhanced.get("validation_regex"):
if defaults["pattern"]:
enhanced["pattern"] = defaults["pattern"]
elif enhanced.get("validation_regex") and not enhanced.get("pattern"):
# Migrate deprecated field
enhanced["pattern"] = enhanced["validation_regex"]
if enhanced.get("placeholder") is None:
if defaults["placeholder"]:
enhanced["placeholder"] = defaults["placeholder"]
elif field_type == "text":
# For text fields, use label as placeholder
enhanced["placeholder"] = f"Enter {enhanced.get('label', 'value').lower()}"
if enhanced.get("help_text") is None and defaults["help_text"]:
enhanced["help_text"] = defaults["help_text"]
return enhanced
def get_validation_rules(field_config: Dict[str, Any]) -> Dict[str, Any]:
"""
Get complete validation rules for frontend
Args:
field_config: Field configuration with defaults applied
Returns:
Validation rules dict ready for frontend consumption
"""
return {
"min_length": field_config.get("min_length"),
"max_length": field_config.get("max_length"),
"pattern": field_config.get("pattern"),
"placeholder": field_config.get("placeholder"),
"help_text": field_config.get("help_text"),
"required": field_config.get("required", True)
}
def validate_field_value(value: Any, field_config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
"""
Validate a field value against its configuration
Args:
value: Value to validate
field_config: Field configuration with validation rules
Returns:
Tuple of (is_valid, error_message)
"""
import re
field_type = field_config.get("type", "text")
required = field_config.get("required", True)
# Check required
if required and (value is None or value == ""):
return False, f"{field_config.get('label', 'Field')} is required"
# If not required and empty, it's valid
if not required and (value is None or value == ""):
return True, None
# Convert value to string for validation
str_value = str(value)
# Check min_length
min_length = field_config.get("min_length")
if min_length is not None and len(str_value) < min_length:
return False, f"{field_config.get('label', 'Field')} must be at least {min_length} characters"
# Check max_length
max_length = field_config.get("max_length")
if max_length is not None and len(str_value) > max_length:
return False, f"{field_config.get('label', 'Field')} must not exceed {max_length} characters"
# Check pattern
pattern = field_config.get("pattern")
if pattern:
try:
if not re.match(pattern, str_value):
help_text = field_config.get("help_text", "Invalid format")
return False, f"{field_config.get('label', 'Field')}: {help_text}"
except re.error:
# Invalid regex pattern - log but don't fail validation
return True, None
# Type-specific validation
if field_type == "number":
try:
float(str_value)
except ValueError:
return False, f"{field_config.get('label', 'Field')} must be a valid number"
elif field_type == "boolean":
if str_value.lower() not in ["true", "false", "1", "0", "yes", "no"]:
return False, f"{field_config.get('label', 'Field')} must be true or false"
elif field_type == "select":
options = field_config.get("options", [])
if options and str_value not in options:
return False, f"{field_config.get('label', 'Field')} must be one of: {', '.join(options)}"
return True, None