From 3ec27c46eb421cde22000041cf21eee7c9f96d07 Mon Sep 17 00:00:00 2001
From: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Date: Sat, 7 Dec 2024 23:21:32 +0100
Subject: [PATCH] chore(examples): update userbook example to use avl_pager
(#3251)
## Description
Updates the `r/demo/userbook` example to use the `avl_pager` package.
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
---------
Co-authored-by: Morgan
---
examples/gno.land/r/demo/userbook/render.gno | 51 ++++++
.../gno.land/r/demo/userbook/userbook.gno | 148 +++---------------
.../r/demo/userbook/userbook_test.gno | 79 ----------
3 files changed, 69 insertions(+), 209 deletions(-)
create mode 100644 examples/gno.land/r/demo/userbook/render.gno
delete mode 100644 examples/gno.land/r/demo/userbook/userbook_test.gno
diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno
new file mode 100644
index 00000000000..22d7f97eabd
--- /dev/null
+++ b/examples/gno.land/r/demo/userbook/render.gno
@@ -0,0 +1,51 @@
+// Package userbook demonstrates a small userbook system working with gnoweb
+package userbook
+
+import (
+ "sort"
+ "strconv"
+
+ "gno.land/p/demo/avl/pager"
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/moul/txlink"
+)
+
+func Render(path string) string {
+ p := pager.NewPager(signupsTree, 2)
+ page := p.MustGetPageByPath(path)
+
+ out := "# Welcome to UserBook!\n\n"
+
+ out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp"))
+ out += "---\n\n"
+
+ var sorted sortedSignups
+ for _, item := range page.Items {
+ sorted = append(sorted, item.Value.(*Signup))
+ }
+
+ sort.Sort(sorted)
+
+ for _, item := range sorted {
+ out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05"))
+ }
+
+ out += "---\n\n"
+ out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n"
+ out += page.Selector() // Repeat selector for ease of navigation
+ return out
+}
+
+type sortedSignups []*Signup
+
+func (s sortedSignups) Swap(i, j int) {
+ s[i], s[j] = s[j], s[i]
+}
+
+func (s sortedSignups) Len() int {
+ return len(s)
+}
+
+func (s sortedSignups) Less(i, j int) bool {
+ return s[i].timestamp.Before(s[j].timestamp)
+}
diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno
index c49bd90fa42..c958dc9e5b0 100644
--- a/examples/gno.land/r/demo/userbook/userbook.gno
+++ b/examples/gno.land/r/demo/userbook/userbook.gno
@@ -1,158 +1,46 @@
-// This realm demonstrates a small userbook system working with gnoweb
+// Package userbook demonstrates a small userbook system working with gnoweb
package userbook
import (
"std"
- "strconv"
+ "time"
"gno.land/p/demo/avl"
- "gno.land/p/demo/mux"
"gno.land/p/demo/ufmt"
)
type Signup struct {
- account string
- height int64
+ address std.Address
+ ordinal int
+ timestamp time.Time
}
-// signups - keep a slice of signed up addresses efficient pagination
-var signups []Signup
+var signupsTree = avl.NewTree()
-// tracker - keep track of who signed up
-var (
- tracker *avl.Tree
- router *mux.Router
-)
-
-const (
- defaultPageSize = 20
- pathArgument = "number"
- subPath = "page/{" + pathArgument + "}"
- signUpEvent = "SignUp"
-)
+const signUpEvent = "SignUp"
func init() {
- // Set up tracker tree
- tracker = avl.NewTree()
-
- // Set up route handling
- router = mux.NewRouter()
- router.HandleFunc("", renderHelper)
- router.HandleFunc(subPath, renderHelper)
-
- // Sign up the deployer
- SignUp()
+ SignUp() // Sign up the deployer
}
func SignUp() string {
// Get transaction caller
- caller := std.PrevRealm().Addr().String()
- height := std.GetHeight()
+ caller := std.PrevRealm().Addr()
// Check if the user is already signed up
- if _, exists := tracker.Get(caller); exists {
+ if _, exists := signupsTree.Get(caller.String()); exists {
panic(caller + " is already signed up!")
}
+ now := time.Now()
// Sign up the user
- tracker.Set(caller, struct{}{})
- signup := Signup{
- caller,
- height,
- }
-
- signups = append(signups, signup)
- std.Emit(signUpEvent, "SignedUpAccount", signup.account)
+ signupsTree.Set(caller.String(), &Signup{
+ std.PrevRealm().Addr(),
+ signupsTree.Size(),
+ now,
+ })
- return ufmt.Sprintf("%s added to userbook up at block #%d!", signup.account, signup.height)
-}
-
-func GetSignupsInRange(page, pageSize int) ([]Signup, int) {
- if page < 1 {
- panic("page number cannot be less than 1")
- }
-
- if pageSize < 1 || pageSize > 50 {
- panic("page size must be from 1 to 50")
- }
-
- // Pagination
- // Calculate indexes
- startIndex := (page - 1) * pageSize
- endIndex := startIndex + pageSize
-
- // If page does not contain any users
- if startIndex >= len(signups) {
- return nil, -1
- }
-
- // If page contains fewer users than the page size
- if endIndex > len(signups) {
- endIndex = len(signups)
- }
-
- return signups[startIndex:endIndex], endIndex
-}
-
-func renderHelper(res *mux.ResponseWriter, req *mux.Request) {
- totalSignups := len(signups)
- res.Write("# Welcome to UserBook!\n\n")
-
- // Get URL parameter
- page, err := strconv.Atoi(req.GetVar("number"))
- if err != nil {
- page = 1 // render first page on bad input
- }
-
- // Fetch paginated signups
- fetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize)
- // Handle empty page case
- if len(fetchedSignups) == 0 {
- res.Write("No users on this page!\n\n")
- res.Write("---\n\n")
- res.Write("[Back to Page #1](/r/demo/userbook:page/1)\n\n")
- return
- }
-
- // Write page title
- res.Write(ufmt.Sprintf("## UserBook - Page #%d:\n\n", page))
-
- // Write signups
- pageStartIndex := defaultPageSize * (page - 1)
- for i, signup := range fetchedSignups {
- out := ufmt.Sprintf("#### User #%d - %s - signed up at Block #%d\n", pageStartIndex+i, signup.account, signup.height)
- res.Write(out)
- }
-
- res.Write("---\n\n")
-
- // Write UserBook info
- latestSignupIndex := totalSignups - 1
- res.Write(ufmt.Sprintf("#### Total users: %d\n", totalSignups))
- res.Write(ufmt.Sprintf("#### Latest signup: User #%d at Block #%d\n", latestSignupIndex, signups[latestSignupIndex].height))
-
- res.Write("---\n\n")
-
- // Write page number
- res.Write(ufmt.Sprintf("You're viewing page #%d", page))
-
- // Write navigation buttons
- var prevPage string
- var nextPage string
- // If we are on any page that is not the first page
- if page > 1 {
- prevPage = ufmt.Sprintf(" - [Previous page](/r/demo/userbook:page/%d)", page-1)
- }
-
- // If there are more pages after the current one
- if endIndex < totalSignups {
- nextPage = ufmt.Sprintf(" - [Next page](/r/demo/userbook:page/%d)\n\n", page+1)
- }
-
- res.Write(prevPage)
- res.Write(nextPage)
-}
+ std.Emit(signUpEvent, "SignedUpAccount", caller.String())
-func Render(path string) string {
- return router.Render(path)
+ return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z))
}
diff --git a/examples/gno.land/r/demo/userbook/userbook_test.gno b/examples/gno.land/r/demo/userbook/userbook_test.gno
deleted file mode 100644
index 8d10d381e08..00000000000
--- a/examples/gno.land/r/demo/userbook/userbook_test.gno
+++ /dev/null
@@ -1,79 +0,0 @@
-package userbook
-
-import (
- "std"
- "strings"
- "testing"
-
- "gno.land/p/demo/testutils"
- "gno.land/p/demo/ufmt"
-)
-
-func TestRender(t *testing.T) {
- // Sign up 20 users + deployer
- for i := 0; i < 20; i++ {
- addrName := ufmt.Sprintf("test%d", i)
- caller := testutils.TestAddress(addrName)
- std.TestSetOrigCaller(caller)
- SignUp()
- }
-
- testCases := []struct {
- name string
- nextPage bool
- prevPage bool
- path string
- expectedNumberOfUsers int
- }{
- {
- name: "1st page render",
- nextPage: true,
- prevPage: false,
- path: "page/1",
- expectedNumberOfUsers: 20,
- },
- {
- name: "2nd page render",
- nextPage: false,
- prevPage: true,
- path: "page/2",
- expectedNumberOfUsers: 1,
- },
- {
- name: "Invalid path render",
- nextPage: true,
- prevPage: false,
- path: "page/invalidtext",
- expectedNumberOfUsers: 20,
- },
- {
- name: "Empty Page",
- nextPage: false,
- prevPage: false,
- path: "page/1000",
- expectedNumberOfUsers: 0,
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- got := Render(tc.path)
- numUsers := countUsers(got)
-
- if tc.prevPage && !strings.Contains(got, "Previous page") {
- t.Fatalf("expected to find Previous page, didn't find it")
- }
- if tc.nextPage && !strings.Contains(got, "Next page") {
- t.Fatalf("expected to find Next page, didn't find it")
- }
-
- if tc.expectedNumberOfUsers != numUsers {
- t.Fatalf("expected %d, got %d users", tc.expectedNumberOfUsers, numUsers)
- }
- })
- }
-}
-
-func countUsers(input string) int {
- return strings.Count(input, "#### User #")
-}