diff --git a/examples/gno.land/p/demo/mux/handler.gno b/examples/gno.land/p/demo/mux/handler.gno index 835d050a52c..4d937dbacab 100644 --- a/examples/gno.land/p/demo/mux/handler.gno +++ b/examples/gno.land/p/demo/mux/handler.gno @@ -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 diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index 7b5b74da91b..eaa2f287069 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -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++ } } diff --git a/examples/gno.land/p/demo/mux/request_test.gno b/examples/gno.land/p/demo/mux/request_test.gno index 5f8088b4964..24c611c1f9d 100644 --- a/examples/gno.land/p/demo/mux/request_test.gno +++ b/examples/gno.land/p/demo/mux/request_test.gno @@ -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) { @@ -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) }) } } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index fe6bf70abdf..4fca43a0378 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -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. @@ -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 } @@ -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 @@ -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 { diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index cc6aad62146..c1c5d218165 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -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 }