-
Notifications
You must be signed in to change notification settings - Fork 379
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add
r/morgan/{home,guestbook}
(#2345)
I wanted a simple realm that allows any user to interact with it and see something happen. This is a guestbook realm; it allows anyone to leave a small message, which will be associated with their address. It also is a nice showcase of how to effectively do pagination with AVL trees. I don't think we have many such examples so far.
- Loading branch information
Showing
6 changed files
with
300 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package guestbook | ||
|
||
import ( | ||
"gno.land/p/demo/ownable" | ||
"gno.land/p/demo/seqid" | ||
) | ||
|
||
var owner = ownable.New() | ||
|
||
// AdminDelete removes the guestbook message with the given ID. | ||
// The user will still be marked as having submitted a message, so they | ||
// won't be able to re-submit a new message. | ||
func AdminDelete(signatureID string) { | ||
owner.AssertCallerIsOwner() | ||
|
||
id, err := seqid.FromString(signatureID) | ||
if err != nil { | ||
panic(err) | ||
} | ||
idb := id.Binary() | ||
if !guestbook.Has(idb) { | ||
panic("signature does not exist") | ||
} | ||
guestbook.Remove(idb) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
module gno.land/r/morgan/guestbook | ||
|
||
require ( | ||
gno.land/p/demo/avl v0.0.0-latest | ||
gno.land/p/demo/ownable v0.0.0-latest | ||
gno.land/p/demo/seqid v0.0.0-latest | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// Realm guestbook contains an implementation of a simple guestbook. | ||
// Come and sign yourself up! | ||
package guestbook | ||
|
||
import ( | ||
"std" | ||
"strconv" | ||
"strings" | ||
"time" | ||
"unicode" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/seqid" | ||
) | ||
|
||
// Signature is a single entry in the guestbook. | ||
type Signature struct { | ||
Message string | ||
Author std.Address | ||
Time time.Time | ||
} | ||
|
||
const ( | ||
maxMessageLength = 140 | ||
maxPerPage = 25 | ||
) | ||
|
||
var ( | ||
signatureID seqid.ID | ||
guestbook avl.Tree // id -> Signature | ||
hasSigned avl.Tree // address -> struct{} | ||
) | ||
|
||
func init() { | ||
Sign("You reached the end of the guestbook!") | ||
} | ||
|
||
const ( | ||
errNotAUser = "this guestbook can only be signed by users" | ||
errAlreadySigned = "you already signed the guestbook!" | ||
errInvalidCharacterInMessage = "invalid character in message" | ||
) | ||
|
||
// Sign signs the guestbook, with the specified message. | ||
func Sign(message string) { | ||
prev := std.PrevRealm() | ||
switch { | ||
case !prev.IsUser(): | ||
panic(errNotAUser) | ||
case hasSigned.Has(prev.Addr().String()): | ||
panic(errAlreadySigned) | ||
} | ||
message = validateMessage(message) | ||
|
||
guestbook.Set(signatureID.Next().Binary(), Signature{ | ||
Message: message, | ||
Author: prev.Addr(), | ||
// NOTE: time.Now() will yield the "block time", which is deterministic. | ||
Time: time.Now(), | ||
}) | ||
hasSigned.Set(prev.Addr().String(), struct{}{}) | ||
} | ||
|
||
func validateMessage(msg string) string { | ||
if len(msg) > maxMessageLength { | ||
panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)") | ||
} | ||
out := "" | ||
for _, ch := range msg { | ||
switch { | ||
case unicode.IsLetter(ch), | ||
unicode.IsNumber(ch), | ||
unicode.IsSpace(ch), | ||
unicode.IsPunct(ch): | ||
out += string(ch) | ||
default: | ||
panic(errInvalidCharacterInMessage) | ||
} | ||
} | ||
return out | ||
} | ||
|
||
func Render(maxID string) string { | ||
var bld strings.Builder | ||
|
||
bld.WriteString("# Guestbook 📝\n\n[Come sign the guestbook!](./guestbook?help&__func=Sign)\n\n---\n\n") | ||
|
||
var maxIDBinary string | ||
if maxID != "" { | ||
mid, err := seqid.FromString(maxID) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
// AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum. | ||
mid-- | ||
maxIDBinary = mid.Binary() | ||
} | ||
|
||
var lastID seqid.ID | ||
var printed int | ||
guestbook.ReverseIterate("", maxIDBinary, func(key string, val interface{}) bool { | ||
sig := val.(Signature) | ||
message := strings.ReplaceAll(sig.Message, "\n", "\n> ") | ||
bld.WriteString("> " + message + "\n>\n") | ||
idValue, ok := seqid.FromBinary(key) | ||
if !ok { | ||
panic("invalid seqid id") | ||
} | ||
|
||
bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n") | ||
lastID = idValue | ||
|
||
printed++ | ||
// stop after exceeding limit | ||
return printed >= maxPerPage | ||
}) | ||
|
||
if printed == 0 { | ||
bld.WriteString("No messages!") | ||
} else if printed >= maxPerPage { | ||
bld.WriteString("<p style='text-align:right'><a href='./guestbook:" + lastID.String() + "'>Next page</a></p>") | ||
} | ||
|
||
return bld.String() | ||
} |
131 changes: 131 additions & 0 deletions
131
examples/gno.land/r/morgan/guestbook/guestbook_test.gno
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package guestbook | ||
|
||
import ( | ||
"std" | ||
"strings" | ||
"testing" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/ownable" | ||
) | ||
|
||
func TestSign(t *testing.T) { | ||
guestbook = avl.Tree{} | ||
hasSigned = avl.Tree{} | ||
|
||
std.TestSetRealm(std.NewUserRealm("g1user")) | ||
Sign("Hello!") | ||
|
||
std.TestSetRealm(std.NewUserRealm("g1user2")) | ||
Sign("Hello2!") | ||
|
||
res := Render("") | ||
t.Log(res) | ||
if !strings.Contains(res, "> Hello!\n>\n> _Written by g1user ") { | ||
t.Error("does not contain first user's message") | ||
} | ||
if !strings.Contains(res, "> Hello2!\n>\n> _Written by g1user2 ") { | ||
t.Error("does not contain second user's message") | ||
} | ||
if guestbook.Size() != 2 { | ||
t.Error("invalid guestbook size") | ||
} | ||
} | ||
|
||
func TestSign_FromRealm(t *testing.T) { | ||
std.TestSetRealm(std.NewCodeRealm("gno.land/r/demo/users")) | ||
|
||
defer func() { | ||
rec := recover() | ||
if rec == nil { | ||
t.Fatal("expected panic") | ||
} | ||
recString, ok := rec.(string) | ||
if !ok { | ||
t.Fatal("not a string", rec) | ||
} else if recString != errNotAUser { | ||
t.Fatal("invalid error", recString) | ||
} | ||
}() | ||
Sign("Hey!") | ||
} | ||
|
||
func TestSign_Double(t *testing.T) { | ||
// Should not allow signing twice. | ||
guestbook = avl.Tree{} | ||
hasSigned = avl.Tree{} | ||
|
||
std.TestSetRealm(std.NewUserRealm("g1user")) | ||
Sign("Hello!") | ||
|
||
defer func() { | ||
rec := recover() | ||
if rec == nil { | ||
t.Fatal("expected panic") | ||
} | ||
recString, ok := rec.(string) | ||
if !ok { | ||
t.Error("type assertion failed", rec) | ||
} else if recString != errAlreadySigned { | ||
t.Error("invalid error message", recString) | ||
} | ||
}() | ||
|
||
Sign("Hello again!") | ||
} | ||
|
||
func TestSign_InvalidMessage(t *testing.T) { | ||
// Should not allow control characters in message. | ||
guestbook = avl.Tree{} | ||
hasSigned = avl.Tree{} | ||
|
||
std.TestSetRealm(std.NewUserRealm("g1user")) | ||
|
||
defer func() { | ||
rec := recover() | ||
if rec == nil { | ||
t.Fatal("expected panic") | ||
} | ||
recString, ok := rec.(string) | ||
if !ok { | ||
t.Error("type assertion failed", rec) | ||
} else if recString != errInvalidCharacterInMessage { | ||
t.Error("invalid error message", recString) | ||
} | ||
}() | ||
Sign("\x00Hello!") | ||
} | ||
|
||
func TestAdminDelete(t *testing.T) { | ||
const ( | ||
userAddr std.Address = "g1user" | ||
adminAddr std.Address = "g1admin" | ||
) | ||
|
||
guestbook = avl.Tree{} | ||
hasSigned = avl.Tree{} | ||
owner = ownable.NewWithAddress(adminAddr) | ||
signatureID = 0 | ||
|
||
std.TestSetRealm(std.NewUserRealm(userAddr)) | ||
|
||
const bad = "Very Bad Message! Nyeh heh heh!" | ||
Sign(bad) | ||
|
||
if rnd := Render(""); !strings.Contains(rnd, bad) { | ||
t.Fatal("render does not contain bad message", rnd) | ||
} | ||
|
||
std.TestSetRealm(std.NewUserRealm(adminAddr)) | ||
AdminDelete(signatureID.String()) | ||
|
||
if rnd := Render(""); strings.Contains(rnd, bad) { | ||
t.Error("render contains bad message", rnd) | ||
} | ||
if guestbook.Size() != 0 { | ||
t.Error("invalid guestbook size") | ||
} | ||
if hasSigned.Size() != 1 { | ||
t.Error("invalid hasSigned size") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/r/morgan/home |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package home | ||
|
||
const staticHome = `# morgan's (gn)home | ||
- [📝 sign my guestbook](/r/morgan/guestbook) | ||
` | ||
|
||
func Render(path string) string { | ||
return staticHome | ||
} |