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

args.gno

8.25 Kb · 265 lines
  1package app
  2
  3import (
  4	"encoding/csv"
  5	"errors"
  6	"strconv"
  7	"strings"
  8	"unicode/utf8"
  9)
 10
 11// The wire format is one CSV record.
 12//
 13// CSV because it is in the standard library, because it quotes correctly --
 14// a task called `buy milk, eggs` survives a round trip, which a naive split
 15// on commas does not -- and because it is writable by hand on a gnokey
 16// command line. Fields are positional, in the order the verb declares them,
 17// which is why the schema forbids a required parameter after an optional one
 18// and why Diff treats a reordering as breaking.
 19//
 20//	Encode(addTask, "buy milk, eggs")  ->  "buy milk, eggs"   (quoted by csv)
 21//	Encode(transfer, "g1abc…", "42")   ->  "g1abc…,42"
 22//
 23// A verb with no parameters takes an empty payload.
 24
 25// Args is a decoded, validated payload. A handler receives one and reads
 26// fields by name; the app has already checked that every field is present and
 27// parses as its declared kind, so the accessors do not return errors.
 28type Args struct {
 29	verb   Verb
 30	values []string
 31}
 32
 33// Verb returns the name of the verb being called.
 34func (a *Args) Verb() string { return a.verb.Name }
 35
 36// Signature returns the declaration this payload was checked against, which
 37// is worth putting in a panic message.
 38func (a *Args) Signature() string { return a.verb.Signature() }
 39
 40// Raw returns the decoded fields in declaration order, for a handler that
 41// would rather loop than name them.
 42func (a *Args) Raw() []string {
 43	dup := make([]string, len(a.values))
 44	copy(dup, a.values)
 45	return dup
 46}
 47
 48// index returns the position of name and whether a value was supplied.
 49// It panics if the verb has no such parameter: that is the handler asking for
 50// something it did not declare, which no payload can fix.
 51func (a *Args) index(name string, want Kind) (int, bool) {
 52	for i, p := range a.verb.Params {
 53		if p.Name != name {
 54			continue
 55		}
 56		if want != "" && p.Kind != want {
 57			panic(errors.New("app: " + a.verb.Name + ": parameter " + name +
 58				" is declared " + string(p.Kind) + ", read as " + string(want)))
 59		}
 60		return i, i < len(a.values)
 61	}
 62	panic(errors.New("app: " + a.verb.Name + " has no parameter " + name +
 63		"; its signature is " + a.verb.Signature()))
 64}
 65
 66// Has reports whether an optional parameter was supplied.
 67func (a *Args) Has(name string) bool {
 68	_, ok := a.index(name, "")
 69	return ok
 70}
 71
 72// String returns a string parameter, or "" if it was optional and omitted.
 73func (a *Args) String(name string) string {
 74	i, ok := a.index(name, KindString)
 75	if !ok {
 76		return ""
 77	}
 78	return a.values[i]
 79}
 80
 81// Int returns an int parameter, or 0 if it was optional and omitted.
 82func (a *Args) Int(name string) int64 {
 83	i, ok := a.index(name, KindInt)
 84	if !ok {
 85		return 0
 86	}
 87	n, _ := strconv.ParseInt(a.values[i], 10, 64) // checked at decode
 88	return n
 89}
 90
 91// Bool returns a bool parameter, or false if it was optional and omitted.
 92func (a *Args) Bool(name string) bool {
 93	i, ok := a.index(name, KindBool)
 94	if !ok {
 95		return false
 96	}
 97	return a.values[i] == "true"
 98}
 99
100// Address returns an address parameter, or the zero address if it was
101// optional and omitted.
102func (a *Args) Address(name string) address {
103	i, ok := a.index(name, KindAddress)
104	if !ok {
105		return ""
106	}
107	return address(a.values[i])
108}
109
110// Encode builds a payload for v from values given in declaration order,
111// validating each against its declared kind. Use it when one realm calls
112// another app, so an encoding mistake fails where it was made.
113func Encode(v Verb, values ...string) string {
114	if err := checkArity(v, len(values)); err != nil {
115		panic(err)
116	}
117	for i, val := range values {
118		if err := v.Params[i].Kind.parse(val); err != nil {
119			panic(fieldErr(v, i, err))
120		}
121	}
122	if len(values) == 0 {
123		return ""
124	}
125	// A lone empty field encodes to nothing at all -- csv does not quote an
126	// empty field, so the record "" and the record with no fields are the
127	// same bytes. Quote it explicitly so the payload says which it is.
128	if len(values) == 1 && values[0] == "" {
129		return `""`
130	}
131	// Fast path: when nothing needs quoting, a CSV record IS the fields
132	// joined by commas, and building a csv.Writer to discover that costs
133	// more than the whole rest of the call. See needsQuotes.
134	plain := true
135	for _, val := range values {
136		if needsQuotes(val) {
137			plain = false
138			break
139		}
140	}
141	if plain {
142		return strings.Join(values, ",")
143	}
144
145	var b strings.Builder
146	w := csv.NewWriter(&b)
147	if err := w.Write(values); err != nil {
148		panic(errors.New("app: " + v.Name + ": " + err.Error()))
149	}
150	w.Flush()
151	if err := w.Error(); err != nil {
152		panic(errors.New("app: " + v.Name + ": " + err.Error()))
153	}
154	return strings.TrimRight(b.String(), "\n")
155}
156
157// splitPlain splits a payload on commas in a single pass, reporting false the
158// moment it meets a byte that gives commas a different meaning. The one pass
159// is the point: bailing out and splitting separately doubles the interpreted
160// work over the whole payload.
161func splitPlain(payload string) ([]string, bool) {
162	out := []string{}
163	start := 0
164	for i := 0; i < len(payload); i++ {
165		switch payload[i] {
166		case '"', '\r', '\n':
167			return nil, false
168		case ',':
169			out = append(out, payload[start:i])
170			start = i + 1
171		}
172	}
173	return append(out, payload[start:]), true
174}
175
176// needsQuotes mirrors csv.Writer's rule exactly. Diverging from it would mean
177// Encode produced something csv.Reader reads back differently, so this is
178// copied rather than approximated: a field is quoted when it is the literal
179// `\.`, when it contains a comma, quote, CR or LF, or when it begins with a
180// space.
181func needsQuotes(field string) bool {
182	if field == "" {
183		return false
184	}
185	if field == `\.` {
186		return true
187	}
188	switch field[0] {
189	case ' ', '\t', '\v', '\f':
190		return true
191	}
192	// A leading non-ASCII space (U+00A0 and friends) is rare enough to hand
193	// to the slow path rather than decode a rune for on every call.
194	if field[0] >= utf8.RuneSelf {
195		return true
196	}
197	// One pass, for the same reason splitPlain is one pass:
198	// strings.ContainsAny is interpreted Gno and costs more over a short
199	// field than the csv.Writer it is trying to avoid.
200	for i := 0; i < len(field); i++ {
201		switch field[i] {
202		case ',', '"', '\r', '\n':
203			return true
204		}
205	}
206	return false
207}
208
209// checkArity reports whether n values can satisfy v.
210func checkArity(v Verb, n int) error {
211	req := v.Required()
212	if n < req {
213		return errors.New("app: " + v.Name + " needs at least " + strconv.Itoa(req) +
214			" parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature())
215	}
216	if n > len(v.Params) {
217		return errors.New("app: " + v.Name + " takes at most " + strconv.Itoa(len(v.Params)) +
218			" parameter(s), got " + strconv.Itoa(n) + "; signature is " + v.Signature())
219	}
220	return nil
221}
222
223// fieldErr names the verb, the parameter and the signature, which is what a
224// caller needs to fix the payload without reading the handler's source.
225func fieldErr(v Verb, i int, err error) error {
226	return errors.New("app: " + v.Name + ": parameter " + v.Params[i].Name +
227		": " + err.Error() + "; signature is " + v.Signature())
228}
229
230// decode parses and validates a payload against v.
231func decode(v Verb, payload string) (*Args, error) {
232	values := []string{}
233	if payload == "" {
234		// Ambiguous on its own: no fields, or one empty field. The verb
235		// decides. A required first parameter means an empty payload is that
236		// parameter, empty -- which is what a gnokey caller passing "" for a
237		// string means, and Encode spells `""` for the same case.
238		if v.Required() >= 1 {
239			values = []string{""}
240		}
241	} else if rec, plain := splitPlain(payload); plain {
242		// Fast path: with no quote and no newline anywhere, no field is
243		// quoted, so every comma is a separator and every field is literal
244		// -- exactly what csv.Reader would return. splitPlain decides that
245		// and does the split in one pass, because every string operation
246		// here is interpreted Gno: two passes with strings.ContainsAny and
247		// strings.Split costs more than the csv parse it replaces.
248		values = rec
249	} else {
250		rec, err := csv.NewReader(strings.NewReader(payload)).Read()
251		if err != nil {
252			return nil, errors.New("app: " + v.Name + ": malformed payload: " + err.Error())
253		}
254		values = rec
255	}
256	if err := checkArity(v, len(values)); err != nil {
257		return nil, err
258	}
259	for i, val := range values {
260		if err := v.Params[i].Kind.parse(val); err != nil {
261			return nil, fieldErr(v, i, err)
262		}
263	}
264	return &Args{verb: v, values: values}, nil
265}