Option sub_category -> sub_categories[] (default []). Both pool filters (instance category_filter, nested @{} ref) become boolean expressions (and/or/not/parens; comma=or) matched against an option's tag set via the shared engine/syntax/subcat_filter matcher, applied BEFORE constraints. Exclude-null is a separate flag: instance exclude_null + the trailing !null ref marker, replacing the old 'null' keyword.
4-segment ref grammar @{uuid#name:expr!null} single-sourced across tokenize.py/scan.py/pipeline.py/_constraints.py (! excluded from name/expr captures); tokenizer REF meta now carries filter_expr + exclude_null instead of a comma-split list. scan.py findall unpack sites updated for the 4th group.
validate_payload: per-option sub_categories[] must be in the registry; optional tag_groups (UI-only axes, members in-registry, <=1 group); registry names validated via subcat_filter.validate_subcat_name (rejects whitespace, boolean/ref chars, reserved words incl. null). wp_api/_validators delegates to the same function.
Constraints stay alive via a primary-tag stopgap: _apply_constraint_to_options + _record_pick use sub_categories[0] as the matrix key; true multi-tag matrix matching is SP3. Cascade fix_subcat_delete/rename + orchestrator scrub the per-option list; instance_schemas category_filter -> string. Existing engine tests migrated to the v2 shape; added tests for the ref regex, payload validation, and multi-tag pool filtering.
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
"""4-segment nested-ref regex + tokenizer meta (SP1).
|
|
|
|
`@{uuid [#name] [:expr] [!null]}` — fixed segment order, `!` excluded
|
|
from the name/expr captures so the null marker stays unambiguous.
|
|
"""
|
|
from engine.syntax.tokenize import _REF_RE, tokenize_text
|
|
from engine.syntax.types import TokenKind
|
|
|
|
|
|
def _seg(s):
|
|
m = _REF_RE.fullmatch(s)
|
|
assert m, s
|
|
return m.group(1), m.group(2), m.group(3), m.group(4) # uuid, name, expr, null
|
|
|
|
|
|
def test_ref_segments():
|
|
assert _seg("@{aabbccdd}") == ("aabbccdd", None, None, None)
|
|
assert _seg("@{aabbccdd#mood}") == ("aabbccdd", "mood", None, None)
|
|
assert _seg("@{aabbccdd:warm or cold}") == ("aabbccdd", None, "warm or cold", None)
|
|
assert _seg("@{aabbccdd:warm or cold!null}") == (
|
|
"aabbccdd", None, "warm or cold", "null",
|
|
)
|
|
assert _seg("@{aabbccdd!null}") == ("aabbccdd", None, None, "null")
|
|
assert _seg("@{aabbccdd#mood:(red or pink) and not cold!null}") == (
|
|
"aabbccdd", "mood", "(red or pink) and not cold", "null",
|
|
)
|
|
|
|
|
|
def _ref_meta(s):
|
|
toks = [t for t in tokenize_text(s) if t.kind == TokenKind.REF]
|
|
assert len(toks) == 1, s
|
|
return toks[0].meta
|
|
|
|
|
|
def test_ref_meta_filter_expr_and_exclude_null():
|
|
assert _ref_meta("@{aabbccdd}") == {"uuid": "aabbccdd"}
|
|
assert _ref_meta("@{aabbccdd:warm or cold}") == {
|
|
"uuid": "aabbccdd", "filter_expr": "warm or cold",
|
|
}
|
|
assert _ref_meta("@{aabbccdd:warm!null}") == {
|
|
"uuid": "aabbccdd", "filter_expr": "warm", "exclude_null": True,
|
|
}
|
|
assert _ref_meta("@{aabbccdd!null}") == {"uuid": "aabbccdd", "exclude_null": True}
|
|
assert _ref_meta("@{aabbccdd#mood:warm}") == {
|
|
"uuid": "aabbccdd", "name": "mood", "filter_expr": "warm",
|
|
}
|
|
|
|
|
|
def test_empty_expr_is_no_filter():
|
|
# `@{uuid:}` — colon present but empty expr ⇒ no filter_expr in meta.
|
|
assert _ref_meta("@{aabbccdd:}") == {"uuid": "aabbccdd"}
|