Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

devtools.gno

7.44 Kb · 203 lines
  1// Package devtools is the worked example of wiring moul's developer packages
  2// into one realm: what a new realm should copy.
  3//
  4// Everything here is a COMBINATION. Any one of these packages has its own demo
  5// showing it alone; the thing that is hard to find is what they look like used
  6// together, which is the only form a real realm ever meets them in. So this
  7// realm imports eight of them and every one earns its place in the output:
  8//
  9//	r/moul/config     the notice block, the pause switch, the explorer to link
 10//	p/moul/mygnoscan  the explorer routes themselves
 11//	p/moul/debug      a metadata panel behind ?debug=1
 12//	p/moul/realmpath  reading the query string Render was handed
 13//	p/moul/kit/ui     escaping what a caller typed, and the tables
 14//	p/moul/md         the markdown, without hand-rolled strings
 15//	p/moul/txlink     the "call this function" link
 16//	p/moul/fifo       the bounded note list, so the state cannot grow forever
 17//
 18// # The three lines worth copying
 19//
 20// A realm that does nothing else should still do these:
 21//
 22//	func Render(path string) string {
 23//		return config.TopBlockFor(realmPath) + body + config.BottomBlockFor(realmPath)
 24//	}
 25//
 26//	func Mutate(cur realm, s string) {
 27//		config.AssertWritableFor(realmPath)  // honours the pause switch
 28//		notes.Append(ui.Inline(s))           // escapes what a caller typed
 29//	}
 30//
 31// The first gives every realm a place to put a warning and a pause banner
 32// without shipping banner code. The second is two separate guards that both
 33// have to be there: the pause one so an incident can stop writes from outside
 34// the realm, and the escaping one because a string a caller typed is the
 35// oldest way to break a page.
 36package devtools
 37
 38import (
 39	"strconv"
 40	"strings"
 41
 42	"chain/runtime"
 43
 44	"gno.land/p/moul/debug/v0"
 45	"gno.land/p/moul/fifo/v0"
 46	"gno.land/p/moul/kit/ui/v0"
 47	"gno.land/p/moul/md/v0"
 48	"gno.land/p/moul/mygnoscan/v0"
 49	"gno.land/p/moul/realmpath/v0"
 50	"gno.land/p/moul/txlink/v0"
 51	"gno.land/r/moul/config/v1"
 52)
 53
 54// realmPath is this realm's own path.
 55//
 56// Passed explicitly to config rather than relying on its zero-argument helpers.
 57// Both work (a plain read is borrowed, so config sees this realm as the
 58// caller), but a constant cannot be wrong, and spelling it out is what a reader
 59// copying this file needs to see.
 60const realmPath = "gno.land/r/moul/x/allinone/devtools/v0"
 61
 62// maxNotes bounds the guest list. fifo drops the oldest rather than growing,
 63// because storage on chain is paid for and never refunded: an unbounded list in
 64// a demo anyone can write to is a bill with no ceiling.
 65const maxNotes = 10
 66
 67var notes = fifo.New(maxNotes)
 68
 69// Add appends a note. It is the only mutating function here, and it carries
 70// both guards a real one should.
 71func Add(cur realm, body string) {
 72	// 1. The pause switch. Set `pause@r/moul/x/allinone/devtools` on
 73	//    r/moul/config and this aborts, with the reason, from outside this
 74	//    realm and without a redeploy.
 75	config.AssertWritableFor(realmPath)
 76
 77	body = strings.TrimSpace(body)
 78	if body == "" {
 79		panic("empty note")
 80	}
 81	if len(body) > 200 {
 82		panic("note too long: max 200 bytes, got " + strconv.Itoa(len(body)))
 83	}
 84
 85	// 2. Escaping. body is a string a caller typed, and it is about to land in
 86	//    a markdown list. ui.Inline is the repo's standard for exactly this.
 87	notes.Append(ui.Inline(body))
 88}
 89
 90// Reset empties the list, so the realm can be put back to a known state
 91// without a redeploy. Not gated by the pause switch: clearing up is the thing
 92// you still want to do while paused.
 93func Reset(cur realm) {
 94	notes = fifo.New(maxNotes)
 95}
 96
 97// Render shows every package at work. The root view is deliberately free of
 98// anything that moves on its own, so an example test can pin it; the live
 99// numbers live under "chain".
100func Render(path string) string {
101	req := realmpath.Parse(path)
102
103	var b strings.Builder
104	b.WriteString(config.TopBlockFor(realmPath))
105	b.WriteString(md.H1("allinone: devtools"))
106	b.WriteString("\nEight packages, used together rather than one at a time. ")
107	b.WriteString(md.Link("source", config.MygnoscanFor(realmPath)))
108	b.WriteString("\n\n")
109
110	switch req.PathPart(0) {
111	case "chain":
112		b.WriteString(renderChain())
113	default:
114		b.WriteString(renderHome(path))
115	}
116
117	// debug prints its own panel only when ?debug=1 is set, and the empty
118	// string otherwise, so this line is safe to leave in a deployed realm.
119	b.WriteString(debug.Render(path))
120	b.WriteString(config.BottomBlockFor(realmPath))
121	return b.String()
122}
123
124func renderHome(path string) string {
125	var b strings.Builder
126
127	b.WriteString(md.H2("Notes"))
128	b.WriteString("\n")
129	// renderNotes returns no trailing newline of its own: md.BulletList and
130	// ui.Empty already end in one, and adding a second here produced two
131	// consecutive blank lines, which gno collapses inside an // Output: block
132	// and which therefore no example test can ever pin.
133	b.WriteString(renderNotes())
134	b.WriteString("\n")
135
136	// txlink builds the "call this function" URL for the CURRENT realm, so it
137	// keeps working after a version bump without anything here changing.
138	b.WriteString(md.BulletList([]string{
139		md.Link("add a note", txlink.NewLink("Add").AddArgs("body", "hello").URL()),
140		md.Link("clear them", txlink.Call("Reset")),
141		md.Link("live chain values", "/r/moul/x/allinone/devtools/v0:chain"),
142		md.Link("toggle the debug panel", debug.ToggleURL(path)),
143	}))
144	b.WriteString("\n")
145
146	b.WriteString(md.H2("What each package does here"))
147	b.WriteString("\n")
148	t := ui.NewTable("package", "what it contributed")
149	t.Row("`r/moul/config`", "the notice block above, the pause guard on `Add`, and which explorer to link")
150	t.Row("`p/moul/mygnoscan`", "the explorer routes, so a link survives a route change")
151	t.Row("`p/moul/debug`", "the `?debug=1` panel, empty otherwise")
152	t.Row("`p/moul/realmpath`", "splitting the render path into a view and a query")
153	t.Row("`p/moul/kit/ui`", "escaping a caller's note, and this table")
154	t.Row("`p/moul/md`", "every heading, link and list on the page")
155	t.Row("`p/moul/txlink`", "the call links above, relative to this realm")
156	t.Row("`p/moul/fifo`", "the note list, bounded so storage cannot grow forever")
157	b.WriteString(t.String())
158
159	return b.String()
160}
161
162// renderChain is everything that moves: the height, and the links built from
163// it. Kept off the root view so the root can be pinned by an example.
164func renderChain() string {
165	s := config.Scanner()
166	h := runtime.ChainHeight()
167
168	var b strings.Builder
169	b.WriteString(md.H2("Live"))
170	b.WriteString("\n")
171
172	t := ui.NewTable("what", "value")
173	t.Row("chain id", ui.Cell(runtime.ChainID()))
174	t.Row("height", strconv.FormatInt(h, 10))
175	t.Row("explorer", ui.Cell(s.Base()))
176	t.Row("network", ui.Cell(s.Network()))
177	b.WriteString(t.String())
178	b.WriteString("\n")
179
180	// mygnoscan.Link used directly, rather than through config, to show that
181	// the builder is an ordinary value: config only decides where it points.
182	b.WriteString(md.BulletList([]string{
183		mygnoscan.Link("this block", s.Block(h)),
184		mygnoscan.Link("this realm's transactions", s.Realm(realmPath, mygnoscan.TabCalls)),
185		mygnoscan.Link("its dependency graph", s.Realm(realmPath, mygnoscan.TabDeps)),
186		mygnoscan.Link("the parked deploy queue", s.Packages(mygnoscan.PackagesInert)),
187	}))
188	b.WriteString("\n")
189	b.WriteString(md.Link("back", "/r/moul/x/allinone/devtools/v0"))
190	b.WriteString("\n")
191	return b.String()
192}
193
194func renderNotes() string {
195	if notes.Size() == 0 {
196		return ui.Empty("no notes yet")
197	}
198	items := []string{}
199	for _, e := range notes.Entries() {
200		items = append(items, e.(string))
201	}
202	return md.BulletList(items)
203}