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: simplify boards2 implementation #3115

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3d0e592
feat: add `assertIsBoardName()` function
jeronimoalbi Nov 12, 2024
45c971a
feat: change ID types to have a `Key()` method
jeronimoalbi Nov 12, 2024
b078c3f
chore: add spacing and semantic changes
jeronimoalbi Nov 12, 2024
24546d2
feat: change public realm func to use asserts for anonymous fees
jeronimoalbi Nov 12, 2024
fca2bf5
feat: change public realm func to use asserts for non user calls
jeronimoalbi Nov 12, 2024
d066545
chore: rename permissions to match Go coding standards
jeronimoalbi Nov 12, 2024
83a9393
feat: change render to use a router
jeronimoalbi Nov 12, 2024
e95177f
chore: rename `Board.RenderBoard()` to `Board.Render()`
jeronimoalbi Nov 12, 2024
9b514aa
chore: rename `misc.gno` to `format.gno`
jeronimoalbi Nov 12, 2024
bfce60d
feat: add `newLink()` function to create Markdown links
jeronimoalbi Nov 12, 2024
5a84bdb
chore: rename `Post.RenderPost()` to `Post.Render()`
jeronimoalbi Nov 12, 2024
b3cb5db
feat: add `mustGetBoard()` function
jeronimoalbi Nov 12, 2024
3f1f4de
chore: semantic changes
jeronimoalbi Nov 12, 2024
b4d35bf
refactor: split `DeletePost` into `DeleteThread` & `DeleteReply`
jeronimoalbi Nov 13, 2024
318d3ef
refactor: split `EditPost` into `EditThread` & `EditReply`
jeronimoalbi Nov 13, 2024
c9b8a3a
feat: add `mustGetThread` & `mustGetReply` funcs
jeronimoalbi Nov 13, 2024
44a8a35
feat: add `assertUserHasPermission()` function
jeronimoalbi Nov 13, 2024
69b0bed
refactor: change `Post.GetReply` to also return "found" boolean
jeronimoalbi Nov 13, 2024
5e15250
refactor: split `Board.GetURLFromThreadAndReplyID()` in two methods
jeronimoalbi Nov 13, 2024
147945d
chore: rename boards state globals for consistency
jeronimoalbi Nov 13, 2024
8c02d71
refactor: change board URL to be dynamic
jeronimoalbi Nov 13, 2024
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
89 changes: 47 additions & 42 deletions examples/gno.land/r/demo/boards2/board.gno
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
package boards

import (
"regexp"
"std"
"strconv"
"strings"
"time"

"gno.land/p/demo/avl"
"gno.land/p/moul/txlink"
)

//----------------------------------------
// Board
var reBoardName = regexp.MustCompile(`^[a-z]{3}[_a-z0-9]{0,23}[0-9]{3}$`)

type BoardID uint64

func (bid BoardID) String() string {
return strconv.Itoa(int(bid))
func (id BoardID) String() string {
return strconv.Itoa(int(id))
}

func (id BoardID) Key() string {
return padZero(uint64(id), 10)
}

type Board struct {
id BoardID // only set for public boards.
url string
name string
creator std.Address
threads avl.Tree // Post.id -> *Post
Expand All @@ -29,17 +33,15 @@ type Board struct {
deleted avl.Tree // TODO reserved for fast-delete.
}

func newBoard(id BoardID, url string, name string, creator std.Address) *Board {
if !reName.MatchString(name) {
panic("invalid name: " + name)
}
exists := gBoardsByName.Has(name)
if exists {
func newBoard(id BoardID, name string, creator std.Address) *Board {
assertIsBoardName(name)

if gBoardsByName.Has(name) {
panic("board already exists")
}

return &Board{
id: id,
url: url,
name: name,
creator: creator,
threads: avl.Tree{},
Expand All @@ -62,39 +64,42 @@ func (board *Board) IsPrivate() bool {
return board.id == 0
}

func (board *Board) GetThread(pid PostID) *Post {
pidkey := postIDKey(pid)
postI, exists := board.threads.Get(pidkey)
if !exists {
return nil
// GetURL returns the relative URL of the board.
func (board *Board) GetURL() string {
return strings.TrimPrefix(std.CurrentRealm().PkgPath(), "gno.land") + ":" + board.name
}

func (board *Board) GetThread(threadID PostID) (_ *Post, found bool) {
v, found := board.threads.Get(threadID.Key())
if !found {
return nil, false
}
return postI.(*Post)
return v.(*Post), true
}

func (board *Board) AddThread(creator std.Address, title string, body string) *Post {
pid := board.incGetPostID()
pidkey := postIDKey(pid)
thread := newPost(board, pid, creator, title, body, pid, 0, 0)
board.threads.Set(pidkey, thread)
board.threads.Set(pid.Key(), thread)
return thread
}

// NOTE: this can be potentially very expensive for threads with many replies.
// TODO: implement optional fast-delete where thread is simply moved.
func (board *Board) DeleteThread(pid PostID) {
pidkey := postIDKey(pid)
_, removed := board.threads.Remove(pidkey)
_, removed := board.threads.Remove(pid.Key())
if !removed {
panic("thread does not exist with id " + pid.String())
}
}

// TODO: Change HasPermission to use a new authorization interface's `CanDo()`
func (board *Board) HasPermission(addr std.Address, perm Permission) bool {
if board.creator == addr {
switch perm {
case EditPermission:
case PermissionEdit:
return true
case DeletePermission:
case PermissionDelete:
return true
default:
return false
Expand All @@ -103,37 +108,37 @@ func (board *Board) HasPermission(addr std.Address, perm Permission) bool {
return false
}

// Renders the board for display suitable as plaintext in
// console. This is suitable for demonstration or tests,
// but not for prod.
func (board *Board) RenderBoard() string {
str := ""
str += "\\[[post](" + board.GetPostFormURL() + ")]\n\n"
func (board *Board) Render() string {
s := "\\[" + newLink("post", board.GetPostFormURL()) + "]\n\n"
if board.threads.Size() > 0 {
board.threads.Iterate("", "", func(key string, value interface{}) bool {
if str != "" {
str += "----------------------------------------\n"
}
str += value.(*Post).RenderSummary() + "\n"
board.threads.Iterate("", "", func(_ string, v interface{}) bool {
s += "----------------------------------------\n"
s += v.(*Post).RenderSummary() + "\n"
return false
})
}
return str
return s
}

func (board *Board) incGetPostID() PostID {
board.postsCtr++
return PostID(board.postsCtr)
}

func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string {
if replyID == 0 {
return board.url + "/" + threadID.String()
} else {
return board.url + "/" + threadID.String() + "/" + replyID.String()
}
func (board *Board) GetURLFromThreadID(threadID PostID) string {
return board.GetURL() + "/" + threadID.String()
}

func (board *Board) GetURLFromReplyID(threadID, replyID PostID) string {
return board.GetURL() + "/" + threadID.String() + "/" + replyID.String()
}

func (board *Board) GetPostFormURL() string {
return txlink.URL("CreateThread", "bid", board.id.String())
}

func assertIsBoardName(name string) {
if !reBoardName.MatchString(name) {
panic("invalid board name: " + name)
}
}
60 changes: 46 additions & 14 deletions examples/gno.land/r/demo/boards2/boards.gno
Original file line number Diff line number Diff line change
@@ -1,22 +1,54 @@
package boards

import (
"regexp"
import "gno.land/p/demo/avl"

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

//----------------------------------------
// Realm (package) state
// Default minimum fee in ugnot required for anonymous users
const defaultAnonymousFee = 100_000_000

var (
gBoards avl.Tree // id -> *Board
gBoardsCtr int // increments Board.id
gBoardsByName avl.Tree // name -> *Board
gDefaultAnonFee = 100000000 // minimum fee required if anonymous
gLastBoardID BoardID
gBoardsByID avl.Tree // string(id) -> *Board
gBoardsByName avl.Tree // string(name) -> *Board
)

//----------------------------------------
// Constants
// incGetBoardID returns a new board ID.
func incGetBoardID() BoardID {
gLastBoardID++
return gLastBoardID
}

// getBoard returns a board for a specific ID.
func getBoard(id BoardID) (_ *Board, found bool) {
v, exists := gBoardsByID.Get(id.Key())
if !exists {
return nil, false
}
return v.(*Board), true
}

// mustGetBoard returns a board or panics when it's not found.
func mustGetBoard(id BoardID) *Board {
board, found := getBoard(id)
if !found {
panic("board does not exist with ID: " + id.String())
}
return board
}

// mustGetThread returns a thread or panics when it's not found.
func mustGetThread(board *Board, threadID PostID) *Post {
thread, found := board.GetThread(threadID)
if !found {
panic("thread does not exist with ID: " + threadID.String())
}
return thread
}

var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{2,29}$`)
// mustGetReply returns a reply or panics when it's not found.
func mustGetReply(thread *Post, replyID PostID) *Post {
reply, found := thread.GetReply(replyID)
if !found {
panic("reply does not exist with ID: " + replyID.String())
}
return reply
}
65 changes: 65 additions & 0 deletions examples/gno.land/r/demo/boards2/format.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package boards

import (
"std"
"strconv"
"strings"

"gno.land/r/demo/users"
)

func padLeft(s string, length int) string {
if len(s) >= length {
return s
}
return strings.Repeat(" ", length-len(s)) + s
}

func padZero(u64 uint64, length int) string {
s := strconv.Itoa(int(u64))
if len(s) >= length {
return s
}
return strings.Repeat("0", length-len(s)) + s
}

func indentBody(indent string, body string) string {
var (
res string
lines = strings.Split(body, "\n")
)
for i, line := range lines {
if i > 0 {
res += "\n"
}
res += indent + line
}
return res
}

// NOTE: length must be greater than 3.
func summaryOf(text string, length int) string {
lines := strings.SplitN(text, "\n", 2)
line := lines[0]
if len(line) > length {
line = line[:(length-3)] + "..."
} else if len(lines) > 1 {
// len(line) <= 80
line = line + "..."
}
return line
}

// newLink returns a Markdown link.
func newLink(label, uri string) string {
return "[" + label + "](" + uri + ")"
}

// newUserLink returns a Markdown link for an account to the users realm.
func newUserLink(addr std.Address) string {
user := users.GetUserByAddress(addr)
if user == nil {
return newLink(addr.String(), "/r/demo/users:"+addr.String())
}
return newLink("@"+user.Name, "/r/demo/users:"+user.Name)
}
Loading
Loading