forked from gnolang/gno
-
Notifications
You must be signed in to change notification settings - Fork 0
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 simple userbook realm (gnolang#1949)
<!-- 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 <[email protected]>
- Loading branch information
Showing
3 changed files
with
240 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,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 | ||
) |
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,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) | ||
} |
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,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 #") | ||
} |