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