Skip to content

Commit 53de839

Browse files
authored
Structured pattern errors with position tracking (#79)
* refactor(parser): extract route parsing into dedicated sub-parsers * feat(parser): add structured PatternError with position tracking for diagnostics * feat(parser): return param count from parsePattern and auto-append trailing slash for hostname-only pattern * feat(parser): reject unclean paths with structured errors instead of silent cleanup * perf(route): reorder Route struct fields for memory alignment * ci: bump minimum Go version to 1.26 * feat(error)!: add Unwrap to PatternError and remove redundant sentinel errors * refactor(parser): inline hostnameValidator into parseHostname * fix(parser): improve error position accuracy for pattern diagnostics * build: update golang.org/x dependencies * fix(parser): use original regexp error as hint instead of prefixed message
1 parent e411077 commit 53de839

17 files changed

Lines changed: 2266 additions & 1751 deletions

.github/workflows/tests.yaml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ on:
33
push:
44
workflow_dispatch:
55

6+
permissions:
7+
contents: read
8+
69
jobs:
710
test:
811
name: Test Fox
912
runs-on: ubuntu-latest
1013
strategy:
1114
matrix:
12-
go: [ '>=1.24' ]
15+
go: [ '>=1.26' ]
1316
steps:
1417
- name: Set up Go
1518
uses: actions/setup-go@v6
@@ -19,6 +22,8 @@ jobs:
1922

2023
- name: Check out code
2124
uses: actions/checkout@v6
25+
with:
26+
persist-credentials: false
2227

2328
- name: Run tests
2429
run: go test -v -coverprofile=coverage.txt -covermode=atomic ./...
@@ -37,7 +42,7 @@ jobs:
3742
runs-on: ubuntu-latest
3843
strategy:
3944
matrix:
40-
go: [ '>=1.24' ]
45+
go: [ '>=1.26' ]
4146
steps:
4247
- name: Set up Go
4348
uses: actions/setup-go@v6
@@ -47,6 +52,8 @@ jobs:
4752

4853
- name: Check out code
4954
uses: actions/checkout@v6
55+
with:
56+
persist-credentials: false
5057

5158
- name: Run linter
5259
uses: golangci/golangci-lint-action@v9

error.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,7 @@ var (
1919
ErrNoClientIPResolver = errors.New("no client ip resolver")
2020
ErrReadOnlyTxn = errors.New("write on read-only transaction")
2121
ErrSettledTxn = errors.New("transaction settled")
22-
ErrParamKeyTooLarge = errors.New("parameter key too large")
23-
ErrTooManyParams = errors.New("too many params")
2422
ErrTooManyMatchers = errors.New("too many matchers")
25-
ErrRegexpNotAllowed = errors.New("regexp not allowed")
2623
ErrInvalidConfig = errors.New("invalid config")
2724
ErrInvalidMatcher = errors.New("invalid matcher")
2825
)
@@ -44,7 +41,7 @@ func (e *RouteConflictError) Error() string {
4441
routef(sb, e.New, 4, true)
4542

4643
if e.isShadowed {
47-
if e.New.catchEmpty {
44+
if e.New.pattern.optionalCatchAll {
4845
sb.WriteString("\nis shadowed by")
4946
} else {
5047
sb.WriteString("\nwould shadow")
@@ -96,3 +93,58 @@ func newRouteNotFoundError(route *Route) error {
9693
sb.WriteString("\nis not registered")
9794
return fmt.Errorf("%w: %s", ErrRouteNotFound, sb.String())
9895
}
96+
97+
type PatternError struct {
98+
err error // wrapped error
99+
Pattern string // provided pattern
100+
Type string // hostname | path
101+
Reason string // syntax | parameter | regexp | constraint
102+
Hint string // hint
103+
Start int // start offset of the offending segment
104+
End int // end offset of the offending segment
105+
}
106+
107+
// Unwrap returns the underlying error, if any.
108+
func (e *PatternError) Unwrap() error {
109+
return e.err
110+
}
111+
112+
// Error returns a human-readable error message with a visual pointer to the offending segment.
113+
func (e *PatternError) Error() string {
114+
var sb strings.Builder
115+
sb.WriteString("pattern: ")
116+
if e.Type != "" {
117+
sb.WriteString(e.Type)
118+
sb.WriteString(": ")
119+
}
120+
sb.WriteString(e.Reason)
121+
sb.WriteString(": ")
122+
sb.WriteString(e.Hint)
123+
if e.Pattern != "" {
124+
sb.WriteByte('\n')
125+
sb.WriteString(" ")
126+
sb.WriteString(e.Pattern)
127+
sb.WriteByte('\n')
128+
sb.WriteString(" ")
129+
for i := 0; i < e.Start; i++ {
130+
sb.WriteByte(' ')
131+
}
132+
n := e.End - e.Start
133+
if n <= 0 {
134+
n = 1
135+
}
136+
for i := 0; i < n; i++ {
137+
sb.WriteByte('^')
138+
}
139+
}
140+
return sb.String()
141+
}
142+
143+
func newPatternError(reason string, start, end int, msg string) *PatternError {
144+
return &PatternError{
145+
Reason: reason,
146+
Start: start,
147+
End: end,
148+
Hint: msg,
149+
}
150+
}

0 commit comments

Comments
 (0)