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.

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}