infinity-data 1.0.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.
- infinity_data/__init__.py +41 -0
- infinity_data/emit/__init__.py +9 -0
- infinity_data/emit/converter.py +55 -0
- infinity_data/frontend.py +37 -0
- infinity_data/infra/__init__.py +4 -0
- infinity_data/infra/diagnostics.py +212 -0
- infinity_data/infra/file.py +83 -0
- infinity_data/infra/ll1_stream.py +71 -0
- infinity_data/infra/location.py +80 -0
- infinity_data/infra/path.py +46 -0
- infinity_data/parser/__init__.py +79 -0
- infinity_data/parser/diagnostics.py +92 -0
- infinity_data/parser/models.py +295 -0
- infinity_data/parser/parser.py +988 -0
- infinity_data/parser/token_stream.py +128 -0
- infinity_data/pipeline.py +252 -0
- infinity_data/sandbox/__init__.py +32 -0
- infinity_data/sandbox/config.py +61 -0
- infinity_data/sandbox/errors.py +106 -0
- infinity_data/sandbox/mediator.py +214 -0
- infinity_data/sandbox/schema.py +21 -0
- infinity_data/semantic/__init__.py +59 -0
- infinity_data/semantic/builder/__init__.py +29 -0
- infinity_data/semantic/builder/builder.py +523 -0
- infinity_data/semantic/builder/models.py +163 -0
- infinity_data/semantic/constraints.py +163 -0
- infinity_data/semantic/diagnostics.py +172 -0
- infinity_data/semantic/executor/__init__.py +10 -0
- infinity_data/semantic/executor/executor.py +259 -0
- infinity_data/semantic/registry/__init__.py +168 -0
- infinity_data/semantic/registry/_core.py +212 -0
- infinity_data/semantic/registry/dict_constraints.py +56 -0
- infinity_data/semantic/registry/general.py +362 -0
- infinity_data/semantic/registry/logic.py +134 -0
- infinity_data/semantic/registry/types.py +136 -0
- infinity_data/semantic/resolver/__init__.py +20 -0
- infinity_data/semantic/resolver/imports.py +191 -0
- infinity_data/semantic/resolver/models.py +67 -0
- infinity_data/semantic/resolver/resolver.py +345 -0
- infinity_data/tokenizer/__init__.py +16 -0
- infinity_data/tokenizer/char_stream.py +72 -0
- infinity_data/tokenizer/diagnostics.py +62 -0
- infinity_data/tokenizer/finalizer.py +271 -0
- infinity_data/tokenizer/models/__init__.py +17 -0
- infinity_data/tokenizer/models/raw_tokens.py +61 -0
- infinity_data/tokenizer/models/tokens.py +245 -0
- infinity_data/tokenizer/tokenizer.py +531 -0
- infinity_data-1.0.0.dist-info/METADATA +60 -0
- infinity_data-1.0.0.dist-info/RECORD +52 -0
- infinity_data-1.0.0.dist-info/WHEEL +5 -0
- infinity_data-1.0.0.dist-info/licenses/LICENSE +21 -0
- infinity_data-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""内置一般约束:range / size / each / in / ip / regex / 格式 / 数值 / eq / unique。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from infinity_data.infra.diagnostics import Diagnostic
|
|
12
|
+
from infinity_data.semantic.builder.models import StdArray, StdLiteral, StdObject, StdValue
|
|
13
|
+
from infinity_data.semantic.registry._core import (
|
|
14
|
+
ConstraintResult,
|
|
15
|
+
Executor,
|
|
16
|
+
describe,
|
|
17
|
+
fail_result,
|
|
18
|
+
ok_result,
|
|
19
|
+
)
|
|
20
|
+
from infinity_data.semantic.registry._core import (
|
|
21
|
+
as_decimal_arg as _as_decimal_arg,
|
|
22
|
+
)
|
|
23
|
+
from infinity_data.semantic.registry._core import (
|
|
24
|
+
as_number as _as_number,
|
|
25
|
+
)
|
|
26
|
+
from infinity_data.semantic.registry._core import (
|
|
27
|
+
as_spec as _as_spec,
|
|
28
|
+
)
|
|
29
|
+
from infinity_data.semantic.registry._core import (
|
|
30
|
+
as_std_value as _as_std_value,
|
|
31
|
+
)
|
|
32
|
+
from infinity_data.semantic.registry._core import (
|
|
33
|
+
as_str as _as_str,
|
|
34
|
+
)
|
|
35
|
+
from infinity_data.semantic.registry._core import (
|
|
36
|
+
std_equal as _std_equal,
|
|
37
|
+
)
|
|
38
|
+
from infinity_data.tokenizer.models.raw_tokens import SourceRange
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
'_check_range',
|
|
42
|
+
'_check_size',
|
|
43
|
+
'_check_each',
|
|
44
|
+
'_check_in',
|
|
45
|
+
'_check_ip',
|
|
46
|
+
'_check_ip4',
|
|
47
|
+
'_check_ip6',
|
|
48
|
+
'_check_regex',
|
|
49
|
+
'_check_email',
|
|
50
|
+
'_check_url',
|
|
51
|
+
'_check_uuid',
|
|
52
|
+
'_check_hostname',
|
|
53
|
+
'_check_positive',
|
|
54
|
+
'_check_negative',
|
|
55
|
+
'_check_nonnegative',
|
|
56
|
+
'_check_eq',
|
|
57
|
+
'_check_unique',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _check_range(
|
|
62
|
+
val: StdValue | None,
|
|
63
|
+
source: SourceRange | None,
|
|
64
|
+
path: str,
|
|
65
|
+
args: list[Any],
|
|
66
|
+
executor: Executor,
|
|
67
|
+
) -> ConstraintResult:
|
|
68
|
+
num = _as_number(val)
|
|
69
|
+
if num is None:
|
|
70
|
+
return fail_result('constraint.numeric_only', {'constraint': 'range', 'actual': describe(val)}, source, path)
|
|
71
|
+
if num.is_nan():
|
|
72
|
+
return fail_result('constraint.nan_not_allowed', {'constraint': 'range'}, source, path)
|
|
73
|
+
lo = _as_decimal_arg(args[0]) if args[0] is not None else None
|
|
74
|
+
hi = _as_decimal_arg(args[1]) if len(args) > 1 and args[1] is not None else None
|
|
75
|
+
if args[0] is not None and lo is None:
|
|
76
|
+
return fail_result('constraint.range_arg', {'value': args[0]}, source, path)
|
|
77
|
+
if len(args) > 1 and args[1] is not None and hi is None:
|
|
78
|
+
return fail_result('constraint.range_arg', {'value': args[1]}, source, path)
|
|
79
|
+
if lo is not None and num < lo:
|
|
80
|
+
return fail_result('constraint.range_below', {'value': num, 'lo': lo}, source, path)
|
|
81
|
+
if hi is not None and num > hi:
|
|
82
|
+
return fail_result('constraint.range_above', {'value': num, 'hi': hi}, source, path)
|
|
83
|
+
return ok_result()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_size(
|
|
87
|
+
val: StdValue | None,
|
|
88
|
+
source: SourceRange | None,
|
|
89
|
+
path: str,
|
|
90
|
+
args: list[Any],
|
|
91
|
+
executor: Executor,
|
|
92
|
+
) -> ConstraintResult:
|
|
93
|
+
size_val: int | None = None
|
|
94
|
+
if isinstance(val, StdLiteral) and val.kind == 'str':
|
|
95
|
+
v = val.value
|
|
96
|
+
size_val = len(v) if isinstance(v, str) else None
|
|
97
|
+
elif isinstance(val, StdArray):
|
|
98
|
+
size_val = len(val.elements)
|
|
99
|
+
elif isinstance(val, StdObject):
|
|
100
|
+
size_val = len(val.fields)
|
|
101
|
+
if size_val is None:
|
|
102
|
+
return fail_result('constraint.size_only', {'actual': describe(val)}, source, path)
|
|
103
|
+
lo = _as_decimal_arg(args[0])
|
|
104
|
+
hi = _as_decimal_arg(args[1]) if len(args) > 1 else None
|
|
105
|
+
if lo is None or (len(args) > 1 and hi is None):
|
|
106
|
+
return fail_result('constraint.size_arg', {}, source, path)
|
|
107
|
+
if size_val < lo or (hi is not None and size_val > hi):
|
|
108
|
+
return fail_result('constraint.size_out', {'size': size_val, 'lo': lo, 'hi': hi}, source, path)
|
|
109
|
+
return ok_result()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _check_each(
|
|
113
|
+
val: StdValue | None,
|
|
114
|
+
source: SourceRange | None,
|
|
115
|
+
path: str,
|
|
116
|
+
args: list[Any],
|
|
117
|
+
executor: Executor,
|
|
118
|
+
) -> ConstraintResult:
|
|
119
|
+
spec = _as_spec(args[0])
|
|
120
|
+
if spec is None:
|
|
121
|
+
return fail_result('constraint.each_need', {}, source, path)
|
|
122
|
+
diags: list[Diagnostic] = []
|
|
123
|
+
if isinstance(val, StdArray):
|
|
124
|
+
for i, elem in enumerate(val.elements):
|
|
125
|
+
r = executor(spec, elem, source, f'{path}[{i}]')
|
|
126
|
+
if not r.ok:
|
|
127
|
+
diags.extend(r.diagnostics)
|
|
128
|
+
elif isinstance(val, StdObject):
|
|
129
|
+
for f in val.fields:
|
|
130
|
+
r = executor(spec, f.value, source, f'{path}.{f.name}')
|
|
131
|
+
if not r.ok:
|
|
132
|
+
diags.extend(r.diagnostics)
|
|
133
|
+
else:
|
|
134
|
+
return fail_result('constraint.each_only', {'actual': describe(val)}, source, path)
|
|
135
|
+
if diags:
|
|
136
|
+
return ConstraintResult(ok=False, diagnostics=diags)
|
|
137
|
+
return ok_result()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_in(
|
|
141
|
+
val: StdValue | None,
|
|
142
|
+
source: SourceRange | None,
|
|
143
|
+
path: str,
|
|
144
|
+
args: list[Any],
|
|
145
|
+
executor: Executor,
|
|
146
|
+
) -> ConstraintResult:
|
|
147
|
+
choices: list[Any] = cast(list[Any], args[0]) if len(args) == 1 and isinstance(args[0], list) else args
|
|
148
|
+
if any(_std_equal(val, _as_std_value(c)) for c in choices):
|
|
149
|
+
return ok_result()
|
|
150
|
+
return fail_result('constraint.in_not_in', {'value': describe(val), 'choices': choices}, source, path)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _check_ip(
|
|
154
|
+
val: StdValue | None,
|
|
155
|
+
source: SourceRange | None,
|
|
156
|
+
path: str,
|
|
157
|
+
args: list[Any],
|
|
158
|
+
executor: Executor,
|
|
159
|
+
) -> ConstraintResult:
|
|
160
|
+
s = _as_str(val)
|
|
161
|
+
if s is None:
|
|
162
|
+
return fail_result('constraint.string_only', {'constraint': 'ip'}, source, path)
|
|
163
|
+
try:
|
|
164
|
+
ipaddress.ip_address(s)
|
|
165
|
+
return ok_result()
|
|
166
|
+
except ValueError:
|
|
167
|
+
return fail_result('constraint.invalid_value', {'what': 'IP 地址', 'value': s}, source, path)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _check_ip4(
|
|
171
|
+
val: StdValue | None,
|
|
172
|
+
source: SourceRange | None,
|
|
173
|
+
path: str,
|
|
174
|
+
args: list[Any],
|
|
175
|
+
executor: Executor,
|
|
176
|
+
) -> ConstraintResult:
|
|
177
|
+
s = _as_str(val)
|
|
178
|
+
if s is None:
|
|
179
|
+
return fail_result('constraint.string_only', {'constraint': 'ip4'}, source, path)
|
|
180
|
+
try:
|
|
181
|
+
ipaddress.IPv4Address(s)
|
|
182
|
+
return ok_result()
|
|
183
|
+
except ValueError:
|
|
184
|
+
return fail_result('constraint.invalid_value', {'what': 'IPv4 地址', 'value': s}, source, path)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _check_ip6(
|
|
188
|
+
val: StdValue | None,
|
|
189
|
+
source: SourceRange | None,
|
|
190
|
+
path: str,
|
|
191
|
+
args: list[Any],
|
|
192
|
+
executor: Executor,
|
|
193
|
+
) -> ConstraintResult:
|
|
194
|
+
s = _as_str(val)
|
|
195
|
+
if s is None:
|
|
196
|
+
return fail_result('constraint.string_only', {'constraint': 'ip6'}, source, path)
|
|
197
|
+
try:
|
|
198
|
+
ipaddress.IPv6Address(s)
|
|
199
|
+
return ok_result()
|
|
200
|
+
except ValueError:
|
|
201
|
+
return fail_result('constraint.invalid_value', {'what': 'IPv6 地址', 'value': s}, source, path)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _check_regex(
|
|
205
|
+
val: StdValue | None,
|
|
206
|
+
source: SourceRange | None,
|
|
207
|
+
path: str,
|
|
208
|
+
args: list[Any],
|
|
209
|
+
executor: Executor,
|
|
210
|
+
) -> ConstraintResult:
|
|
211
|
+
s = _as_str(val)
|
|
212
|
+
if s is None:
|
|
213
|
+
return fail_result('constraint.string_only', {'constraint': 'regex'}, source, path)
|
|
214
|
+
pattern = str(args[0])
|
|
215
|
+
try:
|
|
216
|
+
if re.fullmatch(pattern, s):
|
|
217
|
+
return ok_result()
|
|
218
|
+
return fail_result('constraint.regex_no_match', {'value': s, 'pattern': pattern}, source, path)
|
|
219
|
+
except re.error as e:
|
|
220
|
+
return fail_result('constraint.regex_invalid', {'pattern': pattern, 'error': e}, source, path)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _check_email(
|
|
224
|
+
val: StdValue | None,
|
|
225
|
+
source: SourceRange | None,
|
|
226
|
+
path: str,
|
|
227
|
+
args: list[Any],
|
|
228
|
+
executor: Executor,
|
|
229
|
+
) -> ConstraintResult:
|
|
230
|
+
s = _as_str(val)
|
|
231
|
+
if s is None:
|
|
232
|
+
return fail_result('constraint.string_only', {'constraint': 'email'}, source, path)
|
|
233
|
+
if re.fullmatch(r'[^@\s]+@[^@\s]+\.[^@\s]+', s):
|
|
234
|
+
return ok_result()
|
|
235
|
+
return fail_result('constraint.invalid_value', {'what': '邮箱地址', 'value': s}, source, path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _check_url(
|
|
239
|
+
val: StdValue | None,
|
|
240
|
+
source: SourceRange | None,
|
|
241
|
+
path: str,
|
|
242
|
+
args: list[Any],
|
|
243
|
+
executor: Executor,
|
|
244
|
+
) -> ConstraintResult:
|
|
245
|
+
s = _as_str(val)
|
|
246
|
+
if s is None:
|
|
247
|
+
return fail_result('constraint.string_only', {'constraint': 'url'}, source, path)
|
|
248
|
+
p = urlparse(s)
|
|
249
|
+
if p.scheme and p.netloc:
|
|
250
|
+
return ok_result()
|
|
251
|
+
return fail_result('constraint.invalid_value', {'what': 'URL', 'value': s}, source, path)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _check_uuid(
|
|
255
|
+
val: StdValue | None,
|
|
256
|
+
source: SourceRange | None,
|
|
257
|
+
path: str,
|
|
258
|
+
args: list[Any],
|
|
259
|
+
executor: Executor,
|
|
260
|
+
) -> ConstraintResult:
|
|
261
|
+
s = _as_str(val)
|
|
262
|
+
if s is None:
|
|
263
|
+
return fail_result('constraint.string_only', {'constraint': 'uuid'}, source, path)
|
|
264
|
+
try:
|
|
265
|
+
uuid.UUID(s)
|
|
266
|
+
return ok_result()
|
|
267
|
+
except ValueError:
|
|
268
|
+
return fail_result('constraint.invalid_value', {'what': 'UUID', 'value': s}, source, path)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
_HOSTNAME_RE = re.compile(
|
|
272
|
+
r'^(?=.{1,253}$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'
|
|
273
|
+
r'(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$',
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _check_hostname(
|
|
278
|
+
val: StdValue | None,
|
|
279
|
+
source: SourceRange | None,
|
|
280
|
+
path: str,
|
|
281
|
+
args: list[Any],
|
|
282
|
+
executor: Executor,
|
|
283
|
+
) -> ConstraintResult:
|
|
284
|
+
s = _as_str(val)
|
|
285
|
+
if s is None:
|
|
286
|
+
return fail_result('constraint.string_only', {'constraint': 'hostname'}, source, path)
|
|
287
|
+
if _HOSTNAME_RE.fullmatch(s):
|
|
288
|
+
return ok_result()
|
|
289
|
+
return fail_result('constraint.invalid_value', {'what': '主机名', 'value': s}, source, path)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _check_positive(
|
|
293
|
+
val: StdValue | None,
|
|
294
|
+
source: SourceRange | None,
|
|
295
|
+
path: str,
|
|
296
|
+
args: list[Any],
|
|
297
|
+
executor: Executor,
|
|
298
|
+
) -> ConstraintResult:
|
|
299
|
+
num = _as_number(val)
|
|
300
|
+
if num is None:
|
|
301
|
+
return fail_result('constraint.numeric_only', {'constraint': 'positive'}, source, path)
|
|
302
|
+
if num.is_nan():
|
|
303
|
+
return fail_result('constraint.nan_not_allowed', {'constraint': 'positive'}, source, path)
|
|
304
|
+
return ok_result() if num > 0 else fail_result('constraint.positive_fail', {'value': num}, source, path)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _check_negative(
|
|
308
|
+
val: StdValue | None,
|
|
309
|
+
source: SourceRange | None,
|
|
310
|
+
path: str,
|
|
311
|
+
args: list[Any],
|
|
312
|
+
executor: Executor,
|
|
313
|
+
) -> ConstraintResult:
|
|
314
|
+
num = _as_number(val)
|
|
315
|
+
if num is None:
|
|
316
|
+
return fail_result('constraint.numeric_only', {'constraint': 'negative'}, source, path)
|
|
317
|
+
if num.is_nan():
|
|
318
|
+
return fail_result('constraint.nan_not_allowed', {'constraint': 'negative'}, source, path)
|
|
319
|
+
return ok_result() if num < 0 else fail_result('constraint.negative_fail', {'value': num}, source, path)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _check_nonnegative(
|
|
323
|
+
val: StdValue | None,
|
|
324
|
+
source: SourceRange | None,
|
|
325
|
+
path: str,
|
|
326
|
+
args: list[Any],
|
|
327
|
+
executor: Executor,
|
|
328
|
+
) -> ConstraintResult:
|
|
329
|
+
num = _as_number(val)
|
|
330
|
+
if num is None:
|
|
331
|
+
return fail_result('constraint.numeric_only', {'constraint': 'nonnegative'}, source, path)
|
|
332
|
+
if num.is_nan():
|
|
333
|
+
return fail_result('constraint.nan_not_allowed', {'constraint': 'nonnegative'}, source, path)
|
|
334
|
+
return ok_result() if num >= 0 else fail_result('constraint.nonnegative_fail', {'value': num}, source, path)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _check_eq(
|
|
338
|
+
val: StdValue | None,
|
|
339
|
+
source: SourceRange | None,
|
|
340
|
+
path: str,
|
|
341
|
+
args: list[Any],
|
|
342
|
+
executor: Executor,
|
|
343
|
+
) -> ConstraintResult:
|
|
344
|
+
if _std_equal(val, _as_std_value(args[0])):
|
|
345
|
+
return ok_result()
|
|
346
|
+
return fail_result('constraint.eq_mismatch', {'value': describe(val), 'expected': args[0]}, source, path)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _check_unique(
|
|
350
|
+
val: StdValue | None,
|
|
351
|
+
source: SourceRange | None,
|
|
352
|
+
path: str,
|
|
353
|
+
args: list[Any],
|
|
354
|
+
executor: Executor,
|
|
355
|
+
) -> ConstraintResult:
|
|
356
|
+
if not isinstance(val, StdArray):
|
|
357
|
+
return fail_result('constraint.unique_only', {}, source, path)
|
|
358
|
+
for i in range(len(val.elements)):
|
|
359
|
+
for j in range(i):
|
|
360
|
+
if _std_equal(val.elements[i], val.elements[j]):
|
|
361
|
+
return fail_result('constraint.unique_dup', {'value': describe(val.elements[i])}, source, path)
|
|
362
|
+
return ok_result()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""内置逻辑约束:not / any / one / all / when。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from infinity_data.infra.diagnostics import Diagnostic, Severity
|
|
8
|
+
from infinity_data.semantic.builder.models import StdValue
|
|
9
|
+
from infinity_data.semantic.registry._core import (
|
|
10
|
+
ConstraintResult,
|
|
11
|
+
Executor,
|
|
12
|
+
fail_result,
|
|
13
|
+
ok_result,
|
|
14
|
+
)
|
|
15
|
+
from infinity_data.semantic.registry._core import (
|
|
16
|
+
as_spec as _as_spec,
|
|
17
|
+
)
|
|
18
|
+
from infinity_data.tokenizer.models.raw_tokens import SourceRange
|
|
19
|
+
|
|
20
|
+
__all__ = ['_check_not', '_check_any', '_check_one', '_check_all', '_check_when']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_not(
|
|
24
|
+
val: StdValue | None,
|
|
25
|
+
source: SourceRange | None,
|
|
26
|
+
path: str,
|
|
27
|
+
args: list[Any],
|
|
28
|
+
executor: Executor,
|
|
29
|
+
) -> ConstraintResult:
|
|
30
|
+
spec = _as_spec(args[0])
|
|
31
|
+
if spec is None:
|
|
32
|
+
return fail_result('constraint.not_need', {}, source, path)
|
|
33
|
+
inner = executor(spec, val, source, path)
|
|
34
|
+
if not inner.ok:
|
|
35
|
+
return ok_result() # 内部不满足 → not 满足
|
|
36
|
+
return fail_result('constraint.not_fail', {}, source, path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _check_any(
|
|
40
|
+
val: StdValue | None,
|
|
41
|
+
source: SourceRange | None,
|
|
42
|
+
path: str,
|
|
43
|
+
args: list[Any],
|
|
44
|
+
executor: Executor,
|
|
45
|
+
) -> ConstraintResult:
|
|
46
|
+
diags: list[Diagnostic] = []
|
|
47
|
+
for arg in args:
|
|
48
|
+
spec = _as_spec(arg)
|
|
49
|
+
if spec is None:
|
|
50
|
+
continue
|
|
51
|
+
inner = executor(spec, val, source, path)
|
|
52
|
+
if inner.ok:
|
|
53
|
+
return ok_result()
|
|
54
|
+
diags.extend(inner.diagnostics)
|
|
55
|
+
return ConstraintResult(
|
|
56
|
+
ok=False,
|
|
57
|
+
diagnostics=[
|
|
58
|
+
Diagnostic(Severity.ERROR, 'constraint.any_fail', {}, source, path),
|
|
59
|
+
*diags,
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _check_one(
|
|
65
|
+
val: StdValue | None,
|
|
66
|
+
source: SourceRange | None,
|
|
67
|
+
path: str,
|
|
68
|
+
args: list[Any],
|
|
69
|
+
executor: Executor,
|
|
70
|
+
) -> ConstraintResult:
|
|
71
|
+
satisfied: list[str] = []
|
|
72
|
+
diags: list[Diagnostic] = []
|
|
73
|
+
for arg in args:
|
|
74
|
+
spec = _as_spec(arg)
|
|
75
|
+
if spec is None:
|
|
76
|
+
continue
|
|
77
|
+
inner = executor(spec, val, source, path)
|
|
78
|
+
if inner.ok:
|
|
79
|
+
satisfied.append(spec.name)
|
|
80
|
+
else:
|
|
81
|
+
diags.extend(inner.diagnostics)
|
|
82
|
+
if len(satisfied) == 1:
|
|
83
|
+
return ok_result()
|
|
84
|
+
if not satisfied:
|
|
85
|
+
return ConstraintResult(
|
|
86
|
+
ok=False,
|
|
87
|
+
diagnostics=[
|
|
88
|
+
Diagnostic(Severity.ERROR, 'constraint.one_none', {}, source, path),
|
|
89
|
+
*diags,
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
return fail_result('constraint.one_many', {'count': len(satisfied), 'names': satisfied}, source, path)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_all(
|
|
96
|
+
val: StdValue | None,
|
|
97
|
+
source: SourceRange | None,
|
|
98
|
+
path: str,
|
|
99
|
+
args: list[Any],
|
|
100
|
+
executor: Executor,
|
|
101
|
+
) -> ConstraintResult:
|
|
102
|
+
diags: list[Diagnostic] = []
|
|
103
|
+
for arg in args:
|
|
104
|
+
spec = _as_spec(arg)
|
|
105
|
+
if spec is None:
|
|
106
|
+
continue
|
|
107
|
+
inner = executor(spec, val, source, path)
|
|
108
|
+
if not inner.ok:
|
|
109
|
+
diags.extend(inner.diagnostics)
|
|
110
|
+
if not diags:
|
|
111
|
+
return ok_result()
|
|
112
|
+
return ConstraintResult(
|
|
113
|
+
ok=False,
|
|
114
|
+
diagnostics=[
|
|
115
|
+
Diagnostic(Severity.ERROR, 'constraint.all_fail', {}, source, path),
|
|
116
|
+
*diags,
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _check_when(
|
|
122
|
+
val: StdValue | None,
|
|
123
|
+
source: SourceRange | None,
|
|
124
|
+
path: str,
|
|
125
|
+
args: list[Any],
|
|
126
|
+
executor: Executor,
|
|
127
|
+
) -> ConstraintResult:
|
|
128
|
+
cond = _as_spec(args[0])
|
|
129
|
+
req = _as_spec(args[1])
|
|
130
|
+
if cond is None or req is None:
|
|
131
|
+
return fail_result('constraint.when_need', {}, source, path)
|
|
132
|
+
if not executor(cond, val, source, path).ok:
|
|
133
|
+
return ok_result() # 条件不满足 → 约束满足
|
|
134
|
+
return executor(req, val, source, path)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""内置类型约束:object / ? / int / str / bool / float / list / dict。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from infinity_data.semantic.builder.models import StdArray, StdLiteral, StdObject, StdValue
|
|
8
|
+
from infinity_data.semantic.registry._core import (
|
|
9
|
+
ConstraintResult,
|
|
10
|
+
Executor,
|
|
11
|
+
describe,
|
|
12
|
+
fail_result,
|
|
13
|
+
ok_result,
|
|
14
|
+
)
|
|
15
|
+
from infinity_data.tokenizer.models.raw_tokens import SourceRange
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
'_check_object',
|
|
19
|
+
'_check_nullable',
|
|
20
|
+
'_check_int',
|
|
21
|
+
'_check_float',
|
|
22
|
+
'_check_str',
|
|
23
|
+
'_check_bool',
|
|
24
|
+
'_check_list',
|
|
25
|
+
'_check_dict',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_object(
|
|
30
|
+
val: StdValue | None,
|
|
31
|
+
source: SourceRange | None,
|
|
32
|
+
path: str,
|
|
33
|
+
args: list[Any],
|
|
34
|
+
executor: Executor,
|
|
35
|
+
) -> ConstraintResult:
|
|
36
|
+
if val is None:
|
|
37
|
+
return fail_result('constraint.expect_value', {'expected': 'object'}, source, path)
|
|
38
|
+
return ok_result()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _check_nullable(
|
|
42
|
+
val: StdValue | None,
|
|
43
|
+
source: SourceRange | None,
|
|
44
|
+
path: str,
|
|
45
|
+
args: list[Any],
|
|
46
|
+
executor: Executor,
|
|
47
|
+
) -> ConstraintResult:
|
|
48
|
+
if isinstance(val, StdLiteral) and val.kind in ('null', 'noexist'):
|
|
49
|
+
return ok_result()
|
|
50
|
+
return fail_result(
|
|
51
|
+
'constraint.type_mismatch', {'expected': 'noexist 或 null', 'actual': describe(val)}, source, path
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check_int(
|
|
56
|
+
val: StdValue | None,
|
|
57
|
+
source: SourceRange | None,
|
|
58
|
+
path: str,
|
|
59
|
+
args: list[Any],
|
|
60
|
+
executor: Executor,
|
|
61
|
+
) -> ConstraintResult:
|
|
62
|
+
if val is None:
|
|
63
|
+
return fail_result('constraint.expect_value', {'expected': 'int'}, source, path)
|
|
64
|
+
if isinstance(val, StdLiteral) and val.kind == 'int':
|
|
65
|
+
return ok_result()
|
|
66
|
+
return fail_result('constraint.type_mismatch', {'expected': 'int', 'actual': describe(val)}, source, path)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _check_float(
|
|
70
|
+
val: StdValue | None,
|
|
71
|
+
source: SourceRange | None,
|
|
72
|
+
path: str,
|
|
73
|
+
args: list[Any],
|
|
74
|
+
executor: Executor,
|
|
75
|
+
) -> ConstraintResult:
|
|
76
|
+
if val is None:
|
|
77
|
+
return fail_result('constraint.expect_value', {'expected': 'float'}, source, path)
|
|
78
|
+
if isinstance(val, StdLiteral) and val.kind == 'float':
|
|
79
|
+
return ok_result() # 含 NaN / ±Infinity
|
|
80
|
+
return fail_result('constraint.type_mismatch', {'expected': 'float', 'actual': describe(val)}, source, path)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _check_str(
|
|
84
|
+
val: StdValue | None,
|
|
85
|
+
source: SourceRange | None,
|
|
86
|
+
path: str,
|
|
87
|
+
args: list[Any],
|
|
88
|
+
executor: Executor,
|
|
89
|
+
) -> ConstraintResult:
|
|
90
|
+
if val is None:
|
|
91
|
+
return fail_result('constraint.expect_value', {'expected': 'str'}, source, path)
|
|
92
|
+
if isinstance(val, StdLiteral) and val.kind == 'str':
|
|
93
|
+
return ok_result()
|
|
94
|
+
return fail_result('constraint.type_mismatch', {'expected': 'str', 'actual': describe(val)}, source, path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _check_bool(
|
|
98
|
+
val: StdValue | None,
|
|
99
|
+
source: SourceRange | None,
|
|
100
|
+
path: str,
|
|
101
|
+
args: list[Any],
|
|
102
|
+
executor: Executor,
|
|
103
|
+
) -> ConstraintResult:
|
|
104
|
+
if val is None:
|
|
105
|
+
return fail_result('constraint.expect_value', {'expected': 'bool'}, source, path)
|
|
106
|
+
if isinstance(val, StdLiteral) and val.kind == 'bool':
|
|
107
|
+
return ok_result()
|
|
108
|
+
return fail_result('constraint.type_mismatch', {'expected': 'bool', 'actual': describe(val)}, source, path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _check_list(
|
|
112
|
+
val: StdValue | None,
|
|
113
|
+
source: SourceRange | None,
|
|
114
|
+
path: str,
|
|
115
|
+
args: list[Any],
|
|
116
|
+
executor: Executor,
|
|
117
|
+
) -> ConstraintResult:
|
|
118
|
+
if val is None:
|
|
119
|
+
return fail_result('constraint.expect_value', {'expected': 'list'}, source, path)
|
|
120
|
+
if isinstance(val, StdArray):
|
|
121
|
+
return ok_result()
|
|
122
|
+
return fail_result('constraint.type_mismatch', {'expected': 'list', 'actual': describe(val)}, source, path)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _check_dict(
|
|
126
|
+
val: StdValue | None,
|
|
127
|
+
source: SourceRange | None,
|
|
128
|
+
path: str,
|
|
129
|
+
args: list[Any],
|
|
130
|
+
executor: Executor,
|
|
131
|
+
) -> ConstraintResult:
|
|
132
|
+
if val is None:
|
|
133
|
+
return fail_result('constraint.expect_value', {'expected': 'dict'}, source, path)
|
|
134
|
+
if isinstance(val, StdObject):
|
|
135
|
+
return ok_result()
|
|
136
|
+
return fail_result('constraint.type_mismatch', {'expected': 'dict', 'actual': describe(val)}, source, path)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Phase 1(导入求解器):模板图 / 可见名表 / 数据命名空间。
|
|
2
|
+
|
|
3
|
+
- :class:`TemplateGraphResolver`:模板图求解(resolver.py)
|
|
4
|
+
- :class:`ImportResolver`:导入语句求解(imports.py)
|
|
5
|
+
- 数据模型::class:`ResolvedContext` / :class:`TemplateKey` / :class:`Scope`(models.py)
|
|
6
|
+
|
|
7
|
+
子模块导出自身模型与类,供流水线(pipeline / 测试)直接引用。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from infinity_data.semantic.resolver.imports import ImportResolver
|
|
11
|
+
from infinity_data.semantic.resolver.models import ResolvedContext, Scope, TemplateKey
|
|
12
|
+
from infinity_data.semantic.resolver.resolver import TemplateGraphResolver
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'ImportResolver',
|
|
16
|
+
'ResolvedContext',
|
|
17
|
+
'Scope',
|
|
18
|
+
'TemplateGraphResolver',
|
|
19
|
+
'TemplateKey',
|
|
20
|
+
]
|