From 6783d7f3e99ffdc73e441780835a77fa59a24293 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:03:50 +0100 Subject: [PATCH 1/4] feat: add p/moul/realmpath Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/realmpath/gno.mod | 1 + .../gno.land/p/moul/realmpath/realmpath.gno | 73 +++++++++++++++++++ .../p/moul/realmpath/realmpath_test.gno | 30 ++++++++ 3 files changed, 104 insertions(+) create mode 100644 examples/gno.land/p/moul/realmpath/gno.mod create mode 100644 examples/gno.land/p/moul/realmpath/realmpath.gno create mode 100644 examples/gno.land/p/moul/realmpath/realmpath_test.gno diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod new file mode 100644 index 00000000000..0c012a0c3ae --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/realmpath diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno new file mode 100644 index 00000000000..91daea7687c --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -0,0 +1,73 @@ +// Package realmpath provides utilities for parsing request paths and query +// parameters. It allows you to extract path segments and manipulate query +// values similar to net/url. +package realmpath + +import ( + "net/url" + "std" + "strings" +) + +const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) + +// Request represents a parsed request. +type Request struct { + Path string + Query url.Values + Realm string +} + +// Parse takes a raw path string and returns a Request object. +func Parse(rawPath string) *Request { + // Split the path and query string + path, query := splitPathAndQuery(rawPath) + + // Parse the query string into url.Values + queryValues, _ := url.ParseQuery(query) + + return &Request{ + Path: path, + Query: queryValues, + } +} + +// PathParts returns the segments of the path as a slice of strings. +func (r *Request) PathParts() []string { + return strings.Split(strings.Trim(r.Path, "/"), "/") +} + +// PathPart returns the specified part of the path. +func (r *Request) PathPart(index int) string { + parts := r.PathParts() + if index < 0 || index >= len(parts) { + return "" + } + return parts[index] +} + +// String rebuilds the URL from the path and query values. +func (r *Request) String() string { + if r.Realm == "" { + curPath := std.CurrentRealm().PkgPath() + r.Realm = strings.TrimPrefix(curPath, chainDomain) + } + + // Rebuild the path + reconstructedPath := r.Realm + ":" + strings.Join(r.PathParts(), "/") + + // Rebuild the query string + queryString := r.Query.Encode() + if queryString != "" { + return reconstructedPath + "?" + queryString + } + return reconstructedPath +} + +// splitPathAndQuery separates the path from the query string. +func splitPathAndQuery(rawPath string) (string, string) { + if idx := strings.Index(rawPath, "?"); idx != -1 { + return rawPath[:idx], rawPath[idx+1:] + } + return rawPath, "" +} diff --git a/examples/gno.land/p/moul/realmpath/realmpath_test.gno b/examples/gno.land/p/moul/realmpath/realmpath_test.gno new file mode 100644 index 00000000000..9c4326873f5 --- /dev/null +++ b/examples/gno.land/p/moul/realmpath/realmpath_test.gno @@ -0,0 +1,30 @@ +package realmpath_test + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" + "gno.land/p/moul/realmpath" +) + +func TestExample(t *testing.T) { + // initial parsing + path := "hello/world?foo=bar&baz=foobar" + req := realmpath.Parse(path) + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, "hello/world") + uassert.Equal(t, req.Query.Get("foo"), "bar") + uassert.Equal(t, req.Query.Get("baz"), "foobar") + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar") + + // alter query + req.Query.Set("hey", "salut") + uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar&hey=salut") + + // alter path + req.Path = "bye/ciao" + uassert.Equal(t, req.String(), "/r/lorem/ipsum:bye/ciao?baz=foobar&foo=bar&hey=salut") +} From 4ca103505554ff0c5c96183257830c20964f4327 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:16:50 +0100 Subject: [PATCH 2/4] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/realmpath/realmpath.gno | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno index 91daea7687c..3e1afb6b99e 100644 --- a/examples/gno.land/p/moul/realmpath/realmpath.gno +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -1,6 +1,4 @@ -// Package realmpath provides utilities for parsing request paths and query -// parameters. It allows you to extract path segments and manipulate query -// values similar to net/url. +// Package realmpath is a Lightweight Render.path parsing and link generation library with an idiomatic API, closely resembling that of net/url. package realmpath import ( From 38f1e51583d7c6cc33cdd1ec0ae466cc9ecb0161 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 15:19:52 +0100 Subject: [PATCH 3/4] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .../gno.land/p/moul/realmpath/realmpath.gno | 72 ++++++++++++++----- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno index 3e1afb6b99e..583d3ac8ef9 100644 --- a/examples/gno.land/p/moul/realmpath/realmpath.gno +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -1,4 +1,34 @@ -// Package realmpath is a Lightweight Render.path parsing and link generation library with an idiomatic API, closely resembling that of net/url. +// Package realmpath is a lightweight Render.path parsing and link generation +// library with an idiomatic API, closely resembling that of net/url. +// +// This package provides utilities for parsing request paths and query +// parameters, allowing you to extract path segments and manipulate query +// values. +// +// Example usage: +// +// import ( +// "fmt" +// "gno.land/p/moul/realmpath" +// ) +// +// func main() { +// // Parsing a sample path with query parameters +// path := "hello/world?foo=bar&baz=foobar" +// req := realmpath.Parse(path) +// +// // Accessing parsed path and query parameters +// fmt.Println(req.Path) // Output: hello/world +// fmt.Println(req.Query.Get("foo")) // Output: bar +// fmt.Println(req.Query.Get("baz")) // Output: foobar +// +// // Accessing path parts +// fmt.Println(req.PathPart(0)) // Output: hello +// fmt.Println(req.PathPart(1)) // Output: world +// +// // Rebuilding the URL +// fmt.Println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar +// } package realmpath import ( @@ -11,61 +41,65 @@ const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911) // Request represents a parsed request. type Request struct { - Path string - Query url.Values - Realm string + Path string // The path of the request + Query url.Values // The parsed query parameters + Realm string // The realm associated with the request } // Parse takes a raw path string and returns a Request object. +// It splits the path into its components and parses any query parameters. func Parse(rawPath string) *Request { - // Split the path and query string + // Split the raw path into path and query components path, query := splitPathAndQuery(rawPath) // Parse the query string into url.Values queryValues, _ := url.ParseQuery(query) return &Request{ - Path: path, - Query: queryValues, + Path: path, // Set the path + Query: queryValues, // Set the parsed query values } } // PathParts returns the segments of the path as a slice of strings. +// It trims leading and trailing slashes and splits the path by slashes. func (r *Request) PathParts() []string { return strings.Split(strings.Trim(r.Path, "/"), "/") } // PathPart returns the specified part of the path. +// If the index is out of bounds, it returns an empty string. func (r *Request) PathPart(index int) string { - parts := r.PathParts() + parts := r.PathParts() // Get the path segments if index < 0 || index >= len(parts) { - return "" + return "" // Return empty if index is out of bounds } - return parts[index] + return parts[index] // Return the specified path part } // String rebuilds the URL from the path and query values. +// If the Realm is not set, it automatically retrieves the current realm path. func (r *Request) String() string { + // Automatically set the Realm if it is not already defined if r.Realm == "" { - curPath := std.CurrentRealm().PkgPath() - r.Realm = strings.TrimPrefix(curPath, chainDomain) + curPath := std.CurrentRealm().PkgPath() // Get the current realm path + r.Realm = strings.TrimPrefix(curPath, chainDomain) // Trim the chain domain prefix } - // Rebuild the path + // Rebuild the path using the realm and path parts reconstructedPath := r.Realm + ":" + strings.Join(r.PathParts(), "/") // Rebuild the query string - queryString := r.Query.Encode() + queryString := r.Query.Encode() // Encode the query parameters if queryString != "" { - return reconstructedPath + "?" + queryString + return reconstructedPath + "?" + queryString // Return the full URL with query } - return reconstructedPath + return reconstructedPath // Return the path without query parameters } -// splitPathAndQuery separates the path from the query string. func splitPathAndQuery(rawPath string) (string, string) { if idx := strings.Index(rawPath, "?"); idx != -1 { - return rawPath[:idx], rawPath[idx+1:] + return rawPath[:idx], rawPath[idx+1:] // Split at the first '?' found } - return rawPath, "" + return rawPath, "" // No query string present } From e2f98e5f867ce5fd9212afd1d82134a494d0596b Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Fri, 8 Nov 2024 20:25:53 +0100 Subject: [PATCH 4/4] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/moul/realmpath/gno.mod | 5 + .../gno.land/p/moul/realmpath/realmpath.gno | 29 ++--- .../p/moul/realmpath/realmpath_test.gno | 123 +++++++++++++++++- 3 files changed, 139 insertions(+), 18 deletions(-) diff --git a/examples/gno.land/p/moul/realmpath/gno.mod b/examples/gno.land/p/moul/realmpath/gno.mod index 0c012a0c3ae..e391b76390f 100644 --- a/examples/gno.land/p/moul/realmpath/gno.mod +++ b/examples/gno.land/p/moul/realmpath/gno.mod @@ -1 +1,6 @@ module gno.land/p/moul/realmpath + +require ( + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/urequire v0.0.0-latest +) diff --git a/examples/gno.land/p/moul/realmpath/realmpath.gno b/examples/gno.land/p/moul/realmpath/realmpath.gno index 583d3ac8ef9..c46c97b4bed 100644 --- a/examples/gno.land/p/moul/realmpath/realmpath.gno +++ b/examples/gno.land/p/moul/realmpath/realmpath.gno @@ -7,27 +7,22 @@ // // Example usage: // -// import ( -// "fmt" -// "gno.land/p/moul/realmpath" -// ) +// import "gno.land/p/moul/realmpath" // -// func main() { +// func Render(path string) string { // // Parsing a sample path with query parameters -// path := "hello/world?foo=bar&baz=foobar" +// path = "hello/world?foo=bar&baz=foobar" // req := realmpath.Parse(path) // // // Accessing parsed path and query parameters -// fmt.Println(req.Path) // Output: hello/world -// fmt.Println(req.Query.Get("foo")) // Output: bar -// fmt.Println(req.Query.Get("baz")) // Output: foobar -// -// // Accessing path parts -// fmt.Println(req.PathPart(0)) // Output: hello -// fmt.Println(req.PathPart(1)) // Output: world +// println(req.Path) // Output: hello/world +// println(req.PathPart(0)) // Output: hello +// println(req.PathPart(1)) // Output: world +// println(req.Query.Get("foo")) // Output: bar +// println(req.Query.Get("baz")) // Output: foobar // // // Rebuilding the URL -// fmt.Println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar +// println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar // } package realmpath @@ -82,12 +77,12 @@ func (r *Request) PathPart(index int) string { func (r *Request) String() string { // Automatically set the Realm if it is not already defined if r.Realm == "" { - curPath := std.CurrentRealm().PkgPath() // Get the current realm path - r.Realm = strings.TrimPrefix(curPath, chainDomain) // Trim the chain domain prefix + r.Realm = std.CurrentRealm().PkgPath() // Get the current realm path } // Rebuild the path using the realm and path parts - reconstructedPath := r.Realm + ":" + strings.Join(r.PathParts(), "/") + relativePkgPath := strings.TrimPrefix(r.Realm, chainDomain) // Trim the chain domain prefix + reconstructedPath := relativePkgPath + ":" + strings.Join(r.PathParts(), "/") // Rebuild the query string queryString := r.Query.Encode() // Encode the query parameters diff --git a/examples/gno.land/p/moul/realmpath/realmpath_test.gno b/examples/gno.land/p/moul/realmpath/realmpath_test.gno index 9c4326873f5..a638b40d3ca 100644 --- a/examples/gno.land/p/moul/realmpath/realmpath_test.gno +++ b/examples/gno.land/p/moul/realmpath/realmpath_test.gno @@ -1,6 +1,7 @@ package realmpath_test import ( + "net/url" "std" "testing" @@ -10,6 +11,8 @@ import ( ) func TestExample(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + // initial parsing path := "hello/world?foo=bar&baz=foobar" req := realmpath.Parse(path) @@ -17,7 +20,6 @@ func TestExample(t *testing.T) { uassert.Equal(t, req.Path, "hello/world") uassert.Equal(t, req.Query.Get("foo"), "bar") uassert.Equal(t, req.Query.Get("baz"), "foobar") - std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) uassert.Equal(t, req.String(), "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar") // alter query @@ -28,3 +30,122 @@ func TestExample(t *testing.T) { req.Path = "bye/ciao" uassert.Equal(t, req.String(), "/r/lorem/ipsum:bye/ciao?baz=foobar&foo=bar&hey=salut") } + +func TestParse(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm("gno.land/r/lorem/ipsum")) + + tests := []struct { + rawPath string + realm string // optional + expectedPath string + expectedQuery url.Values + expectedString string + }{ + { + rawPath: "hello/world?foo=bar&baz=foobar", + expectedPath: "hello/world", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/lorem/ipsum:hello/world?baz=foobar&foo=bar", + }, + { + rawPath: "api/v1/resource?search=test&limit=10", + expectedPath: "api/v1/resource", + expectedQuery: url.Values{ + "search": []string{"test"}, + "limit": []string{"10"}, + }, + expectedString: "/r/lorem/ipsum:api/v1/resource?limit=10&search=test", + }, + { + rawPath: "singlepath", + expectedPath: "singlepath", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:singlepath", + }, + { + rawPath: "path/with/trailing/slash/", + expectedPath: "path/with/trailing/slash/", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:path/with/trailing/slash", + }, + { + rawPath: "emptyquery?", + expectedPath: "emptyquery", + expectedQuery: url.Values{}, + expectedString: "/r/lorem/ipsum:emptyquery", + }, + { + rawPath: "path/with/special/characters/?key=val%20ue&anotherKey=with%21special%23chars", + expectedPath: "path/with/special/characters/", + expectedQuery: url.Values{ + "key": []string{"val ue"}, + "anotherKey": []string{"with!special#chars"}, + }, + expectedString: "/r/lorem/ipsum:path/with/special/characters?anotherKey=with%21special%23chars&key=val+ue", + }, + { + rawPath: "path/with/empty/key?keyEmpty&=valueEmpty", + expectedPath: "path/with/empty/key", + expectedQuery: url.Values{ + "keyEmpty": []string{""}, + "": []string{"valueEmpty"}, + }, + expectedString: "/r/lorem/ipsum:path/with/empty/key?=valueEmpty&keyEmpty=", + }, + { + rawPath: "path/with/multiple/empty/keys?=empty1&=empty2", + expectedPath: "path/with/multiple/empty/keys", + expectedQuery: url.Values{ + "": []string{"empty1", "empty2"}, + }, + expectedString: "/r/lorem/ipsum:path/with/multiple/empty/keys?=empty1&=empty2", + }, + { + rawPath: "path/with/percent-encoded/%20space?query=hello%20world", + expectedPath: "path/with/percent-encoded/%20space", // XXX: should we decode? + expectedQuery: url.Values{ + "query": []string{"hello world"}, + }, + expectedString: "/r/lorem/ipsum:path/with/percent-encoded/%20space?query=hello+world", + }, + { + rawPath: "path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + expectedPath: "path/with/very/long/query", + expectedQuery: url.Values{ + "key1": []string{"value1"}, + "key2": []string{"value2"}, + "key3": []string{"value3"}, + "key4": []string{"value4"}, + "key5": []string{"value5"}, + "key6": []string{"value6"}, + }, + expectedString: "/r/lorem/ipsum:path/with/very/long/query?key1=value1&key2=value2&key3=value3&key4=value4&key5=value5&key6=value6", + }, + { + rawPath: "custom/realm?foo=bar&baz=foobar", + realm: "gno.land/r/foo/bar", + expectedPath: "custom/realm", + expectedQuery: url.Values{ + "foo": []string{"bar"}, + "baz": []string{"foobar"}, + }, + expectedString: "/r/foo/bar:custom/realm?baz=foobar&foo=bar", + }, + } + + for _, tt := range tests { + t.Run(tt.rawPath, func(t *testing.T) { + req := realmpath.Parse(tt.rawPath) + req.Realm = tt.realm // set optional realm + urequire.False(t, req == nil, "req should not be nil") + uassert.Equal(t, req.Path, tt.expectedPath) + urequire.Equal(t, len(req.Query), len(tt.expectedQuery)) + uassert.Equal(t, req.Query.Encode(), tt.expectedQuery.Encode()) + // XXX: uassert.Equal(t, req.Query, tt.expectedQuery) + uassert.Equal(t, req.String(), tt.expectedString) + }) + } +}