-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcustom.go
More file actions
367 lines (330 loc) · 9.99 KB
/
custom.go
File metadata and controls
367 lines (330 loc) · 9.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
package nu
import (
"context"
"encoding/binary"
"fmt"
"reflect"
"github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
"github.com/ainvaltin/nu-plugin/operator"
)
/*
Ordering is result type for the PartialCmp CustomValueOp call.
Predefined constants [Incomparable], [Less], [Equal] and [Greater] should
be used by CustomValue implementations.
*/
type Ordering int8
const (
Incomparable Ordering = -128 // the values can't be compared
Less Ordering = -1 // left hand side is less than right hand side
Equal Ordering = 0 // both values are equal
Greater Ordering = 1 // left hand side is greater than right hand side
)
func (op Ordering) encodeMsgpack(enc *msgpack.Encoder) error {
if err := encodeMapStart(enc, "Ordering"); err != nil {
return err
}
switch op {
case Incomparable:
return enc.EncodeNil()
case Less:
return enc.EncodeString("Less")
case Equal:
return enc.EncodeString("Equal")
case Greater:
return enc.EncodeString("Greater")
}
return fmt.Errorf("unsupported Ordering value %d", op)
}
/*
CustomValue is the interface user defined types have to implement to be used as Nu [Custom Value].
The [CustomValueOp] plugin calls are routed to the appropriate method of the variable.
[Custom Value]: https://www.nushell.sh/contributor-book/plugin_protocol_reference.html#custom
[CustomValueOp]: https://www.nushell.sh/contributor-book/plugin_protocol_reference.html#customvalueop-plugin-call
*/
type CustomValue interface {
// The human-readable name of the custom value emitted by the plugin.
Name() string
// Whether the engine should send drop notification about this variable.
NotifyOnDrop() bool
// This method is called to notify the plugin that a CustomValue that had notify_on_drop set to
// true (ie the NotifyOnDrop method returns true) was dropped in the engine - i.e., all copies
// of it have gone out of scope.
Dropped(ctx context.Context) error
// Returns the result of following a numeric cell path (e.g. $custom_value.0) on the custom value.
// This is most commonly used with custom types that act like lists or tables.
// The result may be another custom value. The parameter `optional` is used to control whether the
// path is optional.
FollowPathInt(ctx context.Context, item uint, optional bool) (Value, error)
// Returns the result of following a string cell path (e.g. $custom_value.field) on the custom value.
// This is most commonly used with custom types that act like lists or tables.
// The result may be another custom value. The parameters `optional` and `caseSensitive` are used to
// control whether the path is optional and whether the path is case sensitive.
FollowPathString(ctx context.Context, item string, optional, caseSensitive bool) (Value, error)
// Returns the result of evaluating an Operator on this custom value with another value.
// The rhs Value may be any value - not just the same custom value type.
// The result may be another custom value.
Operation(ctx context.Context, op operator.Operator, rhs Value) (Value, error)
// Compares the custom value to another value and returns the Ordering that should be used, if any.
// The argument may be any value - not just the same custom value type.
PartialCmp(ctx context.Context, v Value) Ordering
// Saves the custom value to a file at the given path.
Save(ctx context.Context, path string) error
// Returns a plain value that is representative of the custom value, or an error if this is not possible.
// Sending a custom value back for this operation is not allowed.
ToBaseValue(ctx context.Context) (Value, error)
}
func encodeCustomValue(enc *msgpack.Encoder, id uint32, value CustomValue) error {
notifyDrop := value.NotifyOnDrop()
cnt := 3 + bval(notifyDrop)
if err := enc.EncodeMapLen(cnt); err != nil {
return err
}
if err := encodeString(enc, "type", "PluginCustomValue"); err != nil {
return err
}
if err := encodeString(enc, "name", value.Name()); err != nil {
return err
}
if err := enc.EncodeString("data"); err != nil {
return err
}
if err := enc.EncodeBytes(binary.BigEndian.AppendUint32(nil, id)); err != nil {
return err
}
if notifyDrop {
if err := encodeBoolean(enc, "notify_on_drop", true); err != nil {
return err
}
}
return nil
}
func decodeCustomValue(dec *msgpack.Decoder, p *Plugin) (cv CustomValue, _ error) {
return cv, decodeMap("CustomValue", dec, func(dec *msgpack.Decoder, key string) (err error) {
switch key {
case "type", "name":
_, err = dec.DecodeString()
case "data":
id, ok := uint32(0), false
if id, err = readCVID(dec); err == nil {
if cv, ok = p.cvals[id]; !ok {
return fmt.Errorf("no CustomValue with id %d", id)
}
}
case "notify_on_drop":
_, err = dec.DecodeBool()
default:
err = errUnknownField
}
return err
})
}
type (
dropped struct{}
toBaseValue struct{}
followPathInt struct {
Path spanned[uint] `msgpack:"index"`
Optional bool `msgpack:"optional"`
}
followPathString struct {
Path spanned[string] `msgpack:"column_name"`
Optional bool `msgpack:"optional"`
Casing string `msgpack:"casing"`
}
partialCmp struct{ value Value }
operation struct {
op operator.Operator
value Value
}
save struct {
Path spanned[string] `msgpack:"path"`
}
)
func (p followPathString) isCaseSensitive() bool {
return p.Casing == "Sensitive"
}
type customValueOp struct {
name string
id uint32
span Span
op any
}
func (cvo *customValueOp) decodeMsgpack(dec *msgpack.Decoder, p *Plugin) error {
cnt, err := dec.DecodeArrayLen()
if err != nil {
return fmt.Errorf("reading CustomValueOp tuple length: %w", err)
}
if cnt != 2 {
return fmt.Errorf("expected 2-tuple, got %d", cnt)
}
// first map with item + span
if err := cvo.readValue(dec); err != nil {
return err
}
// then the op
return cvo.readOperation(dec, p)
}
func (cvo *customValueOp) readOperation(dec *msgpack.Decoder, p *Plugin) error {
c, err := dec.PeekCode()
if err != nil {
return err
}
switch {
case msgpcode.IsFixedString(c), msgpcode.IsString(c):
s, err := dec.DecodeString()
if err != nil {
return err
}
switch s {
case "ToBaseValue":
cvo.op = toBaseValue{}
case "Dropped":
cvo.op = dropped{}
default:
return fmt.Errorf("unknown CustomValueOp command %q", s)
}
case msgpcode.IsFixedMap(c):
name, err := decodeWrapperMap(dec)
if err != nil {
return err
}
switch name {
case "FollowPathInt":
v := followPathInt{}
err = dec.DecodeValue(reflect.ValueOf(&v))
cvo.op = v
case "FollowPathString":
v := followPathString{}
err = dec.DecodeValue(reflect.ValueOf(&v))
cvo.op = v
case "PartialCmp":
v := partialCmp{}
err = v.value.decodeMsgpack(dec, p)
cvo.op = v
case "Operation":
v := operation{}
err = v.decodeMsgpack(dec, p)
cvo.op = v
case "Save":
v := save{}
err = dec.DecodeValue(reflect.ValueOf(&v))
cvo.op = v
default:
return fmt.Errorf("unknown CustomValueOp[1] type %q", name)
}
if err != nil {
return fmt.Errorf("decoding CustomValueOp[1].%s: %w", name, err)
}
default:
return fmt.Errorf("unsupported CustomValueOp[1] value: %d", c)
}
return nil
}
/*
read the first item in the duple, item and span
*/
func (cvo *customValueOp) readValue(dec *msgpack.Decoder) error {
cnt, err := dec.DecodeMapLen()
if err != nil {
return fmt.Errorf("reading CustomValueOp[0] map len: %w", err)
}
for range cnt {
key, err := dec.DecodeString()
if err != nil {
return fmt.Errorf("reading CustomValueOp[0] key: %w", err)
}
switch key {
case "item":
err = cvo.readCustomValueData(dec)
case "span":
err = cvo.span.decodeMsgpack(dec)
default:
return fmt.Errorf("unknown key %q under CustomValueOp[0]", key)
}
if err != nil {
return fmt.Errorf("decoding CustomValueOp[0] key %q: %w", key, err)
}
}
return nil
}
func (cvo *customValueOp) readCustomValueData(dec *msgpack.Decoder) error {
cnt, err := dec.DecodeMapLen()
if err != nil {
return fmt.Errorf("reading CustomValueOp.item map len: %w", err)
}
for range cnt {
key, err := dec.DecodeString()
if err != nil {
return fmt.Errorf("reading CustomValueOp.item key: %w", err)
}
switch key {
case "name":
cvo.name, err = dec.DecodeString()
case "data":
cvo.id, err = readCVID(dec)
case "notify_on_drop":
_, err = dec.DecodeBool()
default:
return fmt.Errorf("unknown key %q under CustomValueOp.item", key)
}
if err != nil {
return fmt.Errorf("decoding CustomValueOp.item key %q: %w", key, err)
}
}
return nil
}
func readCVID(dec *msgpack.Decoder) (uint32, error) {
n, err := dec.DecodeArrayLen()
if err != nil {
return 0, fmt.Errorf("reading Binary array length: %w", err)
}
if n < 1 {
return 0, nil
}
// just "dec.ReadFull(buf)" won't work as uint8 might be encoded using
// two bytes per value but ArrayLen gives us count of items (not bytes)
buf := make([]byte, n)
for i := range n {
if buf[i], err = dec.DecodeUint8(); err != nil {
return 0, fmt.Errorf("reading array item [%d]: %w", i, err)
}
}
if len(buf) != 4 {
return 0, fmt.Errorf("expected CustomValue data to be 4 bytes, got %d", len(buf))
}
return binary.BigEndian.Uint32(buf), nil
}
func (op *operation) decodeMsgpack(dec *msgpack.Decoder, p *Plugin) error {
cnt, err := dec.DecodeArrayLen()
if err != nil {
return fmt.Errorf("reading Operation tuple length: %w", err)
}
if cnt != 2 {
return fmt.Errorf("expected 2-tuple, got %d", cnt)
}
// first map with item + span
cnt, err = dec.DecodeMapLen()
if err != nil {
return fmt.Errorf("reading Operation map len: %w", err)
}
for range cnt {
key, err := dec.DecodeString()
if err != nil {
return fmt.Errorf("reading Operation key: %w", err)
}
switch key {
case "item":
// single item map like {"Math": "Plus"}
err = op.op.DecodeMsgpack(dec)
case "span":
err = (&Span{}).decodeMsgpack(dec)
default:
return fmt.Errorf("unknown key %q under Operation", key)
}
if err != nil {
return fmt.Errorf("decoding Operation key %q: %w", key, err)
}
}
// Value
return op.value.decodeMsgpack(dec, p)
}