File size: 7,527 Bytes
dddd6c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""
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