Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mux): add wildcard (*) for more flexible route matching #3631

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading