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.

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}