From 4b9c94db7353a51ed9a29349f077965051e08c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaza=C3=AF?= <149690535+kazai777@users.noreply.github.com> Date: Sun, 14 Jul 2024 23:09:14 +0200 Subject: [PATCH] feat: r/profile dapp (#1983) Following this [PR](https://github.com/gnolang/gno/pull/181#issuecomment-2061575269) concerning the creation of a realm `profile` I created this realm which allows the creation of profile as well as the associated functions to display the information of a profile with an address or a username. I have some questions concerning this realm: - Currently, if a user modifies his username, his old username is not freed and is therefore no longer available, even if it is no longer in use. Should I free the old username when the user has changed username? - To make it possible to search by username and address, I've created a second avl tree containing both username and address, so that I can find the profile indexed by its address by searching for it by its username. This is the most efficient solution I've found. I'd like to get some feedback on this and know if I should do things differently so that searching by username is more optimized. - Do you have any other suggestions for completing the profile fields, or other interesting features to add? Thanks in advance for your feedback
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
Closes #181 --------- Co-authored-by: kazai Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- examples/gno.land/r/demo/profile/gno.mod | 9 ++ examples/gno.land/r/demo/profile/profile.gno | 121 ++++++++++++++++++ .../gno.land/r/demo/profile/profile_test.gno | 118 +++++++++++++++++ examples/gno.land/r/demo/profile/render.gno | 102 +++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 examples/gno.land/r/demo/profile/gno.mod create mode 100644 examples/gno.land/r/demo/profile/profile.gno create mode 100644 examples/gno.land/r/demo/profile/profile_test.gno create mode 100644 examples/gno.land/r/demo/profile/render.gno diff --git a/examples/gno.land/r/demo/profile/gno.mod b/examples/gno.land/r/demo/profile/gno.mod new file mode 100644 index 00000000000..e7feac5d680 --- /dev/null +++ b/examples/gno.land/r/demo/profile/gno.mod @@ -0,0 +1,9 @@ +module gno.land/r/demo/profile + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/mux v0.0.0-latest + gno.land/p/demo/testutils v0.0.0-latest + gno.land/p/demo/uassert v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/profile/profile.gno b/examples/gno.land/r/demo/profile/profile.gno new file mode 100644 index 00000000000..cc7d80e016d --- /dev/null +++ b/examples/gno.land/r/demo/profile/profile.gno @@ -0,0 +1,121 @@ +package profile + +import ( + "errors" + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/mux" +) + +var ( + fields = avl.NewTree() + router = mux.NewRouter() +) + +const ( + DisplayName = "DisplayName" + Homepage = "Homepage" + Bio = "Bio" + Age = "Age" + Location = "Location" + Avatar = "Avatar" + GravatarEmail = "GravatarEmail" + AvailableForHiring = "AvailableForHiring" + InvalidField = "InvalidField" +) + +func init() { + router.HandleFunc("", homeHandler) + router.HandleFunc("u/{addr}", profileHandler) + router.HandleFunc("f/{addr}/{field}", fieldHandler) +} + +// list of supported string fields +var stringFields = map[string]bool{ + DisplayName: true, + Homepage: true, + Bio: true, + Location: true, + Avatar: true, + GravatarEmail: true, +} + +// list of support int fields +var intFields = map[string]bool{ + Age: true, +} + +// list of support bool fields +var boolFields = map[string]bool{ + AvailableForHiring: true, +} + +// Setters + +func SetStringField(field, value string) error { + addr := std.PrevRealm().Addr() + if _, ok := stringFields[field]; !ok { + return errors.New("invalid string field") + } + + key := addr.String() + ":" + field + fields.Set(key, value) + + return nil +} + +func SetIntField(field string, value int) error { + addr := std.PrevRealm().Addr() + + if _, ok := intFields[field]; !ok { + return errors.New("invalid int field") + } + + key := addr.String() + ":" + field + fields.Set(key, value) + + return nil +} + +func SetBoolField(field string, value bool) error { + addr := std.PrevRealm().Addr() + + if _, ok := boolFields[field]; !ok { + return errors.New("invalid bool field") + } + + key := addr.String() + ":" + field + fields.Set(key, value) + + return nil +} + +// Getters + +func GetStringField(addr std.Address, field, def string) string { + key := addr.String() + ":" + field + if value, ok := fields.Get(key); ok { + return value.(string) + } + + return def +} + +func GetBoolField(addr std.Address, field string, def bool) bool { + key := addr.String() + ":" + field + if value, ok := fields.Get(key); ok { + return value.(bool) + } + + return def +} + +func GetIntField(addr std.Address, field string, def int) int { + key := addr.String() + ":" + field + if value, ok := fields.Get(key); ok { + return value.(int) + } + + return def +} diff --git a/examples/gno.land/r/demo/profile/profile_test.gno b/examples/gno.land/r/demo/profile/profile_test.gno new file mode 100644 index 00000000000..987632a594d --- /dev/null +++ b/examples/gno.land/r/demo/profile/profile_test.gno @@ -0,0 +1,118 @@ +package profile + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +// Global addresses for test users +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + charlie = testutils.TestAddress("charlie") + dave = testutils.TestAddress("dave") + eve = testutils.TestAddress("eve") + frank = testutils.TestAddress("frank") + user1 = testutils.TestAddress("user1") + user2 = testutils.TestAddress("user2") +) + +func TestStringFields(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + // Get before setting + name := GetStringField(alice, DisplayName, "anon") + uassert.Equal(t, "anon", name) + + // Set + err := SetStringField(DisplayName, "Alice foo") + uassert.NoError(t, err) + err = SetStringField(Homepage, "https://example.com") + uassert.NoError(t, err) + + // Get after setting + name = GetStringField(alice, DisplayName, "anon") + homepage := GetStringField(alice, Homepage, "") + bio := GetStringField(alice, Bio, "42") + + uassert.Equal(t, "Alice foo", name) + uassert.Equal(t, "https://example.com", homepage) + uassert.Equal(t, "42", bio) +} + +func TestIntFields(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(bob)) + + // Get before setting + age := GetIntField(bob, Age, 25) + uassert.Equal(t, 25, age) + + // Set + err := SetIntField(Age, 30) + uassert.NoError(t, err) + + // Get after setting + age = GetIntField(bob, Age, 25) + uassert.Equal(t, 30, age) +} + +func TestBoolFields(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(charlie)) + + // Get before setting + hiring := GetBoolField(charlie, AvailableForHiring, false) + uassert.Equal(t, false, hiring) + + // Set + err := SetBoolField(AvailableForHiring, true) + uassert.NoError(t, err) + + // Get after setting + hiring = GetBoolField(charlie, AvailableForHiring, false) + uassert.Equal(t, true, hiring) +} + +func TestInvalidStringField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(dave)) + + err := SetStringField(InvalidField, "test") + uassert.Error(t, err) +} + +func TestInvalidIntField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(eve)) + + err := SetIntField(InvalidField, 123) + uassert.Error(t, err) +} + +func TestInvalidBoolField(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(frank)) + + err := SetBoolField(InvalidField, true) + uassert.Error(t, err) +} + +func TestMultipleProfiles(t *testing.T) { + // Set profile for user1 + std.TestSetRealm(std.NewUserRealm(user1)) + err := SetStringField(DisplayName, "User One") + uassert.NoError(t, err) + + // Set profile for user2 + std.TestSetRealm(std.NewUserRealm(user2)) + err = SetStringField(DisplayName, "User Two") + uassert.NoError(t, err) + + // Get profiles + std.TestSetRealm(std.NewUserRealm(user1)) // Switch back to user1 + name1 := GetStringField(user1, DisplayName, "anon") + std.TestSetRealm(std.NewUserRealm(user2)) // Switch back to user2 + name2 := GetStringField(user2, DisplayName, "anon") + + uassert.Equal(t, "User One", name1) + uassert.Equal(t, "User Two", name2) +} diff --git a/examples/gno.land/r/demo/profile/render.gno b/examples/gno.land/r/demo/profile/render.gno new file mode 100644 index 00000000000..4ff295e65eb --- /dev/null +++ b/examples/gno.land/r/demo/profile/render.gno @@ -0,0 +1,102 @@ +package profile + +import ( + "bytes" + "std" + + "gno.land/p/demo/mux" + "gno.land/p/demo/ufmt" +) + +const ( + BaseURL = "/r/demo/profile" + SetStringFieldURL = BaseURL + "?help&__func=SetStringField&field=%s" + SetIntFieldURL = BaseURL + "?help&__func=SetIntField&field=%s" + SetBoolFieldURL = BaseURL + "?help&__func=SetBoolField&field=%s" + ViewAllFieldsURL = BaseURL + ":u/%s" + ViewFieldURL = BaseURL + ":f/%s/%s" +) + +func homeHandler(res *mux.ResponseWriter, req *mux.Request) { + var b bytes.Buffer + + b.WriteString("## Setters\n") + for field := range stringFields { + link := ufmt.Sprintf(SetStringFieldURL, field) + b.WriteString(ufmt.Sprintf("- [Set %s](%s)\n", field, link)) + } + + for field := range intFields { + link := ufmt.Sprintf(SetIntFieldURL, field) + b.WriteString(ufmt.Sprintf("- [Set %s](%s)\n", field, link)) + } + + for field := range boolFields { + link := ufmt.Sprintf(SetBoolFieldURL, field) + b.WriteString(ufmt.Sprintf("- [Set %s Field](%s)\n", field, link)) + } + + b.WriteString("\n---\n\n") + + res.Write(b.String()) +} + +func profileHandler(res *mux.ResponseWriter, req *mux.Request) { + var b bytes.Buffer + addr := req.GetVar("addr") + + b.WriteString(ufmt.Sprintf("# Profile %s\n", addr)) + + address := std.Address(addr) + + for field := range stringFields { + value := GetStringField(address, field, "n/a") + link := ufmt.Sprintf(SetStringFieldURL, field) + b.WriteString(ufmt.Sprintf("- %s: %s [Edit](%s)\n", field, value, link)) + } + + for field := range intFields { + value := GetIntField(address, field, 0) + link := ufmt.Sprintf(SetIntFieldURL, field) + b.WriteString(ufmt.Sprintf("- %s: %d [Edit](%s)\n", field, value, link)) + } + + for field := range boolFields { + value := GetBoolField(address, field, false) + link := ufmt.Sprintf(SetBoolFieldURL, field) + b.WriteString(ufmt.Sprintf("- %s: %t [Edit](%s)\n", field, value, link)) + } + + res.Write(b.String()) +} + +func fieldHandler(res *mux.ResponseWriter, req *mux.Request) { + var b bytes.Buffer + addr := req.GetVar("addr") + field := req.GetVar("field") + + b.WriteString(ufmt.Sprintf("# Field %s for %s\n", field, addr)) + + address := std.Address(addr) + value := "n/a" + var editLink string + + if _, ok := stringFields[field]; ok { + value = ufmt.Sprintf("%s", GetStringField(address, field, "n/a")) + editLink = ufmt.Sprintf(SetStringFieldURL+"&addr=%s&value=%s", field, addr, value) + } else if _, ok := intFields[field]; ok { + value = ufmt.Sprintf("%d", GetIntField(address, field, 0)) + editLink = ufmt.Sprintf(SetIntFieldURL+"&addr=%s&value=%s", field, addr, value) + } else if _, ok := boolFields[field]; ok { + value = ufmt.Sprintf("%t", GetBoolField(address, field, false)) + editLink = ufmt.Sprintf(SetBoolFieldURL+"&addr=%s&value=%s", field, addr, value) + } + + b.WriteString(ufmt.Sprintf("- %s: %s [Edit](%s)\n", field, value, editLink)) + + res.Write(b.String()) +} + +func Render(path string) string { + return router.Render(path) +}