refactor: single-source wire grammar for the simple filter dialect - #798
Conversation
The marker grammar lived in three hand-written spellings: the decode if-cascade in value.ts, codec-url's 15-case encode switch and the template-literal input union in types.ts — which had already drifted (the startsWith/contains suffix spellings the decoder accepts were missing, so parseTyped rejected valid wire input). One declarative table now drives all three: FILTER_WIRE_SPEC holds a row per positive marker operator; negation twins derive from core's complementOf relation in FILTER_OPERATOR_SEMANTICS (the '!<5' -> lt 5 quirk falls out by construction); the typed-input union is computed from the table's literal types and pinned by compile-time checks. decodeFilterWireValue returns a verdict (the parser only applies the schema drop-vs-throw policy; the empty-value post-check moves into the codec), encodeFilterWireValue throws typed AdapterErrors and self-verifies every token by re-decoding it (the grammar has no escaping, so collisions like eq 'foo~' are value-content-induced). parseFilterScalar survives unchanged as the dialect-shared scalar vocabulary; parseFilterValue/serializeFilterValue become internal; parseFilterWireValue is replaced; the URLFilterOperator enum is deleted. The wire format itself is frozen byte-for-byte — all existing specs pass unchanged; the new filters-wire spec pins the grammar directly at the token boundary, including the degenerate quirks. Refs #797
The simple encoder's 15-case operator switch, serializeLikeText and verifyWireValue re-spelled the wire grammar and defined encoder correctness by re-parsing through another package's decoder. The visitor now delegates per-condition token spelling to parser-simple's encodeFilterWireValue and keeps only the structural subset law (flat root-AND, one condition per field) — marker characters, raw LIKE text and collision policy are no longer expressible here. Refs #797
|
Warning Review limit reached
Next review available in: 56 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Implements #797.
Summary
The simple dialect's filter wire grammar (
filter[age]=>=18,filter[name]=~oh~,filter[id]=!1,2) was spelled three times by hand — the decode if-cascade in parser-simple'svalue.ts, the 15-case encode switch in codec-url's simpleFiltersVisitor, and the template-literal input union intypes.ts. This PR collapses all three into one declarative table in a newparser-simple/src/parameter/filters/wire/module.The table + complement derivation
FILTER_WIRE_SPECholds 8 rows, one per positive marker operator (declared order = decode precedence). Negation twins are not rows: they derive from thecomplementOfrelation in core'sFILTER_OPERATOR_SEMANTICS(#792), so the wire table repeats nothing and the frozen'!<5'→lt 5quirk falls out by construction (the ordering family has no complements).in/ninstay a value shape (comma list through theeqspelling), not a marker.Per-caller contracts
decodeFilterWireValue(input)returns a verdict ({ success: true, condition } | { success: false, code }) — the parser applies the schema drop-vs-throw policy and nothing else. The empty-value post-check that previously lived insideSimpleFiltersParser('','!','<=','~~'decode to empty conditions) is absorbed as thevalueEmptyverdict — the last piece of wire knowledge leaves the parser.encodeFilterWireValue(condition)throws typedAdapterErrors (subset law is unconditional) and self-verifies every emitted token by re-decoding it through the same table. The grammar has no escaping, so collisions are value-content-induced (eq 'foo~'would re-decode asstartsWith 'foo') — per the RFC, the round-trip check survives inside the codec instead of dying, but it disappears from codec-url and from the public API.Derived typed input (fixes a live drift bug)
The
parseTypedinput union is now computed from the table's literalprefix/suffixtypes plus the complement relation. The hand-written union had drifted: it only contained prefix forms, so the runtime-accepted`${V}~`(startsWith),`~${V}~`(contains) and their!forms were type errors. The derived union is exactly legacy + those 4 members — a compile-time-only widening. Compile-timeEqualspins (wire/checks.ts, checked bybuild:types, not exported from the barrel) freeze the derived types against table or core-semantics changes.Export changes (pre-GA)
parseFilterScalarwire/scalar.ts— dialect-shared scalar vocabulary (parser-expression + expression encoder untouched)parseFilterValue,serializeFilterValueparseFilterWireValuedecodeFilterWireValueURLFilterOperatorenumFILTER_WIRE_SPEC,FILTER_WIRE_NEGATION,decodeFilterWireValue,encodeFilterWireValue,FilterWireDecodeFailure+ wire typescodec-url's simple encoder shrinks by ~130 lines and keeps only the structural subset law (flat root-AND, one condition per field); its imports drop to
encodeFilterWireValue.Acceptance gate (wire frozen byte-for-byte)
nx run-many -t testgreen across all 8 test projects (uncached).filters-wire.spec.ts(65 cases): the grammar's first direct token↔condition coverage, pinning the frozen quirks ('!<5'→lt 5,'~'→contains '~','~5~'→contains '5'raw,','→in [], markers inert inside lists) plus two agreement probes — every row decodes back to its own operator (order = precedence, no shadowing) and every wire-expressible complement twin spells as'!'+ its positive form, matched againstFILTER_OPERATOR_SEMANTICS.tsc --extendedDiagnostics): Types 4180 → 4554, Instantiations 6780 → 7590, check time 0.09s → 0.11s.Out of scope (flagged in #797)
A real boolean through
parseTypedthrows in value normalization and the condition is dropped, although the input type advertises booleans — pre-existing, wire-frozen; separate issue to follow.