Skip to content

Commit

Permalink
feat(mux): add wildcard (*) for more flexible route matching (#3631)
Browse files Browse the repository at this point in the history
This pull request is for add some TODOs in `p/demo/mux` :

-  Add handling for NotFoundHandler
-  Add wildcard detection in route
Basic example:
```go
router.HandleFunc("r/user/*", func(rw *ResponseWriter, req *Request)) 
// match:
// "r/user/profile"           
// "r/user/posts"       
// "r/user/home/avatar"
  • Loading branch information
mous1985 authored Feb 5, 2025
1 parent d75f1a2 commit d7bfee2
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 31 deletions.
6 changes: 4 additions & 2 deletions examples/gno.land/p/demo/mux/handler.gno
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type Handler struct {

type HandlerFunc func(*ResponseWriter, *Request)

// TODO: type ErrHandlerFunc func(*ResponseWriter, *Request) error
// TODO: NotFoundHandler
type ErrHandlerFunc func(*ResponseWriter, *Request) error

type NotFoundHandler func(*ResponseWriter, *Request)

// TODO: AutomaticIndex
27 changes: 16 additions & 11 deletions examples/gno.land/p/demo/mux/request.gno
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,29 @@ type Request struct {

// GetVar retrieves a variable from the path based on routing rules.
func (r *Request) GetVar(key string) string {
var (
handlerParts = strings.Split(r.HandlerPath, "/")
reqParts = strings.Split(r.Path, "/")
)

for i := 0; i < len(handlerParts); i++ {
handlerPart := handlerParts[i]
handlerParts := strings.Split(r.HandlerPath, "/")
reqParts := strings.Split(r.Path, "/")
reqIndex := 0
for handlerIndex := 0; handlerIndex < len(handlerParts); handlerIndex++ {
handlerPart := handlerParts[handlerIndex]
switch {
case handlerPart == "*":
// XXX: implement a/b/*/d/e
panic("not implemented")
// If a wildcard "*" is found, consume all remaining segments
wildcardParts := reqParts[reqIndex:]
reqIndex = len(reqParts) // Consume all remaining segments
return strings.Join(wildcardParts, "/") // Return all remaining segments as a string
case strings.HasPrefix(handlerPart, "{") && strings.HasSuffix(handlerPart, "}"):
// If a variable of the form {param} is found we compare it with the key
parameter := handlerPart[1 : len(handlerPart)-1]
if parameter == key {
return reqParts[i]
return reqParts[reqIndex]
}
reqIndex++
default:
// continue
if reqIndex >= len(reqParts) || handlerPart != reqParts[reqIndex] {
return ""
}
reqIndex++
}
}

Expand Down
33 changes: 21 additions & 12 deletions examples/gno.land/p/demo/mux/request_test.gno
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package mux

import (
"fmt"
"testing"

"gno.land/p/demo/uassert"
"gno.land/p/demo/ufmt"
)

func TestRequest_GetVar(t *testing.T) {
Expand All @@ -12,28 +14,35 @@ func TestRequest_GetVar(t *testing.T) {
getVarKey string
expectedOutput string
}{

{"users/{id}", "users/123", "id", "123"},
{"users/123", "users/123", "id", ""},
{"users/{id}", "users/123", "nonexistent", ""},
{"a/{b}/c/{d}", "a/42/c/1337", "b", "42"},
{"a/{b}/c/{d}", "a/42/c/1337", "d", "1337"},
{"{a}", "foo", "a", "foo"},
// TODO: wildcards: a/*/c
// TODO: multiple patterns per slashes: a/{b}-{c}/d
}
{"users/{userId}/posts/{postId}", "users/123/posts/456", "userId", "123"},
{"users/{userId}/posts/{postId}", "users/123/posts/456", "postId", "456"},

// Wildcards
{"*", "users/123", "*", "users/123"},
{"*", "users/123/posts/456", "*", "users/123/posts/456"},
{"*", "users/123/posts/456/comments/789", "*", "users/123/posts/456/comments/789"},
{"users/*", "users/john/posts", "*", "john/posts"},
{"users/*/comments", "users/jane/comments", "*", "jane/comments"},
{"api/*/posts/*", "api/v1/posts/123", "*", "v1/posts/123"},

// wildcards and parameters
{"api/{version}/*", "api/v1/user/settings", "version", "v1"},
}
for _, tt := range cases {
name := fmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath)
name := ufmt.Sprintf("%s-%s", tt.handlerPath, tt.reqPath)
t.Run(name, func(t *testing.T) {
req := &Request{
HandlerPath: tt.handlerPath,
Path: tt.reqPath,
}

output := req.GetVar(tt.getVarKey)
if output != tt.expectedOutput {
t.Errorf("Expected '%q, but got %q", tt.expectedOutput, output)
}
uassert.Equal(t, tt.expectedOutput, output,
"handler: %q, path: %q, key: %q",
tt.handlerPath, tt.reqPath, tt.getVarKey)
})
}
}
35 changes: 30 additions & 5 deletions examples/gno.land/p/demo/mux/router.gno
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "strings"
// Router handles the routing and rendering logic.
type Router struct {
routes []Handler
NotFoundHandler HandlerFunc
NotFoundHandler NotFoundHandler
}

// NewRouter creates a new Router instance.
Expand All @@ -23,8 +23,14 @@ func (r *Router) Render(reqPath string) string {

for _, route := range r.routes {
patParts := strings.Split(route.Pattern, "/")

if len(patParts) != len(reqParts) {
wildcard := false
for _, part := range patParts {
if part == "*" {
wildcard = true
break
}
}
if !wildcard && len(patParts) != len(reqParts) {
continue
}

Expand All @@ -34,7 +40,7 @@ func (r *Router) Render(reqPath string) string {
reqPart := reqParts[i]

if patPart == "*" {
continue
break
}
if strings.HasPrefix(patPart, "{") && strings.HasSuffix(patPart, "}") {
continue
Expand Down Expand Up @@ -63,12 +69,31 @@ func (r *Router) Render(reqPath string) string {
return res.Output()
}

// Handle registers a route and its handler function.
// HandleFunc registers a route and its handler function.
func (r *Router) HandleFunc(pattern string, fn HandlerFunc) {
route := Handler{Pattern: pattern, Fn: fn}
r.routes = append(r.routes, route)
}

// HandleErrFunc registers a route and its error handler function.
func (r *Router) HandleErrFunc(pattern string, fn ErrHandlerFunc) {

// Convert ErrHandlerFunc to regular HandlerFunc
handler := func(res *ResponseWriter, req *Request) {
if err := fn(res, req); err != nil {
res.Write("Error: " + err.Error())
}
}

r.HandleFunc(pattern, handler)
}

// SetNotFoundHandler sets custom message for 404 defaultNotFoundHandler.
func (r *Router) SetNotFoundHandler(handler NotFoundHandler) {
r.NotFoundHandler = handler
}

// stripQueryString removes query string from the request path.
func stripQueryString(reqPath string) string {
i := strings.Index(reqPath, "?")
if i == -1 {
Expand Down
28 changes: 27 additions & 1 deletion examples/gno.land/p/demo/mux/router_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,33 @@ func TestRouter_Render(t *testing.T) {
})
},
},

{
label: "wildcard in route",
path: "hello/Alice/Bob",
expectedOutput: "Matched: Alice/Bob",
setupHandler: func(t *testing.T, r *Router) {
r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) {
path := req.GetVar("*")
uassert.Equal(t, "Alice/Bob", path)
uassert.Equal(t, "hello/Alice/Bob", req.Path)
rw.Write("Matched: " + path)
})
},
},
{
label: "wildcard in route with query string",
path: "hello/Alice/Bob?foo=bar",
expectedOutput: "Matched: Alice/Bob",
setupHandler: func(t *testing.T, r *Router) {
r.HandleFunc("hello/*", func(rw *ResponseWriter, req *Request) {
path := req.GetVar("*")
uassert.Equal(t, "Alice/Bob", path)
uassert.Equal(t, "hello/Alice/Bob?foo=bar", req.RawPath)
uassert.Equal(t, "hello/Alice/Bob", req.Path)
rw.Write("Matched: " + path)
})
},
},
// TODO: {"hello", "Hello, world!"},
// TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc
}
Expand Down

0 comments on commit d7bfee2

Please sign in to comment.