Skip to content
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

feat: add realm r/demo/keystore #958

Merged
merged 11 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions examples/gno.land/r/demo/keystore/keystore.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package keystore

import (
"std"
"strings"

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

var data avl.Tree

var (
BaseURL = "/r/demo/keystore"
StatusOK = "ok"
StatusNoUser = "user not found"
StatusNotFound = "key not found"
StatusNoWriteAccess = "no write access"
StatusCouldNotExecute = "could not execute"
)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be constants

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, fixed in 76b7131

func init() {
data = avl.Tree{} // user -> avl.Tree
}

// KeyStore stores the owner-specific avl.Tree
type KeyStore struct {
Owner std.Address
Data avl.Tree
}

// Set will set a value to a key
// requires write-access (original caller must be caller)
func Set(k, v string) string {
origOwner := std.GetOrigCaller()
return set(origOwner.String(), k, v)
}

// set (private) will set a key to value
// requires write-access (original caller must be caller)
func set(owner, k, v string) string {
origOwner := std.GetOrigCaller()
if origOwner.String() != owner {
return StatusNoWriteAccess
}
keystoreInterface, exists := data.Get(owner)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about extracting this out into a specific helper method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm of the mind to leave it - it only occurs twice in the code and moving out into a function may only reduce the code by a few lines.

if !exists {
data.Set(owner, &KeyStore{
Owner: origOwner,
Data: avl.Tree{},
})
keystoreInterface, _ = data.Get(owner)
}
keystore := keystoreInterface.(*KeyStore)
keystore.Data.Set(k, v)
return StatusOK
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you setting the keystore, and then fetching it again from the map (avl)?

Why not simply define a var keystore KeyStore outside the condition, and if it doesn't exist, assign it in the branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are totally right, thanks for that. Its fixed in 61079bb

}

// Remove removes a key
// requires write-access (original owner must be caller)
func Remove(k string) string {
origOwner := std.GetOrigCaller()
return remove(origOwner.String(), k)
}

// remove (private) removes a key
// requires write-access (original owner must be caller)
func remove(owner, k string) string {
origOwner := std.GetOrigCaller()
if origOwner.String() != owner {
return StatusNoWriteAccess
}
keystoreInterface, exists := data.Get(owner)
if !exists {
data.Set(owner, &KeyStore{
Owner: origOwner,
Data: avl.Tree{},
})
keystoreInterface, _ = data.Get(owner)
}
keystore := keystoreInterface.(*KeyStore)
_, removed := keystore.Data.Remove(k)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 61079bb

if !removed {
return StatusCouldNotExecute
}
return StatusOK
}

// Get returns a value for a key
// read-only
func Get(k string) string {
origOwner := std.GetOrigCaller()
return remove(origOwner.String(), k)
}

// get (private) returns a value for a key
// read-only
func get(owner, k string) string {
keystoreInterface, exists := data.Get(owner)
if !exists {
return StatusNoUser
}
keystore := keystoreInterface.(*KeyStore)
val, found := keystore.Data.Get(k)
if !found {
return StatusNotFound
}
return val.(string)
}

// Size returns size of database
// read-only
func Size() string {
origOwner := std.GetOrigCaller()
return size(origOwner.String())
}

func size(owner string) string {
keystoreInterface, exists := data.Get(owner)
if !exists {
return StatusNoUser
}
keystore := keystoreInterface.(*KeyStore)
return ufmt.Sprintf("%d", keystore.Data.Size())
}

// Render provides url access to the functions of the keystore
// "" -> show all keystores listed by owner
// "owner" -> show all keys for that owner's keystore
// "owner:size" -> returns size of owner's keystore
// "owner:get:key" -> show value for that key in owner's keystore
// "owner:set:key:value" -> sets a key-value pair for owner's keystore (owner must be caller)
// "owner:remove:key" -> removes key from owner keystore (owner must be caller)
func Render(p string) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not keep Render to be read-only?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, usage of Render() should probably be universally understood as read-only for consistency across realm examples though there isn't anything in the docs stating this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Was not aware that Render() should be read-only. Fixed in dd50037

var response string
args := strings.Split(p, ":")
numArgs := len(args)
if p == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a switch, instead? You have return logic for each branch, and only one branch can execute

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah! I'm not a huge fan of switches but I agree its nice here, fixed in 8854a13

if data.Size() > 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simplify this code segment by:

if data.Size() == 0 {
    return "no databases"
}

data.Iterate(...)
// ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I do prefer that pattern. Fixed in 292dc1e

data.Iterate("", "", func(key string, value interface{}) bool {
ks := value.(*KeyStore)
response += ufmt.Sprintf("- [%s](%s:%s) (%d keys)\n", ks.Owner, BaseURL, ks.Owner, ks.Data.Size())
return false
})
} else {
response = "no databases"
}
} else if numArgs == 1 {
owner := args[0]
keystoreInterface, exists := data.Get(owner)
if !exists {
return StatusNoUser
}
ks := keystoreInterface.(*KeyStore)
i := 0
response += ufmt.Sprintf("# %s database\n\n", ks.Owner)
ks.Data.Iterate("", "", func(key string, value interface{}) bool {
response += ufmt.Sprintf("- %d [%s](%s:%s:get:%s)\n", i, key, BaseURL, ks.Owner, key)
i++
return false
})
} else if numArgs == 2 {
owner := args[0]
cmd := args[1]
if cmd == "size" {
return size(owner)
}
} else if numArgs == 3 {
owner := args[0]
cmd := args[1]
key := args[2]
if cmd == "get" {
return get(owner, key)
} else if cmd == "remove" {
// remove will only work if caller is owner
return remove(owner, key)
}
} else if numArgs == 4 {
owner := args[0]
cmd := args[1]
key := args[2]
val := args[3]
if cmd == "set" {
// set only works if caller is owner
return set(owner, key, val)
}
}
return response
}
73 changes: 73 additions & 0 deletions examples/gno.land/r/demo/keystore/keystore_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package keystore

import (
"fmt"
"std"
"strings"
"testing"

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

func TestRender(t *testing.T) {
const (
author1 std.Address = testutils.TestAddress("author1")
author2 std.Address = testutils.TestAddress("author2")
)

tt := []struct {
caller std.Address
owner std.Address
ps []string
exp string
}{
// can set database if the owner is the caller
{author1, author1, []string{"set", "hello", "gno"}, StatusOK},
{author1, author1, []string{"size"}, "1"},
{author1, author1, []string{"set", "hello", "world"}, StatusOK},
{author1, author1, []string{"size"}, "1"},
{author1, author1, []string{"set", "hi", "gno"}, StatusOK},
{author1, author1, []string{"size"}, "2"},
// only owner can remove
{author1, author1, []string{"remove", "hi"}, StatusOK},
{author1, author1, []string{"get", "hi"}, StatusNotFound},
{author1, author1, []string{"size"}, "1"},
// add back
{author1, author1, []string{"set", "hi", "gno"}, StatusOK},
{author1, author1, []string{"size"}, "2"},

// different owner has different database
{author2, author2, []string{"set", "hello", "universe"}, StatusOK},
// either author can get the other info
{author1, author2, []string{"get", "hello"}, "universe"},
// either author can get the other info
{author2, author1, []string{"get", "hello"}, "world"},
// cannot perform write operations is owner (author2) is not caller (author1)
{author1, author2, []string{"set", "hello", "should not work"}, StatusNoWriteAccess},
{author1, author2, []string{"get", "hello"}, "universe"},
// anyone can view the databases
{author1, author2, []string{}, `- [g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6](/r/demo/keystore:g1v96hg6r0wgc47h6lta047h6lta047h6lm33tq6) (2 keys)
- [g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00) (1 keys)`},
// anyone can view the keys in a database
{author1, author2, []string{""}, `# g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00 database

- 0 [hello](/r/demo/keystore:g1v96hg6r0wge97h6lta047h6lta047h6lyz7c00:get:hello)`},
}
for _, tc := range tt {
p := ""
if len(tc.ps) > 0 {
p = tc.owner.String()
for i, psv := range tc.ps {
p += ":" + psv
}
}
p = strings.TrimSuffix(p, ":")
t.Run(p, func(t *testing.T) {
std.TestSetOrigCaller(tc.caller)
act := strings.TrimSpace(Render(p))
if act != tc.exp {
t.Errorf("%v -> '%s', got '%s', wanted '%s'", tc.ps, p, act, tc.exp)
}
})
}
}
Loading