Skip to content

Commit

Permalink
Merge branch 'master' into dev/moul/r/sys/params
Browse files Browse the repository at this point in the history
  • Loading branch information
moul authored Nov 12, 2024
2 parents 6d99f19 + 5b64aa9 commit 15bacef
Show file tree
Hide file tree
Showing 15 changed files with 6,729 additions and 22 deletions.
6 changes: 6 additions & 0 deletions examples/gno.land/p/moul/realmpath/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +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
)
100 changes: 100 additions & 0 deletions examples/gno.land/p/moul/realmpath/realmpath.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// 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 "gno.land/p/moul/realmpath"
//
// func Render(path string) string {
// // Parsing a sample path with query parameters
// path = "hello/world?foo=bar&baz=foobar"
// req := realmpath.Parse(path)
//
// // Accessing parsed path and query parameters
// 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
// println(req.String()) // Output: /r/current/realm:hello/world?baz=foobar&foo=bar
// }
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 // 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 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, // 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() // Get the path segments
if index < 0 || index >= len(parts) {
return "" // Return empty if index is out of bounds
}
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 == "" {
r.Realm = std.CurrentRealm().PkgPath() // Get the current realm path
}

// Rebuild the path using the realm and path parts
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
if queryString != "" {
return reconstructedPath + "?" + queryString // Return the full URL with query
}
return reconstructedPath // Return the path without query parameters
}

func splitPathAndQuery(rawPath string) (string, string) {
if idx := strings.Index(rawPath, "?"); idx != -1 {
return rawPath[:idx], rawPath[idx+1:] // Split at the first '?' found
}
return rawPath, "" // No query string present
}
151 changes: 151 additions & 0 deletions examples/gno.land/p/moul/realmpath/realmpath_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package realmpath_test

import (
"net/url"
"std"
"testing"

"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
"gno.land/p/moul/realmpath"
)

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)
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")
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")
}

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)
})
}
}
27 changes: 14 additions & 13 deletions gnovm/cmd/gno/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"path/filepath"
"strings"

"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
"github.com/gnolang/gno/tm2/pkg/commands"
)
Expand Down Expand Up @@ -55,7 +54,7 @@ func (c *cleanCfg) RegisterFlags(fs *flag.FlagSet) {
&c.modCache,
"modcache",
false,
"remove the entire module download cache",
"remove the entire module download cache and exit",
)
}

Expand All @@ -64,6 +63,19 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error {
return flag.ErrHelp
}

if cfg.modCache {
modCacheDir := gnomod.ModCachePath()
if !cfg.dryRun {
if err := os.RemoveAll(modCacheDir); err != nil {
return err
}
}
if cfg.dryRun || cfg.verbose {
io.Println("rm -rf", modCacheDir)
}
return nil
}

path, err := os.Getwd()
if err != nil {
return err
Expand All @@ -81,17 +93,6 @@ func execClean(cfg *cleanCfg, args []string, io commands.IO) error {
return err
}

if cfg.modCache {
modCacheDir := filepath.Join(gnoenv.HomeDir(), "pkg", "mod")
if !cfg.dryRun {
if err := os.RemoveAll(modCacheDir); err != nil {
return err
}
}
if cfg.dryRun || cfg.verbose {
io.Println("rm -rf", modCacheDir)
}
}
return nil
}

Expand Down
11 changes: 11 additions & 0 deletions gnovm/cmd/gno/clean_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ func TestCleanApp(t *testing.T) {
testDir: "../../tests/integ/minimalist_gnomod",
simulateExternalRepo: true,
},
{
args: []string{"clean", "-modcache"},
testDir: "../../tests/integ/empty_dir",
simulateExternalRepo: true,
},
{
args: []string{"clean", "-modcache", "-n"},
testDir: "../../tests/integ/empty_dir",
simulateExternalRepo: true,
stdoutShouldContain: "rm -rf ",
},
}
testMainCaseRun(t, tc)

Expand Down
6 changes: 3 additions & 3 deletions gnovm/cmd/gno/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ func TestEnvApp(t *testing.T) {
{args: []string{"env", "foo"}, stdoutShouldBe: "\n"},
{args: []string{"env", "foo", "bar"}, stdoutShouldBe: "\n\n"},
{args: []string{"env", "GNOROOT"}, stdoutShouldBe: testGnoRootEnv + "\n"},
{args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n"},
{args: []string{"env", "GNOHOME", "storm"}, stdoutShouldBe: testGnoHomeEnv + "\n\n", noTmpGnohome: true},
{args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOROOT=%q", testGnoRootEnv)},
{args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv)},
{args: []string{"env"}, stdoutShouldContain: fmt.Sprintf("GNOHOME=%q", testGnoHomeEnv), noTmpGnohome: true},

// json
{args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOROOT\": %q", testGnoRootEnv)},
{args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv)},
{args: []string{"env", "-json"}, stdoutShouldContain: fmt.Sprintf("\"GNOHOME\": %q", testGnoHomeEnv), noTmpGnohome: true},
{
args: []string{"env", "-json", "GNOROOT"},
stdoutShouldBe: fmt.Sprintf("{\n\t\"GNOROOT\": %q\n}\n", testGnoRootEnv),
Expand Down
8 changes: 8 additions & 0 deletions gnovm/cmd/gno/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type testMainCase struct {
args []string
testDir string
simulateExternalRepo bool
noTmpGnohome bool

// for the following FooContain+FooBe expected couples, if both are empty,
// then the test suite will require that the "got" is not empty.
Expand Down Expand Up @@ -58,6 +59,13 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) {
mockOut := bytes.NewBufferString("")
mockErr := bytes.NewBufferString("")

if !test.noTmpGnohome {
tmpGnoHome, err := os.MkdirTemp(os.TempDir(), "gnotesthome_")
require.NoError(t, err)
t.Cleanup(func() { os.RemoveAll(tmpGnoHome) })
t.Setenv("GNOHOME", tmpGnoHome)
}

checkOutputs := func(t *testing.T) {
t.Helper()

Expand Down
2 changes: 1 addition & 1 deletion gnovm/cmd/gno/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func execModDownload(cfg *modDownloadCfg, args []string, io commands.IO) error {
}

// fetch dependencies
if err := gnoMod.FetchDeps(gnomod.GetGnoModPath(), cfg.remote, cfg.verbose); err != nil {
if err := gnoMod.FetchDeps(gnomod.ModCachePath(), cfg.remote, cfg.verbose); err != nil {
return fmt.Errorf("fetch: %w", err)
}

Expand Down
Loading

0 comments on commit 15bacef

Please sign in to comment.