Skip to content

feat(commondao): add support for grouping members #4287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
77 changes: 77 additions & 0 deletions examples/gno.land/p/nt/commondao/member_group.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package commondao

import (
"errors"
"strings"
)

// MemberGroup defines an interface for a group of members.
type MemberGroup interface {
// Name returns the name of the group.
Name() string

// Members returns the members that belong to the group.
Members() MemberStorage

// SetMeta sets any metadata relevant to the group.
// Metadata can be used to store data which is specific to the group.
// Usually can be used to store parameter values which would be useful
// during proposal voting or tallying to resolve things like voting
// weights or rights for example.
SetMeta(any)

// GetMeta returns the group metadata.
GetMeta() any
}

// NewMemberGroup creates a new group of members.
func NewMemberGroup(name string, members MemberStorage) (MemberGroup, error) {
if members == nil {
return nil, errors.New("member storage is required")
}

name = strings.TrimSpace(name)
if name == "" {
return nil, errors.New("member group name is required")
}

return &memberGroup{
name: name,
members: members,
}, nil
}

// MustNewMemberGroup creates a new group of members or panics on error.
func MustNewMemberGroup(name string, members MemberStorage) MemberGroup {
g, err := NewMemberGroup(name, members)
if err != nil {
panic(err)
}
return g
}

type memberGroup struct {
name string
members MemberStorage
meta any
}

// Name returns the name of the group.
func (g memberGroup) Name() string {
return g.name
}

// Members returns the members that belong to the group.
func (g memberGroup) Members() MemberStorage {
return g.members
}

// SetMeta sets any metadata relevant to the group.
func (g *memberGroup) SetMeta(meta any) {
g.meta = meta
}

// GetMeta returns the group metadata.
func (g memberGroup) GetMeta() any {
return g.meta
}
37 changes: 37 additions & 0 deletions examples/gno.land/p/nt/commondao/member_group_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package commondao

import (
"testing"

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

func TestMemberGroupNew(t *testing.T) {
g, err := NewMemberGroup("", nil)
urequire.ErrorContains(t, err, "member storage is required")

storage := NewMemberStorage()
g, err = NewMemberGroup("", storage)
urequire.ErrorContains(t, err, "member group name is required")

name := "Tier 1"
g, err = NewMemberGroup(name, storage)
urequire.NoError(t, err, "expect no error")
uassert.Equal(t, name, g.Name(), "expect group name to match")
uassert.NotNil(t, g.Members(), "expect members to be not nil")
uassert.Nil(t, g.GetMeta(), "expect default group meta to be nil")
}

func TestMemberGroupMeta(t *testing.T) {
g, err := NewMemberGroup("Test", NewMemberStorage())
urequire.NoError(t, err, "expect no error")

g.SetMeta(42)
v := g.GetMeta()
urequire.NotEqual(t, nil, v, "expect metadata to be not nil")

meta, ok := v.(int)
urequire.True(t, ok, "expect meta type to be int")
uassert.Equal(t, 42, meta, "expect metadata to match")
}
86 changes: 86 additions & 0 deletions examples/gno.land/p/nt/commondao/member_grouping.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package commondao

import (
"errors"

"gno.land/p/demo/avl"
)

// MemberGrouping defines an interface for storing multiple member groups.
// Member grouping can be used by implementations that require grouping users
// by roles or by tiers for example.
type MemberGrouping interface {
// Size returns the number of groups that grouping contains.
Size() int

// Has checks if a group exists.
Has(name string) bool

// Add adds an new member group if it doesn't exists.
Add(name string) (MemberGroup, error)

// Get returns a member group.
Get(name string) (_ MemberGroup, found bool)

// Delete deletes a member group.
Delete(name string) error

// IterateByOffset iterates all member groups.
IterateByOffset(offset, count int, fn func(MemberGroup) bool)
}

// NewMemberGrouping creates a new members grouping.
func NewMemberGrouping() MemberGrouping {
return &memberGrouping{}
}

type memberGrouping struct {
groups avl.Tree // string(name) -> MemberGroup
}

// Size returns the number of groups that grouping contains.
func (g memberGrouping) Size() int {
return g.groups.Size()
}

// Has checks if a group exists.
func (g memberGrouping) Has(name string) bool {
return g.groups.Has(name)
}

// Add adds an new member group if it doesn't exists.
func (g *memberGrouping) Add(name string) (MemberGroup, error) {
if g.groups.Has(name) {
return nil, errors.New("member group already exists: " + name)
}

mg, err := NewMemberGroup(name, NewMemberStorage())
if err != nil {
return nil, err
}

g.groups.Set(name, mg)
return mg, nil
}

// Get returns a member group.
func (g memberGrouping) Get(name string) (_ MemberGroup, found bool) {
v, found := g.groups.Get(name)
if !found {
return nil, false
}
return v.(MemberGroup), true
}

// Delete deletes a member group.
func (g *memberGrouping) Delete(name string) error {
g.groups.Remove(name)
return nil
}

// IterateByOffset iterates all member groups.
func (g memberGrouping) IterateByOffset(offset, count int, fn func(MemberGroup) bool) {
g.groups.IterateByOffset(offset, count, func(_ string, v any) bool {
return fn(v.(MemberGroup))
})
}
93 changes: 93 additions & 0 deletions examples/gno.land/p/nt/commondao/member_grouping_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package commondao

import (
"testing"

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

func TestMemberGroupingAdd(t *testing.T) {
t.Run("defauls", func(t *testing.T) {
name := "Foo"
g := NewMemberGrouping()

uassert.False(t, g.Has(name), "expect grouping group not to be found")
uassert.Equal(t, 0, g.Size(), "expect grouping to be empty")
})

t.Run("success", func(t *testing.T) {
name := "Foo"
g := NewMemberGrouping()
mg, err := g.Add(name)

urequire.NoError(t, err, "expect no error")
uassert.True(t, g.Has(name), "expect grouping group to be found")
uassert.Equal(t, 1, g.Size(), "expect grouping to have a single group")

urequire.True(t, mg != nil, "expected grouping group to be not nil")
uassert.Equal(t, name, mg.Name(), "expect group to have the right name")
})

t.Run("duplicated name", func(t *testing.T) {
name := "Foo"
g := NewMemberGrouping()
_, err := g.Add(name)
urequire.NoError(t, err, "expect no error")

_, err = g.Add(name)
uassert.ErrorContains(t, err, "member group already exists: Foo", "expect duplication error")
})
}

func TestMemberGroupingGet(t *testing.T) {
t.Run("success", func(t *testing.T) {
name := "Foo"
g := NewMemberGrouping()
g.Add(name)

mg, found := g.Get(name)

urequire.True(t, found, "expect grouping group to be found")
urequire.True(t, mg != nil, "expect grouping group to be not nil")
uassert.Equal(t, name, mg.Name(), "expect group to have the right name")
})

t.Run("group not found", func(t *testing.T) {
g := NewMemberGrouping()

_, found := g.Get("Foo")

urequire.False(t, found, "expect grouping group to be not found")
})
}

func TestMemberGroupingDelete(t *testing.T) {
name := "Foo"
g := NewMemberGrouping()
g.Add(name)

err := g.Delete(name)

uassert.NoError(t, err, "expect no error")
urequire.False(t, g.Has(name), "expect grouping group not to be found")
}

func TestMemberGroupingIterate(t *testing.T) {
groups := []string{"Tier 1", "Tier 2", "Tier 3"}
g := NewMemberGrouping()
for _, name := range groups {
g.Add(name)
}

var i int
g.IterateByOffset(0, g.Size(), func(mg MemberGroup) bool {
urequire.True(t, mg != nil, "expect member group not to be nil")
urequire.Equal(t, groups[i], mg.Name(), "expect group to be iterated in order")

i++
return false
})

uassert.Equal(t, len(groups), i, "expect all groups to be iterated")
}
30 changes: 27 additions & 3 deletions examples/gno.land/p/nt/commondao/member_storage.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"gno.land/p/moul/addrset"
)

// TODO: Support member groups and metadata

// MemberStorage defines an interface for member storages.
type MemberStorage interface {
// Size returns the number of members in the storage.
Expand All @@ -24,12 +22,38 @@ type MemberStorage interface {
// Returns true if member was removed, or false if it was not found.
Remove(std.Address) bool

// Grouping returns member groups when supported.
// When nil is returned it means that grouping of members is not supported.
// Member groups can be used by implementations that require grouping users
// by roles or by tiers for example.
Grouping() MemberGrouping

// IterateByOffset iterates members starting at the given offset.
// The callback can return true to stop iteration.
IterateByOffset(offset, count int, fn func(std.Address) bool)
}

// NewMemberStorage creates a new member storage.
// Function returns a new member storage that doesn't support member groups.
// This type of storage is useful when there is no need to group members.
func NewMemberStorage() MemberStorage {
return &addrset.Set{}
return &memberStorage{}
}

// NewMemberStorageWithGrouping a new member storage with support for member groups.
// Member groups can be used by implementations that require grouping users by roles
// or by tiers for example.
func NewMemberStorageWithGrouping() MemberStorage {
return &memberStorage{grouping: NewMemberGrouping()}
}

type memberStorage struct {
addrset.Set

grouping MemberGrouping
}

// Grouping returns member groups.
func (s memberStorage) Grouping() MemberGrouping {
return s.grouping
}
Loading
Loading