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: add r/demo/showcase #898

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 35 additions & 20 deletions examples/gno.land/p/demo/mux/router.gno
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,48 @@ func NewRouter() *Router {
}
}

func routingMatch(path, patterns []string) bool {
switch {
case len(patterns) == 0 && len(path) == 0: // good match
return true
case len(patterns) == 0, len(path) == 0: // bad match
return false
}

part := path[0]
pattern := patterns[0]

if pattern == "*" {
// Try future pattern to see if we have a match first
future := routingMatch(path[1:], patterns[1:])
if !future && len(path) > len(patterns) {
// Continue comparing the next element with a wildcard
return routingMatch(path[1:], patterns)
}
return future
}

if strings.HasPrefix(pattern, "{") && strings.HasSuffix(pattern, "}") {
// Continue matching if pattern is a placeholder (enclosed with {})
return routingMatch(path[1:], patterns[1:])
}

if part != pattern {
return false // Parts don't match, so the whole path doesn't match
}

// Continue matching the rest of the path and patterns
return routingMatch(path[1:], patterns[1:])
}

// Render renders the output for the given path using the registered route handler.
func (r *Router) Render(reqPath string) string {
reqParts := strings.Split(reqPath, "/")

for _, route := range r.routes {
patParts := strings.Split(route.Pattern, "/")
match := routingMatch(reqParts, patParts)

if len(patParts) != len(reqParts) {
continue
}

match := true
for i := 0; i < len(patParts); i++ {
patPart := patParts[i]
reqPart := reqParts[i]

if patPart == "*" {
continue
}
if strings.HasPrefix(patPart, "{") && strings.HasSuffix(patPart, "}") {
continue
}
if patPart != reqPart {
match = false
break
}
}
if match {
req := &Request{
Path: reqPath,
Expand Down
26 changes: 24 additions & 2 deletions examples/gno.land/p/demo/mux/router_test.gno
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
func TestRouter_Render(t *testing.T) {
// Define handlers and route configuration
router := NewRouter()
router.HandleFunc("hi", func(res *ResponseWriter, req *Request) {
res.Write("Hi, earth!")
})
router.HandleFunc("hello/{name}", func(res *ResponseWriter, req *Request) {
name := req.GetVar("name")
if name != "" {
Expand All @@ -16,19 +19,38 @@ func TestRouter_Render(t *testing.T) {
res.Write("Hello, world!")
}
})
router.HandleFunc("hi", func(res *ResponseWriter, req *Request) {
res.Write("Hi, earth!")
router.HandleFunc("{name}/{size}", func(res *ResponseWriter, req *Request) {
name := req.GetVar("name")
size := req.GetVar("size")
res.Write("my name is: " + name + " and my size is: " + size)
})

// wildcard handler
router.HandleFunc("burger/*/steak/avocado", func(res *ResponseWriter, req *Request) {
res.Write("My Best Burger have: " + strings.TrimPrefix(req.Path, "burger/"))
})
router.HandleFunc("burger/oignon/*", func(res *ResponseWriter, req *Request) {
res.Write("My Worst Burger have: " + strings.TrimPrefix(req.Path, "burger/"))
})

cases := []struct {
path string
expectedOutput string
}{
// comon test
{"hello/Alice", "Hello, Alice!"},
{"hi", "Hi, earth!"},
{"hello/Bob", "Hello, Bob!"},
// TODO: {"hello", "Hello, world!"},
// TODO: hello/, /hello, hello//Alice, hello/Alice/, hello/Alice/Bob, etc

// wildcard test
{"burger/salad/tomato/pickle/steak/avocado", "My Best Burger have: salad/tomato/pickle/steak/avocado"},
{"burger/salad/tomato/pickle/chips/avocado", "404"},
{"burger/salad/tomato/pickle/steak/chips", "404"},
{"burger/steak/avocado", "404"},
{"burger/oignon/avocado", "My Worst Burger have: oignon/avocado"},
{"burger/oignon/avocado/steak/tomato/salad", "My Worst Burger have: oignon/avocado/steak/tomato/salad"},
}
for _, tt := range cases {
t.Run(tt.path, func(t *testing.T) {
Expand Down
15 changes: 13 additions & 2 deletions examples/gno.land/p/demo/ui/ui.gno
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,27 @@ func (l Link) String(dom DOM) string {

type BulletList []DomStringer

func (bl BulletList) String(dom DOM) string {
func (bl BulletList) stringIndent(dom DOM, depth int) string {
output := ""

// indent by 2 spaces * depth
indent := strings.Repeat(" ", depth)
for _, entry := range bl {
output += "- " + entry.String(dom) + "\n"
switch e := entry.(type) {
case BulletList:
output += e.stringIndent(dom, depth+1)
default:
output += indent + "- " + e.String(dom) + "\n"
}
}

return output
}

func (bl BulletList) String(dom DOM) string {
return bl.stringIndent(dom, 0)
}

func Text(s string) DomStringer {
return Raw{Content: s}
}
Expand Down
10 changes: 10 additions & 0 deletions examples/gno.land/r/demo/showcase/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Package showcase provides a framework for creating small modules that showcase
numerous details and expose various usages.

The primary goal is to facilitate easy demonstration and exploration of small
features. Modules self-register, expose commands, and demonstrate different
aspects. This framework encourages experimentation and creativity, allowing
developers to easily add new modules while keeping the main files untouched.
*/
package showcase // import "gno.land/r/demo/showcase"
7 changes: 7 additions & 0 deletions examples/gno.land/r/demo/showcase/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module showcase

require (
"gno.land/p/demo/avl" v0.0.0-latest
"gno.land/p/demo/mux" v0.0.0-latest
"gno.land/p/demo/ui" v0.0.0-latest
)
15 changes: 15 additions & 0 deletions examples/gno.land/r/demo/showcase/mod_avl.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package showcase

func init() {
registerModule(&module{
name: "p/avl",
description: "Helper to store data in an avl.Tree",
commands: []command{
{name: "Store Example", callback: avlExample},
},
})
}

func avlExample(path string) string {
return "XXX: show how data is stored in an avl.Tree"
}
15 changes: 15 additions & 0 deletions examples/gno.land/r/demo/showcase/mod_bf.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package showcase

func init() {
registerModule(&module{
name: "p/bf",
description: "Helper to store data in an bf.Tree",
commands: []command{
{name: "Example", callback: bfExample},
},
})
}

func bfExample(path string) string {
return "XXX: show data has bf"
}
15 changes: 15 additions & 0 deletions examples/gno.land/r/demo/showcase/mod_svg.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package showcase

func init() {
registerModule(&module{
name: "p/svg",
description: "Helper to store data in an svg.Tree",
commands: []command{
{name: "Example", callback: svgExample},
},
})
}

func svgExample(path string) string {
return "XXX: show svg example"
}
174 changes: 174 additions & 0 deletions examples/gno.land/r/demo/showcase/showcase.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package showcase

import (
"strings"

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

const (
realmPath = "/r/demo/showcase:"
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
)

// cleanPath sanitizes the input path by replacing characters that are not in the base62Chars with '-'
func cleanPath(path string) string {
return replaceNotInCutset(path, base62Chars, '-')
}

type command struct {
name string
description string
callback func(path string) string
}

func (c *command) path() string { return cleanPath(c.name) }

type module struct {
name string
description string
commands []command
}

func (m *module) path() string { return cleanPath(m.name) }

var modules = avl.NewTree() // key: module_name(string), value: commands(module)

func registerModule(mod *module) {
modules.Set(mod.path(), mod)
}

func RenderCommand(res *mux.ResponseWriter, req *mux.Request) {
modulevar := cleanPath(req.GetVar("module"))
commandvar := cleanPath(req.GetVar("command"))

value, exist := modules.Get(modulevar)
if !exist {
res.Write("404: module not found")
return
}

module := value.(*module)

var cmd *command
for _, c := range module.commands {
if c.path() == commandvar {
cmd = &c
break
}
}

if cmd == nil {
res.Write("404: command not found")
return
}

fullpath := module.path() + "/" + cmd.path()
path := strings.TrimPrefix(req.Path, fullpath)
path = strings.TrimLeft(path, "/")

dom := ui.DOM{Prefix: realmPath + fullpath}
dom.Title = cmd.name
dom.Body.Append(ui.HR{}, ui.Raw{cmd.callback(path)})

dom.Footer.Append(ui.HR{}, ui.Link{Text: "[Back]", URL: realmPath + module.path()})

res.Write(dom.String())
return
}

func RenderModule(res *mux.ResponseWriter, req *mux.Request) {
modulevar := cleanPath(req.GetVar("module"))

value, exist := modules.Get(modulevar)
if !exist {
res.Write("404: module not found")
return
}

module := value.(*module)

dom := ui.DOM{Prefix: realmPath + module.path() + "/"}
dom.Title = module.name

if module.description != "" {
dom.Body.Append(ui.Paragraph(module.description))
}

items := ui.BulletList{}
for _, command := range module.commands {
items = append(items, ui.Link{Text: command.name, Path: command.path()})
}

// add commands list items
dom.Body.Append(items)

// add back boutton
dom.Footer.Append(ui.HR{}, ui.Link{Text: "[Back]", URL: realmPath})

// write to response
res.Write(dom.String())
return
}

func RenderHome(res *mux.ResponseWriter, req *mux.Request) {
dom := ui.DOM{Prefix: realmPath}
dom.Title = "Showcase"

dom.Body.Append(
ui.Paragraph("Package showcase provides a framework for creating small modules that showcase numerous details and expose various usages."),
ui.Paragraph(`
The primary goal is to facilitate easy demonstration and exploration of small features.
Modules self-register, expose commands, and demonstrate different aspects.
This framework encourages experimentation and creativity, allowing developers to easily add new modules while keeping the main files untouched.`,
))

items := ui.BulletList{}
modules.Iterate("", "", func(key string, value interface{}) bool {
module := value.(*module)
items = append(items, ui.Link{Text: module.name, Path: module.path()})

// add subitems to menu
subitems := ui.BulletList{}
for _, command := range module.commands {
fullPath := module.path() + "/" + command.path()
subitems = append(subitems, ui.Link{Text: command.name, Path: fullPath})
}
items = append(items, subitems)
return false
})

dom.Body.Append(items)

// write to response
res.Write(dom.String())
return
}

func Render(path string) string {
router := mux.NewRouter()
router.HandleFunc("", RenderHome)

// handle module
router.HandleFunc("{module}", RenderModule)

// handle module command and following path
router.HandleFunc("{module}/{command}", RenderCommand)
router.HandleFunc("{module}/{command}/*", RenderCommand)

return router.Render(path)
}

func replaceNotInCutset(s, cutset string, repl rune) string {
var result []rune
for _, c := range s {
if !strings.ContainsRune(cutset, c) {
result = append(result, repl)
} else {
result = append(result, c)
}
}
return string(result)
}
Loading