Skip to content

Commit

Permalink
feat(examples): add simple userbook realm (gnolang#1949)
Browse files Browse the repository at this point in the history
<!-- please provide a detailed description of the changes made in this
pull request. -->

## Description

This PR arose out of a necessity to have a simple, permissionless realm
for demonstration purposes. The idea is that people can sign up once
with their address, and the Render function will paginate 50 addresses
per page, as well as include a UI to go to the next/previous page. This
realm will serve 3 purposes:
- Show how to do pagination
- Show how to use Render in harmony with `gnoweb`
- Exist as a simple entry point for new developers testing out the
chain. This realm will be used as an example in the Getting started
section in the docs.


<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>

---------

Co-authored-by: deelawn <dboltz03@gmail.com>
  • Loading branch information
leohhhn and deelawn committed Apr 26, 2024
1 parent 15ad779 commit f660be5
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 0 deletions.
8 changes: 8 additions & 0 deletions examples/gno.land/r/demo/userbook/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module gno.land/r/demo/userbook

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/ufmt v0.0.0-latest
)
153 changes: 153 additions & 0 deletions examples/gno.land/r/demo/userbook/userbook.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// This realm demonstrates a small userbook system working with gnoweb
package userbook

import (
"std"
"strconv"

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

type Signup struct {
account string
height int64
}

// signups - keep a slice of signed up addresses efficient pagination
var signups []Signup

// tracker - keep track of who signed up
var (
tracker *avl.Tree
router *mux.Router
)

const (
defaultPageSize = 20
pathArgument = "number"
subPath = "page/{" + pathArgument + "}"
)

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()
}

func SignUp() string {
// Get transaction caller
caller := std.PrevRealm().Addr().String()
height := std.GetHeight()

if _, exists := tracker.Get(caller); exists {
panic(caller + " is already signed up!")
}

tracker.Set(caller, struct{}{})
signup := Signup{
caller,
height,
}

signups = append(signups, signup)
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)
}

func Render(path string) string {
return router.Render(path)
}
79 changes: 79 additions & 0 deletions examples/gno.land/r/demo/userbook/userbook_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 #")
}

0 comments on commit f660be5

Please sign in to comment.