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
« 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# ///////////////////////////////////////////////////////////////
6"""
7Schema validators - Validation utilities for dictionary schemas and structures.
9This module provides validation functions for validating dictionary structures,
10required fields, field types, and complex schema validation.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18# Standard library imports
19from typing import Any
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)
41# ///////////////////////////////////////////////////////////////
42# FUNCTIONS
43# ///////////////////////////////////////////////////////////////
46def validate_required_fields(data: dict[str, Any], required_fields: list[str]) -> None:
47 """
48 Validate that required fields are present in a dictionary.
50 Args:
51 data: Dictionary to validate
52 required_fields: List of required field names
54 Raises:
55 TypeError: If data is not a dict
56 RequiredFieldError: If required fields are missing
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")
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)
73 if missing_fields:
74 raise RequiredFieldError(
75 f"Missing required fields: {', '.join(missing_fields)}"
76 )
79def validate_field_types(data: dict[str, Any], field_types: dict[str, type]) -> None:
80 """
81 Validate that fields have the correct types.
83 Args:
84 data: Dictionary to validate
85 field_types: Dictionary mapping field names to expected types
87 Raises:
88 TypeError: If data is not a dict
89 TypeValidationError: If field types are incorrect
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")
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 )
113def validate_config_dict(config: dict[str, Any]) -> None:
114 """
115 Validate a configuration dictionary structure.
117 Args:
118 config: Configuration dictionary to validate
120 Raises:
121 SchemaValidationError: If configuration is invalid
123 Note:
124 Validates required top-level sections and their formats.
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")
137 # Check for required top-level sections
138 required_sections = ["version", "project_name", "main_file"]
139 validate_required_fields(config, required_sections)
141 # Validate version format
142 if not validate_version_string(config["version"]):
143 raise FormatValidationError("Invalid version format")
145 # Validate project name
146 if not validate_string_length(config["project_name"], min_length=1):
147 raise LengthValidationError("Project name cannot be empty")
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")
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.
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 }
176 Args:
177 data: Dictionary to validate
178 schema: Validation schema
180 Raises:
181 SchemaValidationError: If validation fails
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")
193 for field_name, field_schema in schema.items():
194 value = data.get(field_name)
196 # Check required fields
197 if field_schema.get("required", False) and value is None:
198 raise SchemaValidationError(f"Field '{field_name}' is required")
200 # Skip validation for None optional fields
201 if value is None:
202 continue
204 # Check not empty
205 if not field_schema.get("empty", True):
206 validate_not_empty(value, field_name)
208 # Check type
209 if "type" in field_schema:
210 validate_type(value, field_schema["type"], field_name)
212 # Check choices
213 if "choices" in field_schema:
214 validate_one_of(value, field_schema["choices"], field_name)
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)
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)
230 # Check pattern (for strings)
231 if isinstance(value, str) and "pattern" in field_schema:
232 validate_pattern(value, field_schema["pattern"], field_name)