Coverage for src / ezcompiler / utils / validators / schema_validators.py: 97.20%

61 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-27 06:49 +0000

1# /////////////////////////////////////////////////////////////// 

2# SCHEMA_VALIDATORS - Schema validation utilities 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7Schema validators - Validation utilities for dictionary schemas and structures. 

8 

9This module provides validation functions for validating dictionary structures, 

10required fields, field types, and complex schema validation. 

11""" 

12 

13from __future__ import annotations 

14 

15# /////////////////////////////////////////////////////////////// 

16# IMPORTS 

17# /////////////////////////////////////////////////////////////// 

18# Standard library imports 

19from typing import Any 

20 

21# Local imports 

22from ...shared.exceptions.utils.validation_exceptions import ( 

23 FormatValidationError, 

24 LengthValidationError, 

25 RequiredFieldError, 

26 SchemaValidationError, 

27 TypeValidationError, 

28) 

29from .format_validators import validate_version_string 

30from .path_validators import validate_file_path 

31from .string_validators import validate_pattern 

32from .type_validators import validate_type 

33from .value_validators import ( 

34 validate_length, 

35 validate_not_empty, 

36 validate_one_of, 

37 validate_string_length, 

38 validate_value_in_range, 

39) 

40 

41# /////////////////////////////////////////////////////////////// 

42# FUNCTIONS 

43# /////////////////////////////////////////////////////////////// 

44 

45 

46def validate_required_fields(data: dict[str, Any], required_fields: list[str]) -> None: 

47 """ 

48 Validate that required fields are present in a dictionary. 

49 

50 Args: 

51 data: Dictionary to validate 

52 required_fields: List of required field names 

53 

54 Raises: 

55 TypeError: If data is not a dict 

56 RequiredFieldError: If required fields are missing 

57 

58 Example: 

59 >>> validate_required_fields({"name": "test", "age": 25}, ["name", "age"]) 

60 >>> validate_required_fields({"name": "test"}, ["name", "age"]) 

61 Traceback (most recent call last): 

62 ... 

63 RequiredFieldError: Missing required fields: age 

64 """ 

65 if not isinstance(data, dict): 

66 raise TypeError("Data must be a dictionary") 

67 

68 missing_fields = [] 

69 for field in required_fields: 

70 if field not in data or data[field] is None: 

71 missing_fields.append(field) 

72 

73 if missing_fields: 

74 raise RequiredFieldError( 

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

76 ) 

77 

78 

79def validate_field_types(data: dict[str, Any], field_types: dict[str, type]) -> None: 

80 """ 

81 Validate that fields have the correct types. 

82 

83 Args: 

84 data: Dictionary to validate 

85 field_types: Dictionary mapping field names to expected types 

86 

87 Raises: 

88 TypeError: If data is not a dict 

89 TypeValidationError: If field types are incorrect 

90 

91 Example: 

92 >>> validate_field_types({"name": "test", "age": 25}, {"name": str, "age": int}) 

93 >>> validate_field_types({"name": "test", "age": "25"}, {"name": str, "age": int}) 

94 Traceback (most recent call last): 

95 ... 

96 TypeValidationError: Field 'age' must be of type int, got str 

97 """ 

98 if not isinstance(data, dict): 

99 raise TypeError("Data must be a dictionary") 

100 

101 for field, expected_type in field_types.items(): 

102 if ( 

103 field in data 

104 and data[field] is not None 

105 and not isinstance(data[field], expected_type) 

106 ): 

107 raise TypeValidationError( 

108 f"Field '{field}' must be of type {expected_type.__name__}, " 

109 f"got {type(data[field]).__name__}" 

110 ) 

111 

112 

113def validate_config_dict(config: dict[str, Any]) -> None: 

114 """ 

115 Validate a configuration dictionary structure. 

116 

117 Args: 

118 config: Configuration dictionary to validate 

119 

120 Raises: 

121 SchemaValidationError: If configuration is invalid 

122 

123 Note: 

124 Validates required top-level sections and their formats. 

125 

126 Example: 

127 >>> config = { 

128 ... "version": "1.0.0", 

129 ... "project_name": "MyProject", 

130 ... "main_file": "main.py" 

131 ... } 

132 >>> validate_config_dict(config) 

133 """ 

134 if not isinstance(config, dict): 

135 raise SchemaValidationError("Configuration must be a dictionary") 

136 

137 # Check for required top-level sections 

138 required_sections = ["version", "project_name", "main_file"] 

139 validate_required_fields(config, required_sections) 

140 

141 # Validate version format 

142 if not validate_version_string(config["version"]): 

143 raise FormatValidationError("Invalid version format") 

144 

145 # Validate project name 

146 if not validate_string_length(config["project_name"], min_length=1): 

147 raise LengthValidationError("Project name cannot be empty") 

148 

149 # Validate main file path 

150 if not validate_file_path(config["main_file"]): 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true

151 raise FormatValidationError("Invalid main file path") 

152 

153 

154def validate_dict_schema( 

155 data: dict[str, Any], 

156 schema: dict[str, dict[str, Any]], 

157) -> None: 

158 """ 

159 Validate a dictionary against a schema. 

160 

161 Schema format: 

162 { 

163 "field_name": { 

164 "type": (str, int, ...), # Required type(s) 

165 "required": True/False, # Is field required 

166 "empty": True/False, # Allow empty values 

167 "choices": [...] # Valid choices 

168 "min_length": int, # Min length (str/list) 

169 "max_length": int, # Max length (str/list) 

170 "min_value": int/float, # Min value (numeric) 

171 "max_value": int/float, # Max value (numeric) 

172 "pattern": "regex", # Regex pattern (str) 

173 } 

174 } 

175 

176 Args: 

177 data: Dictionary to validate 

178 schema: Validation schema 

179 

180 Raises: 

181 SchemaValidationError: If validation fails 

182 

183 Example: 

184 >>> schema = { # noqa: W605 

185 ... "version": {"type": str, "required": True, "pattern": r"^\\d+\\.\\d+\\.\\d+$"}, # noqa: W605 

186 ... "port": {"type": int, "required": False, "min_value": 1, "max_value": 65535}, 

187 ... } 

188 >>> validate_dict_schema(data, schema) 

189 """ 

190 if not isinstance(data, dict): 

191 raise SchemaValidationError("Data must be a dictionary") 

192 

193 for field_name, field_schema in schema.items(): 

194 value = data.get(field_name) 

195 

196 # Check required fields 

197 if field_schema.get("required", False) and value is None: 

198 raise SchemaValidationError(f"Field '{field_name}' is required") 

199 

200 # Skip validation for None optional fields 

201 if value is None: 

202 continue 

203 

204 # Check not empty 

205 if not field_schema.get("empty", True): 

206 validate_not_empty(value, field_name) 

207 

208 # Check type 

209 if "type" in field_schema: 

210 validate_type(value, field_schema["type"], field_name) 

211 

212 # Check choices 

213 if "choices" in field_schema: 

214 validate_one_of(value, field_schema["choices"], field_name) 

215 

216 # Check length (for strings/lists) 

217 if isinstance(value, (str, list)): 

218 min_len = field_schema.get("min_length") 

219 max_len = field_schema.get("max_length") 

220 if min_len is not None or max_len is not None: 

221 validate_length(value, min_len, max_len, field_name) 

222 

223 # Check numeric range 

224 if isinstance(value, (int, float)): 

225 min_val = field_schema.get("min_value") 

226 max_val = field_schema.get("max_value") 

227 if min_val is not None or max_val is not None: 227 ↛ 231line 227 didn't jump to line 231 because the condition on line 227 was always true

228 validate_value_in_range(value, min_val, max_val, field_name) 

229 

230 # Check pattern (for strings) 

231 if isinstance(value, str) and "pattern" in field_schema: 

232 validate_pattern(value, field_schema["pattern"], field_name)