Skip to content

Commit

Permalink
feat: r/profile dapp (gnolang#1983)
Browse files Browse the repository at this point in the history
Following this
[PR](gnolang#181 (comment))
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

<details><summary>Contributors' checklist...</summary>

- [ ] 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).
</details>

Closes gnolang#181

---------

Co-authored-by: kazai <kazai@777>
Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
  • Loading branch information
4 people authored and gfanton committed Jul 23, 2024
1 parent 18ef040 commit 4b9c94d
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 0 deletions.
9 changes: 9 additions & 0 deletions examples/gno.land/r/demo/profile/gno.mod
Original file line number Diff line number Diff line change
@@ -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
)
121 changes: 121 additions & 0 deletions examples/gno.land/r/demo/profile/profile.gno
Original file line number Diff line number Diff line change
@@ -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
}
118 changes: 118 additions & 0 deletions examples/gno.land/r/demo/profile/profile_test.gno
Original file line number Diff line number Diff line change
@@ -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)
}
102 changes: 102 additions & 0 deletions examples/gno.land/r/demo/profile/render.gno
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 4b9c94d

Please sign in to comment.