Skip to content

feat: matched route pattern and method on results (opt-in routes: true) - #192

Draft
pi0x wants to merge 4 commits into
mainfrom
feat/matched-pattern
Draft

feat: matched route pattern and method on results (opt-in routes: true)#192
pi0x wants to merge 4 commits into
mainfrom
feat/matched-pattern

Conversation

@pi0x

@pi0x pi0x commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

🔍 What & why

findAllRoutes / compiled matchAll / findOverlappingRoutes returned { data, params } with no way to tell which registered pattern produced a layer. Consumers like h3-rules need per-layer pattern attribution (params attribution across pre-merged subsumption chains) and currently work around it by wrapping every registration's data in { route, method, rules }.

This adds an opt-in flag:

findAllRoutes(router, "GET", "/api/v1/users/42", { routes: true });
// [
//   { data, params, route: "/api/**", method: "GET" },
//   { data, params, route: "/api/:v/users/:id?", method: "" },
// ]

🛠️ How

  • addRoute now stores the original registered pattern (before internal group/modifier expansion, so /a/:x? reports /a/:x?) and the uppercased method on every tree entry — threaded through the expansion recursion via an internal _addRoute.
  • routes: true is accepted by findRoute, findAllRoutes, findOverlappingRoutes (new opts arg), and as a compiler option for compileRouter / compileRouterToString (applies to single-match and matchAll).
  • method is "" for method-agnostic registrations.
  • Opt-in by design: without the flag, results and generated compiler code are byte-for-byte unchanged (pinned by test). Core bundle grows ~200B raw / ~85B gzip for the stored fields + mapping (budget bumped with a comment, as usual).
  • Interpreter vs compiled parity with routes: true is pinned with exact toEqual in test/find-all.test.ts; docs added to README ("Matched route attribution") and AGENTS.md.

⚠️ Behavior changes

Two default-path (no-flag) behaviors change for existing consumers:

  • findOverlappingRoutes no longer collapses distinct registrations sharing one data reference. Registering the same middleware object on /a/** and /b/** previously returned one match; it now returns both (use routes: true to tell them apart). Optional/group expansions of a single registration still collapse to one match. Consumers deduplicating by result count must dedup by data reference themselves.
  • Default findRoute for static paths returns a mapped { data, params } object instead of the raw tree entry (parity with the dynamic path and compiled output). Internal fields like paramsRegexp are no longer visible on default results, findRoute(...) === findRoute(..., { params: false }) no longer holds for static routes, and mutating a raw entry's data (via params: false) after registration is not supported — default results snapshot data at addRoute time. Param-less default matches are one shared object per entry (documented in README; clone before mutating).

Resolves item 4 of the h3-rules needs list.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added opt-in route attribution via routes: true for findRoute, findAllRoutes, findOverlappingRoutes, and compileRouter(..., { routes: true }), returning route/method alongside data.
  • Bug Fixes
    • Improved overlapping-route deduplication so distinct registered routes sharing the same underlying data are no longer collapsed.
  • Documentation
    • Updated public API docs and README, including the new opts parameter support and match/mutability semantics.
  • Tests
    • Added/expanded tests for interpreter vs compiled parity and route-attribution/uniqueness behavior.

…ue`)

addRoute stores the original registered pattern and method on each entry;
findRoute/findAllRoutes/findOverlappingRoutes and the compiler expose them
behind an opt-in `routes: true` flag so compiled output size is unchanged
for consumers that don't need attribution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pi0x
pi0x requested a review from pi0 as a code owner July 6, 2026 18:28
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c369212-60dc-4795-8d8a-6698e5ddbf29

📥 Commits

Reviewing files that changed from the base of the PR and between c094b37 and cf89b02.

📒 Files selected for processing (1)
  • src/compiler.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/compiler.ts

📝 Walkthrough

Walkthrough

This PR adds opt-in route and method attribution across route lookup, overlap matching, and compiled output. It also updates stored match shapes, public type contracts, docs, tests, and bundle-size thresholds.

Changes

Route attribution feature

Layer / File(s) Summary
Type contracts for attributed matches
src/types.ts
RouterContext and MatchedRoute gain route, method, and precomputed res fields.
Route registration and stored entries
src/operations/add.ts
addRoute normalizes path and method before delegation, preserves the original registered pattern through expansion, and stores route, method, and a cached res on each entry.
findRoute lookup and return shapes
src/operations/find.ts, test/find.test.ts
findRoute adds opts.routes, delays segment splitting until needed, and returns raw, attributed, or cached match objects depending on options.
findAllRoutes attribution passthrough
src/operations/find-all.ts, test/find-all.test.ts
findAllRoutes adds opts.routes, returns raw entries for params: false, and otherwise includes route and method when attribution is enabled.
findOverlappingRoutes attribution and deduping
src/operations/overlap.ts, test/overlap.test.ts
findOverlappingRoutes adds opts.routes, returns attributed overlap entries, and dedupes by data reference plus route and method.
Compiler support
src/compiler.ts
The compiler gains a routes option, emits route and method in compiled matches, and separates serialization of data vs route strings.
Docs and bundle thresholds
AGENTS.md, README.md, test/bench/bundle.test.ts
Documentation is expanded for attribution, reuse, and overlap behavior, and the bundle-size assertions are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

Possibly related PRs

  • h3js/rou3#178: Both changes touch src/operations/add.ts’s route expansion and registration flow.
  • h3js/rou3#183: Both changes modify src/operations/overlap.ts and the findOverlappingRoutes API shape.

Suggested reviewers: pi0

Poem

A rabbit hopped through routes with glee,
and tagged each match with route and key.
I nibbled tests, then bounded near,
so compiled paths and overlaps are clear. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main opt-in route attribution change and mentions the key routes: true flag.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/matched-pattern

Warning

Tools execution failed with the following error:

Failed to run tools: Stream initialization permanently failed: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

- findOverlappingRoutes dedups per registration (data + route + method),
  so distinct patterns sharing one data reference are all reported
- default findRoute results never leak route/method/paramsRegexp: param-less
  matches return a per-entry object precomputed in addRoute (zero allocation),
  param matches stay a fresh { data, params } — no attribution cost unless
  routes: true is set
- params: false is the only raw-entry return path (findRoute + findAllRoutes)
- compiler dedups route/method strings through the existing $N data table
- MethodData route/method are required; overlap drops positional flag threading

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/operations/find.ts (1)

11-11: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared opts shape into a named interface.

This inline object-literal type is duplicated verbatim in findAllRoutes (src/operations/find-all.ts, Line 11). Consider hoisting a single interface FindOptions { params?: boolean; routes?: boolean; normalize?: boolean } (e.g. in src/types.ts) and reusing it here and in findAllRoutes.

As per coding guidelines, "Prefer interface for defining object shapes in TypeScript."

♻️ Proposed refactor
+// in src/types.ts
+export interface FindOptions {
+  params?: boolean;
+  routes?: boolean;
+  normalize?: boolean;
+}
-  opts?: { params?: boolean; routes?: boolean; normalize?: boolean },
+  opts?: FindOptions,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/operations/find.ts` at line 11, Extract the duplicated inline opts object
shape into a named interface, such as FindOptions, instead of keeping it inline
in find and findAllRoutes. Define the shared object shape once in a common types
location (for example, a shared types module) and update the find and
findAllRoutes signatures to reference that interface. This keeps the opts type
consistent across both functions and follows the guideline to prefer interface
for object shapes.

Source: Coding guidelines

src/operations/overlap.ts (1)

179-197: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a regression test for the primitive-data + expansion tradeoff.

The dedup intentionally skips primitive/absent data (per the comment above), so a route registered with optional/group syntax (e.g. /a/:x?) and a primitive data value will report its pattern once per expanded internal entry instead of collapsing to a single match — unlike the reference-type case, which is already covered in test/overlap.test.ts (Lines 223-231) using object data. Adding an equivalent test with primitive data would lock in this documented tradeoff and guard against an unintended future "fix" that silently changes duplicate-registration detection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/operations/overlap.ts` around lines 179 - 197, Add a regression test in
test/overlap.test.ts covering optional/group expansion with primitive data,
since overlap dedup in src/operations/overlap.ts intentionally does not collapse
non-reference data. Mirror the existing object-data coverage around the
overlap/registration duplicate cases, but register a route like /a/:x? with a
primitive payload and assert the expanded entries are reported separately. This
will lock in the current tradeoff and prevent future changes to shapesOverlap,
seen, or the entry processing loop from accidentally deduping primitive-data
registrations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/operations/add.ts`:
- Line 128: The route data assignment in add should preserve valid falsy values
instead of converting them to null; using data || (null as T) causes 0, empty
strings, and false to be dropped. Update the _data initialization in add to use
nullish coalescing so only undefined or null fall back to the sentinel, keeping
falsy payloads intact when routes match.

---

Nitpick comments:
In `@src/operations/find.ts`:
- Line 11: Extract the duplicated inline opts object shape into a named
interface, such as FindOptions, instead of keeping it inline in find and
findAllRoutes. Define the shared object shape once in a common types location
(for example, a shared types module) and update the find and findAllRoutes
signatures to reference that interface. This keeps the opts type consistent
across both functions and follows the guideline to prefer interface for object
shapes.

In `@src/operations/overlap.ts`:
- Around line 179-197: Add a regression test in test/overlap.test.ts covering
optional/group expansion with primitive data, since overlap dedup in
src/operations/overlap.ts intentionally does not collapse non-reference data.
Mirror the existing object-data coverage around the overlap/registration
duplicate cases, but register a route like /a/:x? with a primitive payload and
assert the expanded entries are reported separately. This will lock in the
current tradeoff and prevent future changes to shapesOverlap, seen, or the entry
processing loop from accidentally deduping primitive-data registrations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8b14480c-b986-4294-83eb-360a4199e6b4

📥 Commits

Reviewing files that changed from the base of the PR and between 236612e and 61d8ec2.

📒 Files selected for processing (12)
  • AGENTS.md
  • README.md
  • src/compiler.ts
  • src/operations/add.ts
  • src/operations/find-all.ts
  • src/operations/find.ts
  • src/operations/overlap.ts
  • src/types.ts
  • test/bench/bundle.test.ts
  • test/find-all.test.ts
  • test/find.test.ts
  • test/overlap.test.ts

Comment thread src/operations/add.ts

// Assign index, params and data to the node
const hasParams = paramsMap.length > 0;
const _data = data || (null as T);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect current line and its history to determine if `||` predates this PR.
fd -t f 'add.ts' src/operations --exec rg -n 'null as T' {}
git log -L '/const _data/,+1:src/operations/add.ts' -n 2 --oneline 2>/dev/null || \
  git log -S 'null as T' --oneline -- src/operations/add.ts

Repository: h3js/rou3

Length of output: 421


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first.
ast-grep outline src/operations/add.ts --view expanded || true
ast-grep outline src/operations/overlap.ts --view expanded || true

# Read the relevant sections with line numbers.
printf '\n--- src/operations/add.ts ---\n'
sed -n '1,220p' src/operations/add.ts | cat -n
printf '\n--- src/operations/overlap.ts ---\n'
sed -n '1,240p' src/operations/overlap.ts | cat -n

# Search for null sentinel usage and route data handling.
printf '\n--- sentinel searches ---\n'
rg -n "null as T|null\)|=== null|== null|data \\|\\||data \\?\\?" src/operations src | sed -n '1,200p'

Repository: h3js/rou3

Length of output: 18148


Preserve falsy route data data || (null as T) turns 0, "", and false into null, so those registered values are lost on match. null is already the missing-data sentinel here, so this should be data ?? (null as T) if falsy payloads are allowed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/operations/add.ts` at line 128, The route data assignment in add should
preserve valid falsy values instead of converting them to null; using data ||
(null as T) causes 0, empty strings, and false to be dropped. Update the _data
initialization in add to use nullish coalescing so only undefined or null fall
back to the sentinel, keeping falsy payloads intact when routes match.

pi0 and others added 2 commits July 6, 2026 20:21
- unify the `routes` flag check (truthiness everywhere, matching
  findRoute/compiler) so all entry points agree for plain-JS callers
- key registration dedup in findOverlappingRoutes on nested maps
  (data -> method -> route) instead of a space-delimited composite
  string that unvalidated method strings could collide
- fall back to a fresh { data, params } when an entry lacks the
  precomputed `res` (context built by pre-attribution rou3) instead
  of silently reporting a match as a miss
- document shared-res scope and the data-snapshot limitation in
  README/AGENTS.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dataRef scanned ctx.data with indexOf per reference — quadratic over
the table, which routes:true roughly doubles (route/method strings).
A value->index Map makes it O(1): compileRouterToString on a 10k-route
router drops 263ms -> 40ms (889ms -> 36ms with routes:true).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pi0
pi0 marked this pull request as draft July 6, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants