eggpipe 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eggpipe/__init__.py +129 -0
- eggpipe/cli/__init__.py +0 -0
- eggpipe/cli/main.py +710 -0
- eggpipe/config/__init__.py +0 -0
- eggpipe/config/app_config.py +35 -0
- eggpipe/config/logging.py +40 -0
- eggpipe/config/project_config.py +162 -0
- eggpipe/core/__init__.py +0 -0
- eggpipe/core/composition.py +431 -0
- eggpipe/core/discovery.py +566 -0
- eggpipe/core/document.py +32 -0
- eggpipe/core/format_registry.py +230 -0
- eggpipe/core/layers.py +385 -0
- eggpipe/core/model.py +308 -0
- eggpipe/core/naming.py +36 -0
- eggpipe/core/parser.py +96 -0
- eggpipe/core/paths.py +12 -0
- eggpipe/core/provenance.py +52 -0
- eggpipe/core/slicing.py +110 -0
- eggpipe/core/validator.py +518 -0
- eggpipe/engine/__init__.py +0 -0
- eggpipe/engine/artifact_writer.py +741 -0
- eggpipe/engine/engine.py +679 -0
- eggpipe/engine/plugin_loader.py +215 -0
- eggpipe/engine/policy.py +27 -0
- eggpipe/engine/report.py +90 -0
- eggpipe/interfaces/__init__.py +0 -0
- eggpipe/interfaces/diagnostics.py +129 -0
- eggpipe/interfaces/formats.py +122 -0
- eggpipe/interfaces/generator.py +235 -0
- eggpipe/py.typed +0 -0
- eggpipe/templating.py +84 -0
- eggpipe/utils/__init__.py +0 -0
- eggpipe/utils/exceptions.py +65 -0
- eggpipe/utils/yaml.py +44 -0
- eggpipe/version.py +3 -0
- eggpipe/watch/__init__.py +0 -0
- eggpipe/watch/watcher.py +168 -0
- eggpipe-0.3.0.dist-info/METADATA +251 -0
- eggpipe-0.3.0.dist-info/RECORD +43 -0
- eggpipe-0.3.0.dist-info/WHEEL +4 -0
- eggpipe-0.3.0.dist-info/entry_points.txt +2 -0
- eggpipe-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Semantic model validation on top of Pydantic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from eggpipe.core.document import ModelDocument
|
|
11
|
+
from eggpipe.core.model import (
|
|
12
|
+
PRIMITIVE_TYPES,
|
|
13
|
+
UNSET,
|
|
14
|
+
Constraints,
|
|
15
|
+
CustomType,
|
|
16
|
+
Entity,
|
|
17
|
+
EnumDef,
|
|
18
|
+
FieldDef,
|
|
19
|
+
ModelSlice,
|
|
20
|
+
SourceModel,
|
|
21
|
+
)
|
|
22
|
+
from eggpipe.interfaces.diagnostics import Diagnostic, error, warning
|
|
23
|
+
|
|
24
|
+
MAX_TYPE_DEPTH = 10
|
|
25
|
+
MAX_IMPORT_DEPTH = 10
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_entity_model(
|
|
29
|
+
model: SourceModel | ModelSlice,
|
|
30
|
+
*,
|
|
31
|
+
primitive_types: set[str] | frozenset[str] | None = None,
|
|
32
|
+
file: Path | None = None,
|
|
33
|
+
) -> list[Diagnostic]:
|
|
34
|
+
primitives = set(PRIMITIVE_TYPES if primitive_types is None else primitive_types)
|
|
35
|
+
diagnostics: list[Diagnostic] = []
|
|
36
|
+
diagnostics.extend(_validate_duplicate_names(model, file))
|
|
37
|
+
diagnostics.extend(_validate_case_collisions(model, file))
|
|
38
|
+
|
|
39
|
+
type_bases = _resolve_type_bases(model, primitives, file, diagnostics)
|
|
40
|
+
|
|
41
|
+
for enum in model.enums:
|
|
42
|
+
diagnostics.extend(_validate_enum(enum, file))
|
|
43
|
+
|
|
44
|
+
referenced: set[str] = set()
|
|
45
|
+
for entity in model.entities:
|
|
46
|
+
diagnostics.extend(
|
|
47
|
+
_validate_entity(entity, model, primitives, type_bases, referenced, file)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
for custom_type in model.types:
|
|
51
|
+
referenced.add(custom_type.base)
|
|
52
|
+
for enum in model.enums:
|
|
53
|
+
if enum.name not in referenced:
|
|
54
|
+
diagnostics.append(
|
|
55
|
+
warning(
|
|
56
|
+
"EG013",
|
|
57
|
+
f"enum {enum.name} is not used",
|
|
58
|
+
file=file,
|
|
59
|
+
entity=enum.name,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
for custom_type in model.types:
|
|
63
|
+
if custom_type.name not in referenced:
|
|
64
|
+
diagnostics.append(
|
|
65
|
+
warning(
|
|
66
|
+
"EG013",
|
|
67
|
+
f"type {custom_type.name} is not used",
|
|
68
|
+
file=file,
|
|
69
|
+
entity=custom_type.name,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return diagnostics
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_document(document: ModelDocument) -> list[Diagnostic]:
|
|
76
|
+
return validate_entity_model(document.model, file=document.path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _validate_duplicate_names(
|
|
80
|
+
model: SourceModel | ModelSlice,
|
|
81
|
+
file: Path | None,
|
|
82
|
+
) -> list[Diagnostic]:
|
|
83
|
+
diagnostics: list[Diagnostic] = []
|
|
84
|
+
buckets = (
|
|
85
|
+
("entity", [item.name for item in model.entities]),
|
|
86
|
+
("enum", [item.name for item in model.enums]),
|
|
87
|
+
("type", [item.name for item in model.types]),
|
|
88
|
+
)
|
|
89
|
+
seen: dict[str, str] = {}
|
|
90
|
+
for kind, names in buckets:
|
|
91
|
+
local: set[str] = set()
|
|
92
|
+
for index, name in enumerate(names):
|
|
93
|
+
if name in local:
|
|
94
|
+
diagnostics.append(
|
|
95
|
+
error(
|
|
96
|
+
"EG002",
|
|
97
|
+
f"duplicate {kind} name: {name}",
|
|
98
|
+
file=file,
|
|
99
|
+
path=f"{kind}[{index}]",
|
|
100
|
+
entity=name,
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
local.add(name)
|
|
104
|
+
previous = seen.get(name)
|
|
105
|
+
if previous is not None and previous != kind:
|
|
106
|
+
diagnostics.append(
|
|
107
|
+
error(
|
|
108
|
+
"EG002",
|
|
109
|
+
f"name {name} is used both as {previous} and as {kind}",
|
|
110
|
+
file=file,
|
|
111
|
+
path=f"{kind}[{index}]",
|
|
112
|
+
entity=name,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
seen[name] = kind
|
|
117
|
+
return diagnostics
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_case_collisions(
|
|
121
|
+
model: SourceModel | ModelSlice,
|
|
122
|
+
file: Path | None,
|
|
123
|
+
) -> list[Diagnostic]:
|
|
124
|
+
diagnostics: list[Diagnostic] = []
|
|
125
|
+
names = [item.name for item in (*model.entities, *model.enums, *model.types)]
|
|
126
|
+
by_lower: dict[str, str] = {}
|
|
127
|
+
for name in names:
|
|
128
|
+
lower = name.lower()
|
|
129
|
+
previous = by_lower.get(lower)
|
|
130
|
+
if previous is not None and previous != name:
|
|
131
|
+
diagnostics.append(
|
|
132
|
+
warning(
|
|
133
|
+
"EG026",
|
|
134
|
+
f"case-insensitive name collision: {previous} and {name}",
|
|
135
|
+
file=file,
|
|
136
|
+
entity=name,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
by_lower[lower] = name
|
|
141
|
+
return diagnostics
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _resolve_type_bases(
|
|
145
|
+
model: SourceModel | ModelSlice,
|
|
146
|
+
primitives: set[str],
|
|
147
|
+
file: Path | None,
|
|
148
|
+
diagnostics: list[Diagnostic],
|
|
149
|
+
) -> dict[str, str]:
|
|
150
|
+
resolved: dict[str, str] = {}
|
|
151
|
+
type_map = {item.name: item for item in model.types}
|
|
152
|
+
enum_names = {item.name for item in model.enums}
|
|
153
|
+
|
|
154
|
+
def resolve(custom_type: CustomType, stack: tuple[str, ...]) -> str | None:
|
|
155
|
+
if custom_type.name in stack:
|
|
156
|
+
diagnostics.append(
|
|
157
|
+
error(
|
|
158
|
+
"EG020",
|
|
159
|
+
f"CustomType.base cycle: {' -> '.join((*stack, custom_type.name))}",
|
|
160
|
+
file=file,
|
|
161
|
+
entity=custom_type.name,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return None
|
|
165
|
+
if len(stack) > MAX_TYPE_DEPTH:
|
|
166
|
+
diagnostics.append(
|
|
167
|
+
error(
|
|
168
|
+
"EG020",
|
|
169
|
+
f"CustomType.base depth exceeds {MAX_TYPE_DEPTH}",
|
|
170
|
+
file=file,
|
|
171
|
+
entity=custom_type.name,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return None
|
|
175
|
+
base = custom_type.base
|
|
176
|
+
if base in primitives:
|
|
177
|
+
return base
|
|
178
|
+
if base in enum_names:
|
|
179
|
+
diagnostics.append(
|
|
180
|
+
error(
|
|
181
|
+
"EG004",
|
|
182
|
+
f"CustomType.base cannot reference an enum: {base}",
|
|
183
|
+
file=file,
|
|
184
|
+
entity=custom_type.name,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
parent = type_map.get(base)
|
|
189
|
+
if parent is None:
|
|
190
|
+
diagnostics.append(
|
|
191
|
+
error(
|
|
192
|
+
"EG004",
|
|
193
|
+
f"unknown base type {base} for {custom_type.name}",
|
|
194
|
+
file=file,
|
|
195
|
+
entity=custom_type.name,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return None
|
|
199
|
+
return resolve(parent, (*stack, custom_type.name))
|
|
200
|
+
|
|
201
|
+
for custom_type in model.types:
|
|
202
|
+
base = resolve(custom_type, ())
|
|
203
|
+
if base is not None:
|
|
204
|
+
resolved[custom_type.name] = base
|
|
205
|
+
return resolved
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _validate_enum(enum: EnumDef, file: Path | None) -> list[Diagnostic]:
|
|
209
|
+
diagnostics: list[Diagnostic] = []
|
|
210
|
+
if not enum.values:
|
|
211
|
+
diagnostics.append(
|
|
212
|
+
error("EG019", f"enum {enum.name} is empty", file=file, entity=enum.name)
|
|
213
|
+
)
|
|
214
|
+
names: set[str] = set()
|
|
215
|
+
values: set[tuple[str, Any]] = set()
|
|
216
|
+
for index, item in enumerate(enum.values):
|
|
217
|
+
if item.name in names:
|
|
218
|
+
diagnostics.append(
|
|
219
|
+
error(
|
|
220
|
+
"EG019",
|
|
221
|
+
f"duplicate value name in enum {enum.name}: {item.name}",
|
|
222
|
+
file=file,
|
|
223
|
+
entity=enum.name,
|
|
224
|
+
field=item.name,
|
|
225
|
+
path=f"enums[{index}]",
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
names.add(item.name)
|
|
229
|
+
value = item.value if item.value is not None else item.name
|
|
230
|
+
marker = (type(value).__name__, value)
|
|
231
|
+
if marker in values:
|
|
232
|
+
diagnostics.append(
|
|
233
|
+
error(
|
|
234
|
+
"EG019",
|
|
235
|
+
f"duplicate value in enum {enum.name}: {value!r}",
|
|
236
|
+
file=file,
|
|
237
|
+
entity=enum.name,
|
|
238
|
+
field=item.name,
|
|
239
|
+
path=f"enums[{index}]",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
values.add(marker)
|
|
243
|
+
return diagnostics
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _validate_entity(
|
|
247
|
+
entity: Entity,
|
|
248
|
+
model: SourceModel | ModelSlice,
|
|
249
|
+
primitives: set[str],
|
|
250
|
+
type_bases: dict[str, str],
|
|
251
|
+
referenced: set[str],
|
|
252
|
+
file: Path | None,
|
|
253
|
+
) -> list[Diagnostic]:
|
|
254
|
+
diagnostics: list[Diagnostic] = []
|
|
255
|
+
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", entity.name):
|
|
256
|
+
diagnostics.append(
|
|
257
|
+
warning(
|
|
258
|
+
"EG012",
|
|
259
|
+
f"entity {entity.name} is not PascalCase",
|
|
260
|
+
file=file,
|
|
261
|
+
entity=entity.name,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
if len({field.name for field in entity.fields}) != len(entity.fields):
|
|
265
|
+
seen: set[str] = set()
|
|
266
|
+
for index, field in enumerate(entity.fields):
|
|
267
|
+
if field.name in seen:
|
|
268
|
+
diagnostics.append(
|
|
269
|
+
error(
|
|
270
|
+
"EG003",
|
|
271
|
+
f"duplicate field {field.name} in {entity.name}",
|
|
272
|
+
file=file,
|
|
273
|
+
entity=entity.name,
|
|
274
|
+
field=field.name,
|
|
275
|
+
path=f"entities[{entity.name}].fields[{index}]",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
seen.add(field.name)
|
|
279
|
+
|
|
280
|
+
enum_map = {item.name: item for item in model.enums}
|
|
281
|
+
type_map = {item.name: item for item in model.types}
|
|
282
|
+
for index, field in enumerate(entity.fields):
|
|
283
|
+
location = {
|
|
284
|
+
"file": file,
|
|
285
|
+
"entity": entity.name,
|
|
286
|
+
"field": field.name,
|
|
287
|
+
"path": f"entities[{entity.name}].fields[{index}]",
|
|
288
|
+
}
|
|
289
|
+
if field.type in enum_map or field.type in type_map:
|
|
290
|
+
referenced.add(field.type)
|
|
291
|
+
elif field.type in primitives:
|
|
292
|
+
pass
|
|
293
|
+
else:
|
|
294
|
+
diagnostics.append(error("EG004", f"unknown type {field.type}", **location))
|
|
295
|
+
|
|
296
|
+
if field.default is None and not field.nullable:
|
|
297
|
+
diagnostics.append(
|
|
298
|
+
error(
|
|
299
|
+
"EG010",
|
|
300
|
+
"default: null is not allowed when nullable: false",
|
|
301
|
+
**location,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
if field.required and field.nullable:
|
|
305
|
+
diagnostics.append(
|
|
306
|
+
warning(
|
|
307
|
+
"EG011",
|
|
308
|
+
"required: true and nullable: true is valid only for PATCH",
|
|
309
|
+
**location,
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
if field.required and not field.description:
|
|
313
|
+
diagnostics.append(
|
|
314
|
+
warning(
|
|
315
|
+
"EG015",
|
|
316
|
+
f"required field {field.name} has no description",
|
|
317
|
+
**location,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
effective = type_bases.get(field.type, field.type)
|
|
322
|
+
constraints = _merge_constraints(field, type_map.get(field.type))
|
|
323
|
+
diagnostics.extend(_validate_constraints(constraints, effective, location))
|
|
324
|
+
if field.default is not UNSET and field.default is not None:
|
|
325
|
+
diagnostics.extend(
|
|
326
|
+
_validate_default(
|
|
327
|
+
field.default,
|
|
328
|
+
effective,
|
|
329
|
+
enum_map.get(field.type),
|
|
330
|
+
constraints,
|
|
331
|
+
location,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
return diagnostics
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _merge_constraints(
|
|
338
|
+
field: FieldDef,
|
|
339
|
+
custom_type: CustomType | None,
|
|
340
|
+
) -> Constraints | None:
|
|
341
|
+
if field.constraints is None:
|
|
342
|
+
return custom_type.constraints if custom_type is not None else None
|
|
343
|
+
if custom_type is None or custom_type.constraints is None:
|
|
344
|
+
return field.constraints
|
|
345
|
+
|
|
346
|
+
base = custom_type.constraints
|
|
347
|
+
override = field.constraints
|
|
348
|
+
values: dict[str, Any] = {}
|
|
349
|
+
for name in ("min", "max", "min_length", "max_length", "pattern"):
|
|
350
|
+
if name in override.model_fields_set:
|
|
351
|
+
values[name] = getattr(override, name)
|
|
352
|
+
else:
|
|
353
|
+
values[name] = getattr(base, name)
|
|
354
|
+
return Constraints(**values)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _validate_constraints(
|
|
358
|
+
constraints: Constraints | None,
|
|
359
|
+
base_type: str,
|
|
360
|
+
location: dict[str, Any],
|
|
361
|
+
) -> list[Diagnostic]:
|
|
362
|
+
if constraints is None:
|
|
363
|
+
return []
|
|
364
|
+
diagnostics: list[Diagnostic] = []
|
|
365
|
+
string_like = base_type in {"string", "text", "uuid"}
|
|
366
|
+
numeric = base_type in {"integer", "bigint", "float"}
|
|
367
|
+
temporal = base_type in {"date", "datetime"}
|
|
368
|
+
|
|
369
|
+
if constraints.pattern is not None and not string_like:
|
|
370
|
+
diagnostics.append(error("EG021", "pattern applies only to string types", **location))
|
|
371
|
+
if (constraints.min_length is not None or constraints.max_length is not None) and not (
|
|
372
|
+
string_like
|
|
373
|
+
):
|
|
374
|
+
diagnostics.append(
|
|
375
|
+
error(
|
|
376
|
+
"EG021",
|
|
377
|
+
"min_length/max_length apply only to string types",
|
|
378
|
+
**location,
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
if (constraints.min is not None or constraints.max is not None) and not (numeric or temporal):
|
|
382
|
+
diagnostics.append(error("EG021", "min/max apply only to numbers and dates", **location))
|
|
383
|
+
if (
|
|
384
|
+
constraints.min is not None
|
|
385
|
+
and constraints.max is not None
|
|
386
|
+
and _safe_compare(constraints.min, constraints.max) > 0
|
|
387
|
+
):
|
|
388
|
+
diagnostics.append(error("EG021", "min is greater than max", **location))
|
|
389
|
+
if (
|
|
390
|
+
constraints.min_length is not None
|
|
391
|
+
and constraints.max_length is not None
|
|
392
|
+
and constraints.min_length > constraints.max_length
|
|
393
|
+
):
|
|
394
|
+
diagnostics.append(error("EG021", "min_length is greater than max_length", **location))
|
|
395
|
+
if constraints.pattern is not None:
|
|
396
|
+
try:
|
|
397
|
+
re.compile(constraints.pattern)
|
|
398
|
+
except re.error as exc:
|
|
399
|
+
diagnostics.append(error("EG005", f"invalid regex: {exc}", **location))
|
|
400
|
+
return diagnostics
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _validate_default(
|
|
404
|
+
default: object,
|
|
405
|
+
base_type: str,
|
|
406
|
+
enum: EnumDef | None,
|
|
407
|
+
constraints: Constraints | None,
|
|
408
|
+
location: dict[str, Any],
|
|
409
|
+
) -> list[Diagnostic]:
|
|
410
|
+
diagnostics: list[Diagnostic] = []
|
|
411
|
+
if enum is not None:
|
|
412
|
+
values = {item.value if item.value is not None else item.name for item in enum.values}
|
|
413
|
+
names = {item.name for item in enum.values}
|
|
414
|
+
if default not in values and default not in names:
|
|
415
|
+
diagnostics.append(
|
|
416
|
+
error(
|
|
417
|
+
"EG017",
|
|
418
|
+
f"default {default!r} is not in enum {enum.name}",
|
|
419
|
+
**location,
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
return diagnostics
|
|
423
|
+
|
|
424
|
+
valid = _default_matches_type(default, base_type)
|
|
425
|
+
if not valid:
|
|
426
|
+
diagnostics.append(
|
|
427
|
+
error(
|
|
428
|
+
"EG016",
|
|
429
|
+
f"default {default!r} does not match type {base_type}",
|
|
430
|
+
**location,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
return diagnostics
|
|
434
|
+
if constraints is not None:
|
|
435
|
+
diagnostics.extend(_validate_default_constraints(default, constraints, location))
|
|
436
|
+
return diagnostics
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _default_matches_type(value: object, base_type: str) -> bool:
|
|
440
|
+
if base_type in {"string", "text"}:
|
|
441
|
+
return isinstance(value, str)
|
|
442
|
+
if base_type == "uuid":
|
|
443
|
+
return isinstance(value, str)
|
|
444
|
+
if base_type in {"integer", "bigint"}:
|
|
445
|
+
return isinstance(value, int) and not isinstance(value, bool)
|
|
446
|
+
if base_type == "boolean":
|
|
447
|
+
return isinstance(value, bool)
|
|
448
|
+
if base_type == "float":
|
|
449
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
450
|
+
if base_type == "date":
|
|
451
|
+
return isinstance(value, date) and not isinstance(value, datetime)
|
|
452
|
+
if base_type == "datetime":
|
|
453
|
+
return isinstance(value, datetime)
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _validate_default_constraints(
|
|
458
|
+
value: object,
|
|
459
|
+
constraints: Constraints,
|
|
460
|
+
location: dict[str, Any],
|
|
461
|
+
) -> list[Diagnostic]:
|
|
462
|
+
diagnostics: list[Diagnostic] = []
|
|
463
|
+
try:
|
|
464
|
+
if constraints.min is not None and _safe_compare(value, constraints.min) < 0:
|
|
465
|
+
diagnostics.append(
|
|
466
|
+
error(
|
|
467
|
+
"EG017", f"default {value!r} is less than min {constraints.min!r}", **location
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
if constraints.max is not None and _safe_compare(value, constraints.max) > 0:
|
|
471
|
+
diagnostics.append(
|
|
472
|
+
error(
|
|
473
|
+
"EG017",
|
|
474
|
+
f"default {value!r} is greater than max {constraints.max!r}",
|
|
475
|
+
**location,
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
except TypeError:
|
|
479
|
+
diagnostics.append(
|
|
480
|
+
error("EG017", f"default {value!r} cannot be compared with min/max", **location)
|
|
481
|
+
)
|
|
482
|
+
if isinstance(value, str):
|
|
483
|
+
if constraints.min_length is not None and len(value) < constraints.min_length:
|
|
484
|
+
diagnostics.append(
|
|
485
|
+
error(
|
|
486
|
+
"EG017",
|
|
487
|
+
f"default is shorter than min_length {constraints.min_length}",
|
|
488
|
+
**location,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
491
|
+
if constraints.max_length is not None and len(value) > constraints.max_length:
|
|
492
|
+
diagnostics.append(
|
|
493
|
+
error(
|
|
494
|
+
"EG017",
|
|
495
|
+
f"default is longer than max_length {constraints.max_length}",
|
|
496
|
+
**location,
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
if constraints.pattern is not None and not re.fullmatch(constraints.pattern, value):
|
|
500
|
+
diagnostics.append(
|
|
501
|
+
error(
|
|
502
|
+
"EG017",
|
|
503
|
+
f"default does not match pattern {constraints.pattern!r}",
|
|
504
|
+
**location,
|
|
505
|
+
)
|
|
506
|
+
)
|
|
507
|
+
return diagnostics
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _safe_compare(left: object, right: object) -> int:
|
|
511
|
+
if left < right: # type: ignore[operator]
|
|
512
|
+
return -1
|
|
513
|
+
if left > right: # type: ignore[operator]
|
|
514
|
+
return 1
|
|
515
|
+
return 0
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
__all__ = ["validate_document", "validate_entity_model"]
|
|
File without changes
|