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

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}