Skip to content

Commit 22c2c68

Browse files
committed
refactor!: remove sub-router support
1 parent 5b6f695 commit 22c2c68

8 files changed

Lines changed: 7 additions & 923 deletions

File tree

README.md

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ The current API is not yet stabilized. Breaking changes may occur before `v1.0.0
3030

3131
**Flexible routing:** Fox strikes a balance between routing flexibility, performance and clarity by enforcing clear priority rules, ensuring that
3232
there are no unintended matches and maintaining high performance even for complex routing patterns. Supported features include named parameters,
33-
suffix and infix catch-all, regexp constraints, hostname matching, method and method-less routes, route matchers, and sub-routers.
33+
suffix and infix catch-all, regexp constraints, hostname matching, method and method-less routes, and route matchers.
3434

3535
**Trailing slash handling:** Automatically handle trailing slash inconsistencies by either ignoring them, redirecting to
3636
the canonical path, or enforcing strict matching based on your needs.
@@ -51,7 +51,6 @@ the canonical path, or enforcing strict matching based on your needs.
5151
* [Named wildcards](#named-wildcards-catch-all)
5252
* [Route matchers](#route-matchers)
5353
* [Method-less routes](#method-less-routes)
54-
* [Sub-Routers](#sub-routers)
5554
* [Hostname validation & restrictions](#hostname-validation--restrictions)
5655
* [Path encoding](#path-encoding)
5756
* [Priority rules](#priority-rules)
@@ -250,28 +249,6 @@ f.MustAdd(fox.MethodGet, "/resource", GetHandler)
250249
f.MustAdd(fox.MethodAny, "/resource", FallbackHandler)
251250
````
252251

253-
#### Sub-Routers
254-
Fox provides a composable routing API where routers can be mounted as regular routes, each with its own middleware and configuration.
255-
256-
```go
257-
api := fox.MustRouter(fox.WithMiddleware(AuthMiddleware()))
258-
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/", HelloHandler)
259-
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUser)
260-
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users/{id}", GetUser)
261-
api.MustAdd(fox.MethodPost, "/users", CreateUser)
262-
263-
f := fox.MustRouter(fox.DefaultOptions())
264-
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))
265-
f.MustAdd(fox.MethodAny, "/api*{mount}", fox.Sub(api))
266-
```
267-
268-
Requests matching the prefix are delegated to the mounted router with the remaining path.
269-
270-
Use cases include:
271-
- Applying middleware, matchers or other configuration to a route prefix
272-
- Managing entire route subtree at runtime (e.g. insert, update, or delete via the parent router)
273-
- Organizing routes into groups with shared configuration
274-
275252
#### Hostname validation & restrictions
276253

277254
Hostnames are validated to conform to the [LDH (letters, digits, hyphens) rule](https://datatracker.ietf.org/doc/html/rfc3696.html#section-2)
@@ -643,44 +620,6 @@ func main() {
643620
}
644621
````
645622

646-
Alternatively, you can use a sub-router to apply CORS only to a specific section of your API.
647-
648-
````go
649-
package main
650-
651-
import (
652-
"log"
653-
"net/http"
654-
655-
"github.com/jub0bs/cors"
656-
"github.com/fox-toolkit/fox"
657-
)
658-
659-
func main() {
660-
corsMw, err := cors.NewMiddleware(cors.Config{
661-
Origins: []string{"https://example.com"},
662-
Methods: []string{http.MethodHead, http.MethodGet, http.MethodPost},
663-
RequestHeaders: []string{"Authorization"},
664-
})
665-
if err != nil {
666-
log.Fatal(err)
667-
}
668-
corsMw.SetDebug(true) // turn debug mode on (optional)
669-
670-
f := fox.MustRouter()
671-
f.MustAdd([]string{http.MethodHead, http.MethodGet}, "/*{filepath}", fox.WrapH(http.FileServer(http.Dir("./public/"))))
672-
673-
api := fox.MustRouter(
674-
fox.WithAutoOptions(true), // let Fox automatically handle OPTIONS requests
675-
fox.WithMiddlewareFor(fox.RouteHandler|fox.OptionsHandler, fox.WrapM(corsMw.Wrap)),
676-
)
677-
api.MustAdd([]string{http.MethodHead, http.MethodGet}, "/users", ListUsers)
678-
api.MustAdd(fox.MethodPost, "/users", CreateUser)
679-
680-
f.MustAdd(fox.MethodAny, "/api*{any}", fox.Sub(api)) // note: Method-less route
681-
}
682-
````
683-
684623
The CORS protocol is complex and security-sensitive. We do **NOT** recommend implementing CORS handling manually. Instead,
685624
consider using [jub0bs/cors](https://github.com/jub0bs/cors), which performs extensive validation before allowing middleware creation, helping you avoid common pitfalls.
686625

benchmark_test.go

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -271,37 +271,6 @@ func BenchmarkContext_CloneWith(b *testing.B) {
271271
}
272272
}
273273

274-
func BenchmarkSubRouter(b *testing.B) {
275-
276-
main := MustRouter()
277-
sub1 := MustRouter()
278-
sub2 := MustRouter()
279-
280-
sub2.MustAdd(MethodGet, "/users/email", emptyHandler)
281-
sub1.MustAdd(MethodAny, "/{name}/*{any}", Sub(sub2))
282-
main.MustAdd(MethodAny, "/{v1}/*{any}", Sub(sub1))
283-
284-
req := httptest.NewRequest(http.MethodGet, "/v1/john/users/email", nil)
285-
w := new(mockResponseWriter)
286-
287-
b.ResetTimer()
288-
b.ReportAllocs()
289-
for range b.N {
290-
main.ServeHTTP(w, req)
291-
}
292-
}
293-
294-
func BenchmarkStaticAllSubRouter(b *testing.B) {
295-
f := MustRouter()
296-
sub := MustRouter()
297-
for _, route := range staticRoutes {
298-
require.NoError(b, onlyError(sub.Add([]string{route.method}, route.path, emptyHandler)))
299-
}
300-
f.MustAdd(MethodAny, "example.com/*{any}", Sub(sub))
301-
302-
benchRoute(b, f, staticRoutes)
303-
}
304-
305274
func BenchmarkVeryLongPattern(b *testing.B) {
306275
f := MustRouter()
307276
f.MustAdd(MethodGet, "/hello/very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very_very", emptyHandler)

context.go

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"net/http"
1313
"net/url"
1414
"slices"
15-
"strings"
1615

1716
"github.com/fox-toolkit/fox/internal/bytesconv"
1817
"github.com/fox-toolkit/fox/internal/netutil"
@@ -71,8 +70,6 @@ type Context struct {
7170
w ResponseWriter
7271
req *http.Request
7372
params *[]string
74-
paramsKeys *[]string
75-
subPatterns *[]string
7673
skipStack *skipStack
7774
route *Route
7875
tree *iTree // no reset
@@ -94,8 +91,6 @@ func (c *Context) reset(w http.ResponseWriter, r *http.Request) {
9491
c.cachedQueries = nil
9592
c.scope = RouteHandler
9693
*c.params = (*c.params)[:0]
97-
*c.paramsKeys = (*c.paramsKeys)[:0]
98-
*c.subPatterns = (*c.subPatterns)[:0]
9994
}
10095

10196
func (c *Context) resetNil() {
@@ -104,8 +99,6 @@ func (c *Context) resetNil() {
10499
c.cachedQueries = nil
105100
c.route = nil
106101
*c.params = (*c.params)[:0]
107-
*c.paramsKeys = (*c.paramsKeys)[:0]
108-
*c.subPatterns = (*c.subPatterns)[:0]
109102
}
110103

111104
// resetWithRequest resets the [Context] to its initial state, with the provided [http.Request]. This is used
@@ -126,8 +119,6 @@ func (c *Context) resetWithWriter(w ResponseWriter, r *http.Request) {
126119
c.cachedQueries = nil
127120
c.scope = RouteHandler
128121
*c.params = (*c.params)[:0]
129-
*c.paramsKeys = (*c.paramsKeys)[:0]
130-
*c.subPatterns = (*c.subPatterns)[:0]
131122
}
132123

133124
// Request returns the [http.Request].
@@ -189,9 +180,8 @@ func (c *Context) ClientIP() (*net.IPAddr, error) {
189180
// Params returns an iterator over the matched wildcard parameters for the current route.
190181
func (c *Context) Params() iter.Seq[Param] {
191182
return func(yield func(Param) bool) {
192-
keys := c.keys()
193183
for i, p := range *c.params {
194-
if !yield(Param{Key: keys[i], Value: p}) {
184+
if !yield(Param{Key: c.route.params[i], Value: p}) {
195185
return
196186
}
197187
}
@@ -200,25 +190,14 @@ func (c *Context) Params() iter.Seq[Param] {
200190

201191
// Param retrieve a matching wildcard segment by name.
202192
func (c *Context) Param(name string) string {
203-
keys := c.keys()
204193
for i, p := range *c.params {
205-
if keys[i] == name {
194+
if c.route.params[i] == name {
206195
return p
207196
}
208197
}
209198
return ""
210199
}
211200

212-
func (c *Context) keys() []string {
213-
if len(*c.paramsKeys) > 0 {
214-
return *c.paramsKeys
215-
}
216-
if c.route != nil {
217-
return c.route.params
218-
}
219-
return nil
220-
}
221-
222201
// Method returns the request method.
223202
func (c *Context) Method() string {
224203
return c.req.Method
@@ -275,20 +254,7 @@ func (c *Context) Header(key string) string {
275254

276255
// Pattern returns the registered route pattern or an empty string if the handler is called in a scope other than [RouteHandler].
277256
func (c *Context) Pattern() string {
278-
switch len(*c.subPatterns) {
279-
case 0:
280-
return c.pattern
281-
case 1:
282-
return (*c.subPatterns)[0] + c.pattern
283-
}
284-
285-
var sb strings.Builder
286-
sb.Grow(len(c.pattern) + sumLen(*c.subPatterns))
287-
for _, p := range *c.subPatterns {
288-
sb.WriteString(p)
289-
}
290-
sb.WriteString(c.pattern)
291-
return sb.String()
257+
return c.pattern
292258
}
293259

294260
// Route returns the registered [Route] or nil if the handler is called in a scope other than [RouteHandler].
@@ -342,18 +308,10 @@ func (c *Context) Clone() *Context {
342308
cp.rec.ResponseWriter = noopWriter{c.rec.Header().Clone()}
343309
cp.w = noUnwrap{&cp.rec}
344310

345-
subPatterns := make([]string, len(*c.subPatterns))
346-
copy(subPatterns, *c.subPatterns)
347-
cp.subPatterns = &subPatterns
348-
349311
params := make([]string, len(*c.params))
350312
copy(params, *c.params)
351313
cp.params = &params
352314

353-
keys := make([]string, len(*c.paramsKeys))
354-
copy(keys, *c.paramsKeys)
355-
cp.paramsKeys = &keys
356-
357315
return &cp
358316
}
359317

@@ -371,8 +329,6 @@ func (c *Context) CloneWith(w ResponseWriter, r *http.Request) *Context {
371329
cp.pattern = c.pattern
372330
cp.cachedQueries = nil // For safety, in case r is a different request than c.req
373331

374-
copyWithResize(cp.subPatterns, c.subPatterns)
375-
copyWithResize(cp.paramsKeys, c.paramsKeys)
376332
copyWithResize(cp.params, c.params)
377333

378334
return cp
@@ -513,11 +469,3 @@ func (mw wrapM) handle(c *Context) {
513469
mw.next(cc)
514470
})).ServeHTTP(flusherWriter{c.Writer()}, r)
515471
}
516-
517-
func sumLen(s []string) int {
518-
var n int
519-
for _, v := range s {
520-
n += len(v)
521-
}
522-
return n
523-
}

context_test.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,6 @@ func TestWrapF(t *testing.T) {
493493
rte, err := f.NewRoute(MethodGet, "/{foo}", emptyHandler)
494494
require.NoError(t, err)
495495
c.route = rte
496-
*c.paramsKeys = rte.params
497496

498497
params := make(Params, 0)
499498
if tc.params != nil {
@@ -561,7 +560,6 @@ func TestWrapH(t *testing.T) {
561560
rte, err := f.NewRoute(MethodGet, "/{foo}", emptyHandler)
562561
require.NoError(t, err)
563562
c.route = rte
564-
*c.paramsKeys = rte.params
565563

566564
params := make(Params, 0)
567565
if tc.params != nil {

0 commit comments

Comments
 (0)