From 3f53ba50f2e562b5a2bc97442266b5e7acd585eb Mon Sep 17 00:00:00 2001 From: Eltrac Date: Fri, 28 Nov 2025 23:13:23 +0800 Subject: feat(router, context)!: support query string also move path split logic to parser.go --- engine.go | 3 ++- parser.go | 55 ++++++++++++++++++++++++++++++++++++++++ parser_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ router.go | 15 +++++------ router_test.go | 35 ++++++++------------------ types.go | 1 + 6 files changed, 154 insertions(+), 34 deletions(-) create mode 100644 parser.go create mode 100644 parser_test.go diff --git a/engine.go b/engine.go index e3c8ca8..f777e6d 100644 --- a/engine.go +++ b/engine.go @@ -33,7 +33,7 @@ func (api *Engine) DELETE(path string, h Handler) { // serve and start func (a *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - handler, params := a.router.Find(req.Method, req.URL.Path) + handler, params, query := a.router.Find(req.Method, req.URL.Path) if handler == nil { http.NotFound(w, req) return @@ -42,6 +42,7 @@ func (a *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { W: w, R: req, Params: params, + Querys: query, }) } diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..87b06d0 --- /dev/null +++ b/parser.go @@ -0,0 +1,55 @@ +package growel + +import ( + "fmt" + "strings" +) + +func ParsePath(path string) ([]string, map[string]string) { + var query_str string + path, query_str = extractQueryStr(path) + + parts := splitPath(path) + + if len(parts) == 0 { + return parts, map[string]string{} + } + + query := parseQueryStr(query_str) + + return parts, query +} + +func splitPath(path string) []string { + return strings.Split(strings.Trim(path, "/"), "/") +} + +func extractQueryStr(str string) (path string, querystr string) { + str_slice := strings.Split(str, "?") + leng := len(str_slice) + if leng > 2 { + fmt.Println("Error parsing path: multiple '?' found.") + // return anyway + } else if leng == 1 { + // no query string found + return str_slice[0], "" + } + return str_slice[0], str_slice[1] +} + +func parseQueryStr(str string) map[string]string { + query := make(map[string]string) + if str == "" { + return query + } + pairs := strings.Split(str, "&") + for _, pair := range pairs { + kv := strings.Split(pair, "=") + if len(kv) != 2 { + fmt.Println("Error parsing query string: invalid key-value pair.") + continue + } + query[kv[0]] = kv[1] + } + return query +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..c63f758 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,79 @@ +package growel + +import ( + "fmt" + "reflect" + "testing" +) + +func TestRouterSplitPath(t *testing.T) { + tests := []struct { + input string + want []string + }{ + {"/", []string{""}}, + {"/hello", []string{"hello"}}, + {"/hello/world", []string{"hello", "world"}}, + {"//hello//world//", []string{"hello", "", "world"}}, + {"/hello?world=true", []string{"hello?world=true"}}, + } + + for _, tt := range tests { + got := splitPath(tt.input) + if len(got) != len(tt.want) { + t.Fatalf("splitPath(%q) length mismatch: got %v want %v", tt.input, got, tt.want) + } + for i := range got { + if got[i] != tt.want[i] { + t.Fatalf("splitPath(%q) mismatch: got %v want %v", tt.input, got, tt.want) + } + } + } +} + +func TestExtractQueryStr(t *testing.T) { + tests := []struct { + input string + want1 string + want2 string + }{ + {"", "", ""}, + {"hello", "hello", ""}, + {"user/23/", "user/23/", ""}, + {"api?str=123&yrs=23", "api", "str=123&yrs=23"}, + {"w?st?r", "w", "st"}, + {"w?str", "w", "str"}, + } + + for _, tt := range tests { + got1, got2 := extractQueryStr(tt.input) + if got1 != tt.want1 && got2 != tt.want2 { + fmt.Printf("want1: %s, got1: %s\n", tt.want1, got1) + fmt.Printf("want2: %s, got2: %s\n", tt.want2, got2) + t.Fail() + } + } +} + +func TestParseQueryStr(t *testing.T) { + tests := []struct { + input string + want map[string]string + }{ + {"str=123&yrs=23", map[string]string{ + "str": "123", + "yrs": "23", + }}, + {"str=abc", map[string]string{ + "str": "abc", + }}, + } + + for _, tt := range tests { + got := parseQueryStr(tt.input) + if !reflect.DeepEqual(got, tt.want) { + fmt.Printf("want: %s, got: %s\n", tt.want, got) + t.Fail() + } + } +} diff --git a/router.go b/router.go index 961230a..d17d132 100644 --- a/router.go +++ b/router.go @@ -8,17 +8,14 @@ func NewRouter() *Router { return &Router{} } -func (r *Router) splitPath(path string) []string { - return strings.Split(strings.Trim(path, "/"), "/") -} - func (r *Router) Add(method string, path string, h Handler) { - parts := r.splitPath(path) + parts, _ := ParsePath(path) r.routes = append(r.routes, Route{method, parts, h}) } -func (r *Router) Find(method string, path string) (Handler, map[string]string) { - parts := r.splitPath(path) +func (r *Router) Find(method string, path string) ( + handler Handler, params map[string]string, query map[string]string) { + parts, query := ParsePath(path) for _, rt := range r.routes { // match HTTP method @@ -49,8 +46,8 @@ func (r *Router) Find(method string, path string) (Handler, map[string]string) { } } if matched { - return rt.handler, params + return rt.handler, params, query } } - return nil, nil + return nil, nil, nil } diff --git a/router_test.go b/router_test.go index 24a2faa..e03d16f 100644 --- a/router_test.go +++ b/router_test.go @@ -10,7 +10,7 @@ func TestRouterStaticMatch(t *testing.T) { r := NewRouter() r.Add("GET", "/hello", dummy) - h, params := r.Find("GET", "/hello") + h, params, _ := r.Find("GET", "/hello") if h == nil { t.Fatal("expected handler, got nil") } @@ -23,7 +23,7 @@ func TestRouterStaticNoMatch(t *testing.T) { r := NewRouter() r.Add("GET", "/hello", dummy) - h, _ := r.Find("GET", "/nothello") + h, _, _ := r.Find("GET", "/nothello") if h != nil { t.Fatal("expected nil handler, got non-nil") } @@ -33,7 +33,7 @@ func TestRouterParamMatch(t *testing.T) { r := NewRouter() r.Add("GET", "/user/:id", dummy) - h, params := r.Find("GET", "/user/42") + h, params, _ := r.Find("GET", "/user/42") if h == nil { t.Fatal("expected handler, got nil") } @@ -46,34 +46,21 @@ func TestRouterParamMismatch(t *testing.T) { r := NewRouter() r.Add("GET", "/user/:id", dummy) - h, _ := r.Find("GET", "/user") + h, _, _ := r.Find("GET", "/user") if h != nil { t.Fatal("expected nil handler, got non-nil") } } -func TestRouterSplitPath(t *testing.T) { +func TestRouterQueryMatch(t *testing.T) { r := NewRouter() + r.Add("GET", "/user", dummy) - tests := []struct { - input string - want []string - }{ - {"/", []string{""}}, - {"/hello", []string{"hello"}}, - {"/hello/world", []string{"hello", "world"}}, - {"//hello//world//", []string{"hello", "", "world"}}, + h, _, query := r.Find("GET", "/user?id=42") + if h == nil { + t.Fatal("expected handler, got nil") } - - for _, tt := range tests { - got := r.splitPath(tt.input) - if len(got) != len(tt.want) { - t.Fatalf("splitPath(%q) length mismatch: got %v want %v", tt.input, got, tt.want) - } - for i := range got { - if got[i] != tt.want[i] { - t.Fatalf("splitPath(%q) mismatch: got %v want %v", tt.input, got, tt.want) - } - } + if query["id"] != "42" { + t.Fatalf("expected id=42, got %v", query) } } diff --git a/types.go b/types.go index e785f4c..45bdf2c 100644 --- a/types.go +++ b/types.go @@ -6,6 +6,7 @@ type Context struct { W http.ResponseWriter R *http.Request Params map[string]string + Querys map[string]string } type Handler func(c *Context) -- cgit v1.2.3