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}