diff --git a/examples/gno.land/p/demo/mux/query.gno b/examples/gno.land/p/demo/mux/query.gno new file mode 100644 index 00000000000..dc464750f3c --- /dev/null +++ b/examples/gno.land/p/demo/mux/query.gno @@ -0,0 +1,67 @@ +package mux + +import "strings" + +type QueryValues map[string][]string + +/* +Query() returns a full map of key-listValue from request +to get a list of params by key, use: req.Query()["key"] +to get single param by key, use: req.Query().Get("key") +to get request path use GetPath() +*/ + +func (r *Request) QueryFull() (QueryValues, string) { + urlQueries := QueryValues{} + pathQuery := "" + // checking if there is query in r.Path + if strings.Contains(r.Path, "?") { + // get baseUrl and queryString + rParts := strings.SplitN(r.Path, "?", 2) + if len(rParts) == 1 { + // if this is not a `query string` query, then returns full path as queryPath + return urlQueries, rParts[0] + } + baseUrl := rParts[0] + queryString := rParts[1] + + // find the target path + baseUrlParts := strings.Split(baseUrl, "/") + rTargetPath := baseUrlParts[len(baseUrlParts)-1] + pathQuery = rTargetPath + + // process the queryString + // find the first param index + if strings.Contains(queryString, "=") { + for _, keyValue := range strings.Split(queryString, "&") { + parts := strings.SplitN(keyValue, "=", 2) + if len(parts) != 2 { + return urlQueries, rParts[0] + } + key, value := parts[0], parts[1] + urlQueries[key] = append(urlQueries[key], value) + } + } + } + return urlQueries, pathQuery +} + +// get the query by key +// if there is more than one query, returns the very first param +func (qs QueryValues) Get(key string) string { + listMatching, ok := qs[key] + if ok { + return listMatching[0] + } + return "" +} + +func (r *Request) Query() QueryValues { + uQuery, _ := r.QueryFull() + return uQuery +} + +func (r *Request) GetQueryPath() string { + _, pQuery := r.QueryFull() + return pQuery +} diff --git a/examples/gno.land/p/demo/mux/query_test.gno b/examples/gno.land/p/demo/mux/query_test.gno new file mode 100644 index 00000000000..22a7ed5b891 --- /dev/null +++ b/examples/gno.land/p/demo/mux/query_test.gno @@ -0,0 +1,101 @@ +package mux + +import ( + "fmt" + "testing" +) + +func TestQuery_Get(t *testing.T) { + cases := []struct { + name string + reqPath string + key string + expectedOutput string + }{ + // get by key + {"Get_query_key", "api/v1/?name=testname&age=12", "name", "testname"}, + {"Get_query_repeat", "api/v1/user?name=name1&name=name2&name=name3", "name", "name1"}, + {"Get_query_with_pre_path", "api/v1/unprocesspath?user=thinhnx&age=12", "user", "thinhnx"}, + {"Get_query_empty", "api/v1/?name=testname&age=12", "loc", ""}, + {"Get_query_with_slash", "api/v1/?name=testname&endpoint=testdomain.com/v1/with/slash/", "endpoint", "testdomain.com/v1/with/slash/"}, + {"Get_query_with_extra_slash", "api/v1/unprocesspath?user=thinhnx&age=12&addr=12/34/NewYork//", "addr", "12/34/NewYork//"}, + } + + for _, tt := range cases { + name := fmt.Sprintf("%s-%s", tt.name, tt.reqPath) + t.Run(name, func(t *testing.T) { + req := &Request{ + HandlerPath: "", + Path: tt.reqPath, + } + output := req.Query().Get(tt.key) + if output != tt.expectedOutput { + t.Errorf("Expected %q, but got %q", tt.expectedOutput, output) + } + }) + } +} + +func TestQuery_QueryList(t *testing.T) { + cases := []struct { + name string + reqPath string + key string + expectedOutput []string + }{ + // get all queries + {"Get_name_list", "api/v1/user?name=name0&age=12&name=name1&age=30", "name", []string{"name0", "name1"}}, + {"Get_age_list", "api/v1/user?name=name0&age=12&name=name1&age=30", "age", []string{"12", "30"}}, + {"Get_loc_list", "api/v1/user?name=name0&age=12&name=name1&age=30&loc=HN", "loc", []string{"HN"}}, + {"Get_empty_list", "api/v1/user?name=name0&age=12&name=name1&age=30&loc=HN", "addr", []string{}}, + } + + for _, tt := range cases { + name := fmt.Sprintf("%s-%s", tt.name, tt.reqPath) + t.Run(name, func(t *testing.T) { + req := &Request{ + HandlerPath: "", + Path: tt.reqPath, + } + // simple check for length of expected and output list + output := req.Query()[tt.key] + if len(output) != len(tt.expectedOutput) { + t.Errorf("Expected %q, but got %q", tt.expectedOutput, output) + } + // check the corresponding elements + listOutput := req.Query()[tt.key] + for i, ttExpt := range tt.expectedOutput { + if ttExpt != listOutput[i] { + t.Errorf("Expected %q, but got %q", ttExpt, listOutput[i]) + } + } + }) + } +} + +func TestQuery_GetPath(t *testing.T) { + cases := []struct { + name string + reqPath string + expectedOutput string + }{ + // get all queries + {"Get_user", "api/v1/user?name=name1&name=name2&name=name3", "user"}, + {"Get_empty_path", "api/v1/?user=thinhnx&age=12&addr=12/34/NewYork", ""}, + } + + for _, tt := range cases { + name := fmt.Sprintf("%s-%s", tt.name, tt.reqPath) + t.Run(name, func(t *testing.T) { + req := &Request{ + HandlerPath: "", + Path: tt.reqPath, + } + // check the corresponding elements + outputGP := req.GetQueryPath() + if tt.expectedOutput != outputGP { + t.Errorf("Expected %q, but got %q", tt.expectedOutput, outputGP) + } + }) + } +} diff --git a/examples/gno.land/p/demo/mux/request.gno b/examples/gno.land/p/demo/mux/request.gno index f7996fe40fe..d8057910e3f 100644 --- a/examples/gno.land/p/demo/mux/request.gno +++ b/examples/gno.land/p/demo/mux/request.gno @@ -24,6 +24,10 @@ func (r *Request) GetVar(key string) string { case strings.HasPrefix(handlerPart, "{") && strings.HasSuffix(handlerPart, "}"): parameter := handlerPart[1 : len(handlerPart)-1] if parameter == key { + // check if this request has query strings or not + if strings.Contains(reqParts[i], "?") { + return strings.Split(reqParts[i], "?")[0] + } return reqParts[i] } default: diff --git a/examples/gno.land/p/demo/mux/request_test.gno b/examples/gno.land/p/demo/mux/request_test.gno index 5f8088b4964..3677193e91e 100644 --- a/examples/gno.land/p/demo/mux/request_test.gno +++ b/examples/gno.land/p/demo/mux/request_test.gno @@ -18,6 +18,9 @@ func TestRequest_GetVar(t *testing.T) { {"a/{b}/c/{d}", "a/42/c/1337", "b", "42"}, {"a/{b}/c/{d}", "a/42/c/1337", "d", "1337"}, {"{a}", "foo", "a", "foo"}, + {"/api/v1/{user}", "/api/v1/testuser?name=test", "user", "testuser"}, + {"/api/{v}/{user}/long/slash", "/api/v1/testuser?name=test/long/slash", "v", "v1"}, + {"/api/v1/{user}/long/slash", "/api/v1/testuser?name=test/long/slash", "user", "testuser"}, // TODO: wildcards: a/*/c // TODO: multiple patterns per slashes: a/{b}-{c}/d } diff --git a/examples/gno.land/p/demo/mux/router.gno b/examples/gno.land/p/demo/mux/router.gno index a2efb3a4ebf..1d46b91cc79 100644 --- a/examples/gno.land/p/demo/mux/router.gno +++ b/examples/gno.land/p/demo/mux/router.gno @@ -32,6 +32,9 @@ func (r *Router) Render(reqPath string) string { patPart := patParts[i] reqPart := reqParts[i] + if strings.HasPrefix(patPart, "?") { + continue + } if patPart == "*" { continue } diff --git a/examples/gno.land/p/demo/mux/router_test.gno b/examples/gno.land/p/demo/mux/router_test.gno index 13fd5b97955..1c636db895b 100644 --- a/examples/gno.land/p/demo/mux/router_test.gno +++ b/examples/gno.land/p/demo/mux/router_test.gno @@ -17,6 +17,29 @@ func TestRouter_Render(t *testing.T) { res.Write("Hi, earth!") }) + // handler func for query case + router.HandleFunc("hello/{user}/?", func(res *ResponseWriter, req *Request) { + name := req.GetVar("user") + queryPath := req.GetQueryPath() + urlQ := req.Query() + + if len(urlQ) != 0 && queryPath != "" { + queryNum := 0 + dataToWrite := "" + for key, values := range urlQ { + queryNum++ + valuesResult := "" + for _, vl := range values { + valuesResult += vl + "," + } + dataToWrite += " key: " + key + " and value: " + valuesResult + } + res.Write("Hello, " + name + " with path " + queryPath + " ===" + dataToWrite) + } else { + res.Write("Hello, world!") + } + }) + cases := []struct { path string expectedOutput string @@ -24,6 +47,9 @@ func TestRouter_Render(t *testing.T) { {"hello/Alice", "Hello, Alice!"}, {"hi", "Hi, earth!"}, {"hello/Bob", "Hello, Bob!"}, + {"hello/Bob/action?testaction=punch", "Hello, Bob with path action === key: testaction and value: punch,"}, + {"hello/Alice/finding?docs=gno&book=gnobook", "Hello, Alice with path finding === key: docs and value: gno, key: book and value: gnobook,"}, + {"hello/Celena/finding?color=red&color=blue", "Hello, Celena with path finding === key: color and value: red,blue,"}, // TODO: {"hello", "Hello, world!"}, // TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc }