Realm
gno.land/r/moul/x/upgrade/schema/facade/v0
Overview
Realm Path
gno.land/r/moul/x/upgrade/schema/facade/v0
Exported Functions
0
State Entries
11
Source Files
3
Total Package Entries
26
Exported Functions
No exported functions found.
State
11 state entries
Source Code
FILES
facade.gno
go
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/moul/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}
279Raw Package Data
Raw JSON data