schema.gno
9.88 Kb · 372 lines
1package app
2
3import (
4 "errors"
5 "strconv"
6 "strings"
7)
8
9// A schema is what a handler says about itself: the verbs it answers, what
10// each one takes, and what it gives back. Handlers declare one; the app reads
11// it to enumerate the API, to validate payloads before dispatch, and to refuse
12// an upgrade that would break a caller.
13//
14// This is the part a verb-dispatch API is usually missing. Without it, "the
15// API" is whatever the live handler's switch statement happens to accept: not
16// discoverable, not checkable, and free to change under callers without
17// anything noticing. A schema makes it data, which is the same move that makes
18// the API growable in the first place.
19//
20// What it is not is static typing. Gno has no reflection, so nothing here can
21// look at your Go types. Validation happens at call time against declarations
22// you wrote by hand, and a handler whose switch disagrees with its own schema
23// is a bug the schema cannot catch.
24
25// Kind is the type of one payload field. The set is deliberately small: these
26// are the types that survive a string encoding without ambiguity.
27type Kind string
28
29const (
30 KindString Kind = "string"
31 KindInt Kind = "int" // int64, base 10
32 KindBool Kind = "bool" // "true" or "false"
33 KindAddress Kind = "address" // bech32, validated
34 KindNone Kind = "none" // a verb that returns nothing meaningful
35)
36
37// Valid reports whether k is a known kind.
38func (k Kind) Valid() bool {
39 switch k {
40 case KindString, KindInt, KindBool, KindAddress, KindNone:
41 return true
42 }
43 return false
44}
45
46// parse checks that v is a well-formed value of kind k.
47func (k Kind) parse(v string) error {
48 switch k {
49 case KindString, KindNone:
50 return nil
51 case KindInt:
52 if _, err := strconv.ParseInt(v, 10, 64); err != nil {
53 return errors.New("expected an integer, got " + strconv.Quote(v))
54 }
55 case KindBool:
56 if v != "true" && v != "false" {
57 return errors.New("expected true or false, got " + strconv.Quote(v))
58 }
59 case KindAddress:
60 if !address(v).IsValid() {
61 return errors.New("expected an address, got " + strconv.Quote(v))
62 }
63 default:
64 return errors.New("unknown kind " + string(k))
65 }
66 return nil
67}
68
69// Field is one parameter of a verb.
70type Field struct {
71 Name string
72 Kind Kind
73 Doc string
74 Opt bool // may be omitted, and only at the end of the parameter list
75}
76
77// P declares a required parameter.
78func P(name string, kind Kind, doc string) Field {
79 return Field{Name: name, Kind: kind, Doc: doc}
80}
81
82// Opt declares an optional parameter. Optional parameters must come last, and
83// appending one is the only way to add a parameter to a verb without breaking
84// callers.
85func Opt(name string, kind Kind, doc string) Field {
86 return Field{Name: name, Kind: kind, Doc: doc, Opt: true}
87}
88
89// Verb is one operation's signature.
90type Verb struct {
91 Name string
92 Doc string
93 Result Kind
94 Params []Field
95}
96
97// Op declares a verb.
98func Op(name, doc string, result Kind, params ...Field) Verb {
99 return Verb{Name: name, Doc: doc, Result: result, Params: params}
100}
101
102// Required returns how many leading parameters must be supplied.
103func (v Verb) Required() int {
104 n := 0
105 for _, p := range v.Params {
106 if p.Opt {
107 break
108 }
109 n++
110 }
111 return n
112}
113
114// Signature renders the verb as a one-line declaration:
115//
116// add(text:string, urgent:bool?) -> int
117func (v Verb) Signature() string {
118 var b strings.Builder
119 b.WriteString(v.Name)
120 b.WriteString("(")
121 for i, p := range v.Params {
122 if i > 0 {
123 b.WriteString(", ")
124 }
125 b.WriteString(p.Name + ":" + string(p.Kind))
126 if p.Opt {
127 b.WriteString("?")
128 }
129 }
130 b.WriteString(") -> " + string(v.Result))
131 return b.String()
132}
133
134// Schema is a handler's whole API, sorted by verb name.
135type Schema struct {
136 verbs []Verb
137}
138
139// NewSchema validates and sorts the declarations. It panics on a schema that
140// could not be served correctly: a blank or duplicate name, an unknown kind, a
141// required parameter after an optional one, or a duplicate parameter name.
142// Those are all authoring mistakes, and a handler carrying one should fail to
143// deploy rather than mis-describe itself forever.
144func NewSchema(verbs ...Verb) *Schema {
145 s := &Schema{verbs: make([]Verb, 0, len(verbs))}
146 for _, v := range verbs {
147 if strings.TrimSpace(v.Name) == "" || v.Name != strings.TrimSpace(v.Name) {
148 panic(errors.New("app: verb names must be non-blank and unpadded"))
149 }
150 if !v.Result.Valid() {
151 panic(errors.New("app: verb " + v.Name + ": unknown result kind " + string(v.Result)))
152 }
153 if _, dup := s.Lookup(v.Name); dup {
154 panic(errors.New("app: duplicate verb " + v.Name))
155 }
156 seenOpt := false
157 for i, p := range v.Params {
158 if strings.TrimSpace(p.Name) == "" || p.Name != strings.TrimSpace(p.Name) {
159 panic(errors.New("app: verb " + v.Name + ": parameter names must be non-blank and unpadded"))
160 }
161 if !p.Kind.Valid() || p.Kind == KindNone {
162 panic(errors.New("app: verb " + v.Name + ": parameter " + p.Name +
163 " has unusable kind " + string(p.Kind)))
164 }
165 if p.Opt {
166 seenOpt = true
167 } else if seenOpt {
168 // The encoding is positional, so a required parameter after an
169 // optional one could never be supplied without the optional.
170 panic(errors.New("app: verb " + v.Name + ": required parameter " +
171 p.Name + " follows an optional one"))
172 }
173 for j := 0; j < i; j++ {
174 if v.Params[j].Name == p.Name {
175 panic(errors.New("app: verb " + v.Name + ": duplicate parameter " + p.Name))
176 }
177 }
178 }
179 at := len(s.verbs)
180 for i, e := range s.verbs {
181 if v.Name < e.Name {
182 at = i
183 break
184 }
185 }
186 s.verbs = append(s.verbs, Verb{})
187 copy(s.verbs[at+1:], s.verbs[at:])
188 s.verbs[at] = v
189 }
190 return s
191}
192
193// Names returns the verb names, sorted.
194func (s *Schema) Names() []string {
195 if s == nil {
196 return nil
197 }
198 out := make([]string, 0, len(s.verbs))
199 for _, v := range s.verbs {
200 out = append(out, v.Name)
201 }
202 return out
203}
204
205// Lookup returns the verb by name.
206func (s *Schema) Lookup(name string) (Verb, bool) {
207 if s == nil {
208 return Verb{}, false
209 }
210 for _, v := range s.verbs {
211 if v.Name == name {
212 return v, true
213 }
214 }
215 return Verb{}, false
216}
217
218// Verbs returns a copy of the declarations.
219func (s *Schema) Verbs() []Verb {
220 if s == nil {
221 return nil
222 }
223 dup := make([]Verb, len(s.verbs))
224 copy(dup, s.verbs)
225 return dup
226}
227
228// Signature renders one verb, or "" if it is not declared.
229func (s *Schema) Signature(name string) string {
230 if v, ok := s.Lookup(name); ok {
231 return v.Signature()
232 }
233 return ""
234}
235
236// Markdown renders the API as a table, for Render.
237func (s *Schema) Markdown() string {
238 if s == nil || len(s.verbs) == 0 {
239 return "_No API declared._\n"
240 }
241 var b strings.Builder
242 b.WriteString("| Verb | Payload | Returns | |\n|---|---|---|---|\n")
243 for _, v := range s.verbs {
244 params := make([]string, 0, len(v.Params))
245 for _, p := range v.Params {
246 f := p.Name + ":" + string(p.Kind)
247 if p.Opt {
248 f += "?"
249 }
250 params = append(params, f)
251 }
252 p := strings.Join(params, ", ")
253 if p == "" {
254 p = "—"
255 }
256 b.WriteString("| `" + v.Name + "` | `" + p + "` | `" + string(v.Result) +
257 "` | " + v.Doc + " |\n")
258 }
259 return b.String()
260}
261
262// JSON renders the schema for tooling: a client can build a form from this
263// without reading any Gno source.
264func (s *Schema) JSON() string {
265 var b strings.Builder
266 b.WriteString(`{"verbs":[`)
267 if s != nil {
268 for i, v := range s.verbs {
269 if i > 0 {
270 b.WriteString(",")
271 }
272 b.WriteString(`{"name":` + jsonStr(v.Name) +
273 `,"doc":` + jsonStr(v.Doc) +
274 `,"result":` + jsonStr(string(v.Result)) +
275 `,"params":[`)
276 for j, p := range v.Params {
277 if j > 0 {
278 b.WriteString(",")
279 }
280 b.WriteString(`{"name":` + jsonStr(p.Name) +
281 `,"kind":` + jsonStr(string(p.Kind)) +
282 `,"doc":` + jsonStr(p.Doc) +
283 `,"optional":` + strconv.FormatBool(p.Opt) + `}`)
284 }
285 b.WriteString(`]}`)
286 }
287 }
288 b.WriteString(`]}`)
289 return b.String()
290}
291
292// Diff reports what upgrading from prev to s would break for existing callers.
293// An empty result means every payload that worked before still works.
294//
295// Breaking: a verb disappears, a parameter disappears or changes kind or
296// position, a new required parameter appears, or a result kind changes.
297// Not breaking: a new verb, a new optional parameter at the end, new
298// documentation.
299func (s *Schema) Diff(prev *Schema) []string {
300 out := []string{}
301 if prev == nil {
302 return out
303 }
304 for _, old := range prev.verbs {
305 nv, ok := s.Lookup(old.Name)
306 if !ok {
307 out = append(out, "verb "+old.Name+" removed")
308 continue
309 }
310 if nv.Result != old.Result {
311 out = append(out, "verb "+old.Name+": result changed from "+
312 string(old.Result)+" to "+string(nv.Result))
313 }
314 for i, op := range old.Params {
315 if i >= len(nv.Params) {
316 out = append(out, "verb "+old.Name+": parameter "+op.Name+" removed")
317 continue
318 }
319 np := nv.Params[i]
320 if np.Name != op.Name {
321 out = append(out, "verb "+old.Name+": parameter "+strconv.Itoa(i)+
322 " renamed from "+op.Name+" to "+np.Name)
323 }
324 if np.Kind != op.Kind {
325 out = append(out, "verb "+old.Name+": parameter "+op.Name+
326 " changed from "+string(op.Kind)+" to "+string(np.Kind))
327 }
328 if np.Opt != op.Opt && !np.Opt {
329 out = append(out, "verb "+old.Name+": parameter "+op.Name+
330 " became required")
331 }
332 }
333 for i := len(old.Params); i < len(nv.Params); i++ {
334 if !nv.Params[i].Opt {
335 out = append(out, "verb "+old.Name+": required parameter "+
336 nv.Params[i].Name+" added")
337 }
338 }
339 }
340 return out
341}
342
343// jsonStr quotes and escapes a string for the JSON rendering above.
344func jsonStr(s string) string {
345 var b strings.Builder
346 b.WriteString(`"`)
347 for _, r := range s {
348 switch r {
349 case '"':
350 b.WriteString(`\"`)
351 case '\\':
352 b.WriteString(`\\`)
353 case '\n':
354 b.WriteString(`\n`)
355 case '\r':
356 b.WriteString(`\r`)
357 case '\t':
358 b.WriteString(`\t`)
359 default:
360 if r < 0x20 {
361 b.WriteString(`\u00`)
362 const hex = "0123456789abcdef"
363 b.WriteByte(hex[(r>>4)&0xf])
364 b.WriteByte(hex[r&0xf])
365 continue
366 }
367 b.WriteRune(r)
368 }
369 }
370 b.WriteString(`"`)
371 return b.String()
372}