Coverage for src/dataknobs_fsm/functions/library/validators.py: 0%

206 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-20 16:46 -0600

1"""Built-in validator functions for FSM. 

2 

3This module provides commonly used validation functions that can be 

4referenced in FSM configurations. 

5""" 

6 

7import re 

8from typing import Any, Dict, List, Union 

9 

10from pydantic import BaseModel, ValidationError 

11 

12from dataknobs_fsm.functions.base import IValidationFunction, ValidationError as FSMValidationError 

13 

14 

15class RequiredFieldsValidator(IValidationFunction): 

16 """Validate that required fields are present in data.""" 

17 

18 def __init__(self, fields: List[str], allow_none: bool = False): 

19 """Initialize the validator. 

20  

21 Args: 

22 fields: List of required field names. 

23 allow_none: Whether to allow None values for required fields. 

24 """ 

25 self.fields = fields 

26 self.allow_none = allow_none 

27 

28 def validate(self, data: Dict[str, Any]) -> bool: 

29 """Validate that all required fields are present. 

30  

31 Args: 

32 data: Data to validate. 

33  

34 Returns: 

35 True if valid, False otherwise. 

36  

37 Raises: 

38 FSMValidationError: If validation fails with details. 

39 """ 

40 if not isinstance(data, dict): 

41 raise FSMValidationError(f"Expected dict, got {type(data).__name__}") 

42 

43 missing_fields = [] 

44 none_fields = [] 

45 

46 for field in self.fields: 

47 if field not in data: 

48 missing_fields.append(field) 

49 elif not self.allow_none and data[field] is None: 

50 none_fields.append(field) 

51 

52 if missing_fields: 

53 raise FSMValidationError( 

54 f"Missing required fields: {', '.join(missing_fields)}" 

55 ) 

56 

57 if none_fields: 

58 raise FSMValidationError( 

59 f"Fields cannot be None: {', '.join(none_fields)}" 

60 ) 

61 

62 return True 

63 

64 

65class SchemaValidator(IValidationFunction): 

66 """Validate data against a Pydantic schema.""" 

67 

68 def __init__(self, schema: Union[type[BaseModel], Dict[str, Any]]): 

69 """Initialize the validator. 

70  

71 Args: 

72 schema: Pydantic model class or schema dictionary. 

73 """ 

74 if isinstance(schema, dict): 

75 # Create a dynamic Pydantic model from dictionary 

76 from pydantic import create_model 

77 self.schema = create_model("DynamicSchema", **schema) 

78 else: 

79 self.schema = schema 

80 

81 def validate(self, data: Dict[str, Any]) -> bool: 

82 """Validate data against the schema. 

83  

84 Args: 

85 data: Data to validate. 

86  

87 Returns: 

88 True if valid, False otherwise. 

89  

90 Raises: 

91 FSMValidationError: If validation fails with details. 

92 """ 

93 try: 

94 self.schema(**data) 

95 return True 

96 except ValidationError as e: 

97 errors = [] 

98 for error in e.errors(): 

99 field_path = ".".join(str(loc) for loc in error["loc"]) 

100 errors.append(f"{field_path}: {error['msg']}") 

101 

102 raise FSMValidationError( 

103 f"Schema validation failed: {'; '.join(errors)}" 

104 ) from e 

105 

106 def get_validation_rules(self) -> Dict[str, Any]: 

107 """Get the validation rules.""" 

108 if hasattr(self.schema, 'model_json_schema'): 

109 return self.schema.model_json_schema() 

110 elif hasattr(self.schema, '__annotations__'): 

111 return dict(self.schema.__annotations__) 

112 else: 

113 return {"schema": str(self.schema)} 

114 

115 

116class RangeValidator(IValidationFunction): 

117 """Validate that numeric values are within specified ranges.""" 

118 

119 def __init__( 

120 self, 

121 field_ranges: Dict[str, Dict[str, Union[int, float]]], 

122 ): 

123 """Initialize the validator. 

124  

125 Args: 

126 field_ranges: Dictionary mapping field names to range specifications. 

127 Each range can have 'min', 'max', or both. 

128 """ 

129 self.field_ranges = field_ranges 

130 

131 def validate(self, data: Dict[str, Any]) -> bool: 

132 """Validate that values are within specified ranges. 

133  

134 Args: 

135 data: Data to validate. 

136  

137 Returns: 

138 True if valid, False otherwise. 

139  

140 Raises: 

141 FSMValidationError: If validation fails with details. 

142 """ 

143 errors = [] 

144 

145 for field, range_spec in self.field_ranges.items(): 

146 if field not in data: 

147 continue 

148 

149 value = data[field] 

150 if not isinstance(value, (int, float)): 

151 errors.append(f"{field}: Expected numeric value, got {type(value).__name__}") 

152 continue 

153 

154 if "min" in range_spec and value < range_spec["min"]: 

155 errors.append(f"{field}: Value {value} is below minimum {range_spec['min']}") 

156 

157 if "max" in range_spec and value > range_spec["max"]: 

158 errors.append(f"{field}: Value {value} is above maximum {range_spec['max']}") 

159 

160 if errors: 

161 raise FSMValidationError("; ".join(errors)) 

162 

163 return True 

164 

165 

166class PatternValidator(IValidationFunction): 

167 """Validate that string values match specified patterns.""" 

168 

169 def __init__( 

170 self, 

171 field_patterns: Dict[str, str], 

172 flags: int = 0, 

173 ): 

174 """Initialize the validator. 

175  

176 Args: 

177 field_patterns: Dictionary mapping field names to regex patterns. 

178 flags: Regex flags to apply (e.g., re.IGNORECASE). 

179 """ 

180 self.field_patterns = {} 

181 for field, pattern in field_patterns.items(): 

182 self.field_patterns[field] = re.compile(pattern, flags) 

183 

184 def validate(self, data: Dict[str, Any]) -> bool: 

185 """Validate that values match specified patterns. 

186  

187 Args: 

188 data: Data to validate. 

189  

190 Returns: 

191 True if valid, False otherwise. 

192  

193 Raises: 

194 FSMValidationError: If validation fails with details. 

195 """ 

196 errors = [] 

197 

198 for field, pattern in self.field_patterns.items(): 

199 if field not in data: 

200 continue 

201 

202 value = data[field] 

203 if not isinstance(value, str): 

204 errors.append(f"{field}: Expected string value, got {type(value).__name__}") 

205 continue 

206 

207 if not pattern.match(value): 

208 errors.append(f"{field}: Value '{value}' does not match pattern") 

209 

210 if errors: 

211 raise FSMValidationError("; ".join(errors)) 

212 

213 return True 

214 

215 

216class TypeValidator(IValidationFunction): 

217 """Validate that fields have expected types.""" 

218 

219 def __init__( 

220 self, 

221 field_types: Dict[str, Union[type, List[type]]], 

222 strict: bool = False, 

223 ): 

224 """Initialize the validator. 

225  

226 Args: 

227 field_types: Dictionary mapping field names to expected types. 

228 strict: If True, reject extra fields not in field_types. 

229 """ 

230 self.field_types = field_types 

231 self.strict = strict 

232 

233 def validate(self, data: Dict[str, Any]) -> bool: 

234 """Validate that fields have expected types. 

235  

236 Args: 

237 data: Data to validate. 

238  

239 Returns: 

240 True if valid, False otherwise. 

241  

242 Raises: 

243 FSMValidationError: If validation fails with details. 

244 """ 

245 errors = [] 

246 

247 # Check field types 

248 for field, expected_type in self.field_types.items(): 

249 if field not in data: 

250 continue 

251 

252 value = data[field] 

253 if isinstance(expected_type, list): 

254 # Multiple allowed types 

255 if not any(isinstance(value, t) for t in expected_type): 

256 type_names = ", ".join(t.__name__ for t in expected_type) 

257 errors.append( 

258 f"{field}: Expected one of [{type_names}], " 

259 f"got {type(value).__name__}" 

260 ) 

261 else: 

262 # Single expected type 

263 if not isinstance(value, expected_type): 

264 errors.append( 

265 f"{field}: Expected {expected_type.__name__}, " 

266 f"got {type(value).__name__}" 

267 ) 

268 

269 # Check for extra fields if strict mode 

270 if self.strict: 

271 extra_fields = set(data.keys()) - set(self.field_types.keys()) 

272 if extra_fields: 

273 errors.append(f"Unexpected fields: {', '.join(extra_fields)}") 

274 

275 if errors: 

276 raise FSMValidationError("; ".join(errors)) 

277 

278 return True 

279 

280 

281class LengthValidator(IValidationFunction): 

282 """Validate that collections have expected lengths.""" 

283 

284 def __init__( 

285 self, 

286 field_lengths: Dict[str, Dict[str, int]], 

287 ): 

288 """Initialize the validator. 

289  

290 Args: 

291 field_lengths: Dictionary mapping field names to length specifications. 

292 Each spec can have 'min', 'max', or 'exact'. 

293 """ 

294 self.field_lengths = field_lengths 

295 

296 def validate(self, data: Dict[str, Any]) -> bool: 

297 """Validate that collections have expected lengths. 

298  

299 Args: 

300 data: Data to validate. 

301  

302 Returns: 

303 True if valid, False otherwise. 

304  

305 Raises: 

306 FSMValidationError: If validation fails with details. 

307 """ 

308 errors = [] 

309 

310 for field, length_spec in self.field_lengths.items(): 

311 if field not in data: 

312 continue 

313 

314 value = data[field] 

315 if not hasattr(value, "__len__"): 

316 errors.append(f"{field}: Value does not have a length") 

317 continue 

318 

319 length = len(value) 

320 

321 if "exact" in length_spec and length != length_spec["exact"]: 

322 errors.append( 

323 f"{field}: Length {length} does not match expected {length_spec['exact']}" 

324 ) 

325 

326 if "min" in length_spec and length < length_spec["min"]: 

327 errors.append(f"{field}: Length {length} is below minimum {length_spec['min']}") 

328 

329 if "max" in length_spec and length > length_spec["max"]: 

330 errors.append(f"{field}: Length {length} is above maximum {length_spec['max']}") 

331 

332 if errors: 

333 raise FSMValidationError("; ".join(errors)) 

334 

335 return True 

336 

337 

338class UniqueValidator(IValidationFunction): 

339 """Validate that values in collections are unique.""" 

340 

341 def __init__( 

342 self, 

343 fields: List[str], 

344 key: str | None = None, 

345 ): 

346 """Initialize the validator. 

347  

348 Args: 

349 fields: List of field names to check for uniqueness. 

350 key: Optional key to extract from collection items for uniqueness check. 

351 """ 

352 self.fields = fields 

353 self.key = key 

354 

355 def validate(self, data: Dict[str, Any]) -> bool: 

356 """Validate that values are unique. 

357  

358 Args: 

359 data: Data to validate. 

360  

361 Returns: 

362 True if valid, False otherwise. 

363  

364 Raises: 

365 FSMValidationError: If validation fails with details. 

366 """ 

367 errors = [] 

368 

369 for field in self.fields: 

370 if field not in data: 

371 continue 

372 

373 value = data[field] 

374 if not isinstance(value, (list, tuple, set)): 

375 errors.append(f"{field}: Expected collection, got {type(value).__name__}") 

376 continue 

377 

378 if self.key: 

379 # Extract values using key 

380 try: 

381 values = [item[self.key] if isinstance(item, dict) else getattr(item, self.key) 

382 for item in value] 

383 except (KeyError, AttributeError) as e: 

384 errors.append(f"{field}: Cannot extract key '{self.key}': {e}") 

385 continue 

386 else: 

387 values = list(value) 

388 

389 # Check for duplicates 

390 seen = set() 

391 duplicates = set() 

392 for v in values: 

393 if v in seen: 

394 duplicates.add(str(v)) 

395 seen.add(v) 

396 

397 if duplicates: 

398 errors.append(f"{field}: Duplicate values found: {', '.join(duplicates)}") 

399 

400 if errors: 

401 raise FSMValidationError("; ".join(errors)) 

402 

403 return True 

404 

405 

406class DependencyValidator(IValidationFunction): 

407 """Validate field dependencies (if field A exists, field B must also exist).""" 

408 

409 def __init__( 

410 self, 

411 dependencies: Dict[str, Union[str, List[str]]], 

412 ): 

413 """Initialize the validator. 

414  

415 Args: 

416 dependencies: Dictionary mapping field names to their dependencies. 

417 """ 

418 self.dependencies = dependencies 

419 

420 def validate(self, data: Dict[str, Any]) -> bool: 

421 """Validate field dependencies. 

422  

423 Args: 

424 data: Data to validate. 

425  

426 Returns: 

427 True if valid, False otherwise. 

428  

429 Raises: 

430 FSMValidationError: If validation fails with details. 

431 """ 

432 errors = [] 

433 

434 for field, deps in self.dependencies.items(): 

435 if field not in data: 

436 continue 

437 

438 deps_list = deps if isinstance(deps, list) else [deps] 

439 

440 missing_deps = [dep for dep in deps_list if dep not in data] 

441 

442 if missing_deps: 

443 errors.append( 

444 f"Field '{field}' requires: {', '.join(missing_deps)}" 

445 ) 

446 

447 if errors: 

448 raise FSMValidationError("; ".join(errors)) 

449 

450 return True 

451 

452 

453class CompositeValidator(IValidationFunction): 

454 """Compose multiple validators into a single validator.""" 

455 

456 def __init__( 

457 self, 

458 validators: List[IValidationFunction], 

459 stop_on_first_error: bool = False, 

460 ): 

461 """Initialize the composite validator. 

462  

463 Args: 

464 validators: List of validators to apply. 

465 stop_on_first_error: If True, stop at first validation error. 

466 """ 

467 self.validators = validators 

468 self.stop_on_first_error = stop_on_first_error 

469 

470 def validate(self, data: Dict[str, Any]) -> bool: 

471 """Apply all validators to the data. 

472  

473 Args: 

474 data: Data to validate. 

475  

476 Returns: 

477 True if all validators pass. 

478  

479 Raises: 

480 FSMValidationError: If any validation fails. 

481 """ 

482 errors = [] 

483 

484 for validator in self.validators: 

485 try: 

486 validator.validate(data) 

487 except FSMValidationError as e: 

488 if self.stop_on_first_error: 

489 raise 

490 errors.append(str(e)) 

491 

492 if errors: 

493 raise FSMValidationError("; ".join(errors)) 

494 

495 return True 

496 

497 

498# Convenience functions for creating validators 

499def required_fields(*fields: str, allow_none: bool = False) -> RequiredFieldsValidator: 

500 """Create a RequiredFieldsValidator.""" 

501 return RequiredFieldsValidator(list(fields), allow_none) 

502 

503 

504def schema(model: Union[type[BaseModel], Dict[str, Any]]) -> SchemaValidator: 

505 """Create a SchemaValidator.""" 

506 return SchemaValidator(model) 

507 

508 

509def range_check(**field_ranges: Dict[str, Union[int, float]]) -> RangeValidator: 

510 """Create a RangeValidator.""" 

511 return RangeValidator(field_ranges) 

512 

513 

514def pattern(**field_patterns: str) -> PatternValidator: 

515 """Create a PatternValidator.""" 

516 return PatternValidator(field_patterns) 

517 

518 

519def type_check(**field_types: Union[type, List[type]]) -> TypeValidator: 

520 """Create a TypeValidator.""" 

521 return TypeValidator(field_types) 

522 

523 

524def length(**field_lengths: Dict[str, int]) -> LengthValidator: 

525 """Create a LengthValidator.""" 

526 return LengthValidator(field_lengths) 

527 

528 

529def unique(*fields: str, key: str | None = None) -> UniqueValidator: 

530 """Create a UniqueValidator.""" 

531 return UniqueValidator(list(fields), key) 

532 

533 

534def depends_on(**dependencies: Union[str, List[str]]) -> DependencyValidator: 

535 """Create a DependencyValidator.""" 

536 return DependencyValidator(dependencies)