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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

facade.gno

9.12 Kb · 278 lines
  1// untrusted-render: every string Render echoes is either a verb name validated
  2// by parseSchema against [a-z0-9_] at accept time, or a package path read off a
  3// crossing frame. No caller-typed payload is ever rendered.
  4//
  5// Package facade is the permanent entry point of the "API as data" upgrade
  6// pattern (pattern G of the exploration; see ../../README.md).
  7//
  8// Patterns E and F put a Go interface at the permanent path, which fixes the
  9// method set at deploy: adding an operation later needs a whole extra realm.
 10// This one puts a single entry point there instead,
 11//
 12//	Call(cur realm, verb, payload string) string
 13//
 14// and moves the API into DATA that each implementation declares. The signature
 15// that can never change is that one line; everything the application does can
 16// still grow.
 17//
 18// Three things fall out of the API being data, and they are the reason to pay
 19// the price below:
 20//
 21//  1. Callers can ENUMERATE it. Verbs, Signature and SchemaText answer without
 22//     a transaction, so a client discovers the API instead of being compiled
 23//     against it.
 24//  2. Payloads are CHECKED before the handler runs, so an arity mistake is one
 25//     abort with a readable message rather than whatever the handler does with
 26//     the wrong number of arguments.
 27//  3. Upgrades can be DIFFED. Accept refuses a handler whose schema would drop
 28//     or reshape a verb some existing caller depends on, which no amount of Go
 29//     interface satisfaction can catch: a handler is free to satisfy Handler
 30//     and answer nothing.
 31//
 32// The price is the type system. Arguments are strings a caller encodes, and a
 33// misspelled verb is an abort at runtime rather than a compile error. Pattern F
 34// is the other side of that trade and both ship here on purpose.
 35//
 36// State is deliberately out of scope. These handlers are pure; where an
 37// application's data should live is pattern C's question, and the answer does
 38// not change because the entry point became a string.
 39package facade
 40
 41import (
 42	"strings"
 43
 44	"gno.land/p/nt/avl/v0"
 45	"gno.land/p/nt/ownable/v0"
 46	"gno.land/p/nt/ufmt/v0"
 47)
 48
 49const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul
 50
 51const prefix = "gno.land/r/nym-moultest123/x/upgrade/schema/impl/"
 52
 53// sep is the payload separator. A single byte, because the point here is the
 54// shape of the boundary and not the encoding: a real one would need escaping,
 55// and choosing it is a decision this pattern does not make for you.
 56const sep = "|"
 57
 58// Handler is the whole interface an implementation satisfies. It never changes,
 59// because everything that would have changed it is in Schema instead.
 60type Handler interface {
 61	// Schema declares the API, one verb per line, "name arg1 arg2".
 62	Schema() string
 63	// Invoke runs a verb. The facade has already checked that the verb exists
 64	// and that args has exactly the declared arity.
 65	Invoke(verb string, args []string) string
 66}
 67
 68type verb struct {
 69	name   string
 70	params []string
 71}
 72
 73var (
 74	Ownable = ownable.NewWithAddress(owner)
 75
 76	candidates = avl.NewTree() // pkgpath -> Handler
 77	live       Handler
 78	livePath   string
 79	liveVerbs  = avl.NewTree() // verb name -> *verb, for lookup
 80	liveOrder  []string        // the same verbs in DECLARATION order, for listing
 81)
 82
 83// Propose nominates the calling realm, exactly as in pattern F. Its schema is
 84// parsed here so a malformed one is rejected at proposal rather than at accept.
 85func Propose(cur realm, h Handler) {
 86	caller := cur.Previous().PkgPath()
 87	if !strings.HasPrefix(caller, prefix) {
 88		panic("unauthorized: " + caller + " is not under " + prefix)
 89	}
 90	if h == nil {
 91		panic("handler must not be nil")
 92	}
 93	parseSchema(h.Schema()) // panics if malformed
 94	candidates.Set(caller, h)
 95}
 96
 97// Accept promotes a candidate, and refuses one that would break an existing
 98// caller. This is the check a Go interface cannot express.
 99func Accept(cur realm, pkgPath string) {
100	h, next, order := resolve(cur, pkgPath)
101	assertNoRegression(next)
102	live, livePath, liveVerbs, liveOrder = h, pkgPath, next, order
103}
104
105// AcceptBreaking promotes a candidate that Accept refuses.
106//
107// It exists because the diff is SYMMETRIC, which is not obvious until it bites:
108// once v1 has added a verb, rolling back to v0 drops that verb and is a
109// regression by exactly the same rule that protects callers going forward. A
110// pattern that can only move forward is worse than one with no diff at all, so
111// the escape hatch is required, and making it a separate function is the point:
112// the owner has to type a different word, and the audit log shows which one.
113//
114// Use it to roll back, and to retire a verb nobody calls any more. Not to make
115// an upgrade go through.
116func AcceptBreaking(cur realm, pkgPath string) {
117	h, next, order := resolve(cur, pkgPath)
118	live, livePath, liveVerbs, liveOrder = h, pkgPath, next, order
119}
120
121// resolve is the owner check and the lookup both accepts share.
122func resolve(cur realm, pkgPath string) (Handler, *avl.Tree, []string) {
123	Ownable.AssertOwnedBy(cur.Previous().Address())
124	v := candidates.Get(pkgPath)
125	if v == nil {
126		panic("no candidate at " + pkgPath)
127	}
128	h := v.(Handler)
129	t, order := parseSchema(h.Schema())
130	return h, t, order
131}
132
133// assertNoRegression is the upgrade diff. A new schema may ADD verbs and may not
134// remove one or change its arity, because a caller compiled against the old one
135// is still out there calling it.
136func assertNoRegression(next *avl.Tree) {
137	// Walk in DECLARATION order, not avl order, so the verb named in the abort
138	// is the first one a reader of the live schema would reach. Iterating the
139	// tree reports whichever violation happens to sort first, which makes the
140	// message depend on a verb's spelling.
141	for _, name := range liveOrder {
142		old := liveVerbs.Get(name).(*verb)
143		n := next.Get(name)
144		if n == nil {
145			panic("schema regression: the candidate drops verb " + name + ", which an existing caller may still call; AcceptBreaking overrides")
146		}
147		if len(n.(*verb).params) != len(old.params) {
148			panic("schema regression: the candidate changes the arity of verb " + name + "; AcceptBreaking overrides")
149		}
150	}
151}
152
153// Call is the one signature this realm is committed to forever.
154func Call(cur realm, verbName, payload string) string {
155	assertLive()
156	v := liveVerbs.Get(verbName)
157	if v == nil {
158		panic("unknown verb " + verbName + ", known: " + strings.Join(Verbs(), ", "))
159	}
160	spec := v.(*verb)
161	args := []string{}
162	if payload != "" {
163		args = strings.Split(payload, sep)
164	}
165	if len(args) != len(spec.params) {
166		panic(ufmt.Sprintf("verb %s takes %d argument(s), got %d, signature is %s",
167			verbName, len(spec.params), len(args), Signature(verbName)))
168	}
169	return live.Invoke(verbName, args)
170}
171
172// Verbs lists the accepted API in DECLARATION order, which is the order the
173// handler wrote it in and the order a reader of the schema expects. Iterating
174// the avl tree instead would list them alphabetically, silently: caught by a
175// test, not by a compiler.
176func Verbs() []string {
177	return liveOrder
178}
179
180// Signature is one verb's shape, as a caller would write it.
181func Signature(name string) string {
182	v := liveVerbs.Get(name)
183	if v == nil {
184		return ""
185	}
186	return name + "(" + strings.Join(v.(*verb).params, ", ") + ")"
187}
188
189// SchemaText is the whole accepted API in the declaration format, so a client
190// can read back exactly what the handler declared.
191func SchemaText() string {
192	out := ""
193	for _, n := range Verbs() {
194		v := liveVerbs.Get(n).(*verb)
195		out += n
196		for _, p := range v.params {
197			out += " " + p
198		}
199		out += "\n"
200	}
201	return out
202}
203
204// Live is the package path currently serving, or "" before the first Accept.
205func Live() string {
206	return livePath
207}
208
209// Candidates lists every path that has nominated itself, in order.
210func Candidates() []string {
211	out := []string{}
212	candidates.Iterate("", "", func(k string, _ any) bool {
213		out = append(out, k)
214		return false
215	})
216	return out
217}
218
219func assertLive() {
220	if live == nil {
221		panic("no handler accepted")
222	}
223}
224
225// parseSchema turns the declaration text into verbs, and is the only validation
226// of a name that Render later echoes.
227func parseSchema(text string) (*avl.Tree, []string) {
228	t := avl.NewTree()
229	order := []string{}
230	for _, line := range strings.Split(text, "\n") {
231		line = strings.TrimSpace(line)
232		if line == "" {
233			continue
234		}
235		fields := strings.Split(line, " ")
236		name := fields[0]
237		assertIdent(name)
238		params := []string{}
239		for _, f := range fields[1:] {
240			if f == "" {
241				continue
242			}
243			assertIdent(f)
244			params = append(params, f)
245		}
246		if t.Get(name) != nil {
247			panic("malformed schema: verb " + name + " declared twice")
248		}
249		t.Set(name, &verb{name: name, params: params})
250		order = append(order, name)
251	}
252	if t.Size() == 0 {
253		panic("malformed schema: no verbs declared")
254	}
255	return t, order
256}
257
258func assertIdent(s string) {
259	if s == "" {
260		panic("malformed schema: empty name")
261	}
262	for _, c := range s {
263		if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' {
264			panic("malformed schema: " + s + " is not [a-z0-9_]")
265		}
266	}
267}
268
269func Render(_ string) string {
270	if live == nil {
271		return ufmt.Sprintf("schema/facade/v0: no handler accepted (%d candidate(s))\n", candidates.Size())
272	}
273	out := ufmt.Sprintf("schema/facade/v0: %s\n", livePath)
274	for _, n := range Verbs() {
275		out += "- " + Signature(n) + "\n"
276	}
277	return out
278}