-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdecode.go
More file actions
295 lines (273 loc) · 6.37 KB
/
decode.go
File metadata and controls
295 lines (273 loc) · 6.37 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
package execx
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
"gopkg.in/yaml.v3"
)
// Decoder decodes serialized data into a destination value.
type Decoder interface {
Decode(data []byte, dst any) error
}
// DecoderFunc adapts a function to a Decoder.
type DecoderFunc func(data []byte, dst any) error
type decodeSource int
const (
decodeStdout decodeSource = iota
decodeStderr
decodeCombined
)
type decodeConfig struct {
source decodeSource
trim bool
}
func defaultDecodeConfig() decodeConfig {
return decodeConfig{source: decodeStdout}
}
// DecodeChain configures typed decoding for a command.
type DecodeChain struct {
cmd *Cmd
decoder Decoder
cfg decodeConfig
}
// Decode configures a custom decoder for this command.
// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode custom
//
// type payload struct {
// Name string
// }
// decoder := execx.DecoderFunc(func(data []byte, dst any) error {
// out, ok := dst.(*payload)
// if !ok {
// return fmt.Errorf("expected *payload")
// }
// _, val, ok := strings.Cut(string(data), "=")
// if !ok {
// return fmt.Errorf("invalid payload")
// }
// out.Name = val
// return nil
// })
// var out payload
// _ = execx.Command("printf", "name=gopher").
// Decode(decoder).
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (c *Cmd) Decode(decoder Decoder) *DecodeChain {
return &DecodeChain{
cmd: c,
decoder: decoder,
cfg: defaultDecodeConfig(),
}
}
// Decode implements Decoder.
func (f DecoderFunc) Decode(data []byte, dst any) error {
return f(data, dst)
}
// DecodeJSON configures JSON decoding for this command.
// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode json
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("printf", `{"name":"gopher"}`).
// DecodeJSON().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (c *Cmd) DecodeJSON() *DecodeChain {
return c.Decode(DecoderFunc(json.Unmarshal))
}
// DecodeYAML configures YAML decoding for this command.
// Decoding reads from stdout by default; use FromStdout, FromStderr, or FromCombined to select a source.
// @group Decoding
//
// Example: decode yaml
//
// type payload struct {
// Name string `yaml:"name"`
// }
// var out payload
// _ = execx.Command("printf", "name: gopher").
// DecodeYAML().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (c *Cmd) DecodeYAML() *DecodeChain {
return c.Decode(DecoderFunc(yaml.Unmarshal))
}
// FromStdout decodes from stdout (default).
// @group Decoding
//
// Example: decode from stdout
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("printf", `{"name":"gopher"}`).
// DecodeJSON().
// FromStdout().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (d *DecodeChain) FromStdout() *DecodeChain {
d.cfg.source = decodeStdout
return d
}
// FromStderr decodes from stderr.
// @group Decoding
//
// Example: decode from stderr
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}' 1>&2`).
// DecodeJSON().
// FromStderr().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (d *DecodeChain) FromStderr() *DecodeChain {
d.cfg.source = decodeStderr
return d
}
// FromCombined decodes from combined stdout+stderr.
// @group Decoding
//
// Example: decode combined
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("sh", "-c", `printf '{"name":"gopher"}'`).
// DecodeJSON().
// FromCombined().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (d *DecodeChain) FromCombined() *DecodeChain {
d.cfg.source = decodeCombined
return d
}
// Trim trims whitespace before decoding.
// @group Decoding
//
// Example: decode trim
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("printf", " {\"name\":\"gopher\"} ").
// DecodeJSON().
// Trim().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (d *DecodeChain) Trim() *DecodeChain {
d.cfg.trim = true
return d
}
// Into executes the command and decodes into dst.
// @group Decoding
//
// Example: decode into
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("printf", `{"name":"gopher"}`).
// DecodeJSON().
// Into(&out)
// fmt.Println(out.Name)
// // #string gopher
func (d *DecodeChain) Into(dst any) error {
return decodeInto(d.cmd, dst, d.decoder, d.cfg)
}
// DecodeWith executes the command and decodes stdout into dst.
// @group Decoding
//
// Example: decode with
//
// type payload struct {
// Name string `json:"name"`
// }
// var out payload
// _ = execx.Command("printf", `{"name":"gopher"}`).
// DecodeWith(&out, execx.DecoderFunc(json.Unmarshal))
// fmt.Println(out.Name)
// // #string gopher
func (c *Cmd) DecodeWith(dst any, decoder Decoder) error {
return decodeInto(c, dst, decoder, defaultDecodeConfig())
}
func decodeInto(c *Cmd, dst any, decoder Decoder, cfg decodeConfig) error {
if c == nil {
return errors.New("command is nil")
}
if decoder == nil {
return errors.New("decoder is nil")
}
if dst == nil {
return errors.New("destination is nil")
}
val := reflect.ValueOf(dst)
if val.Kind() != reflect.Ptr || val.IsNil() {
return errors.New("destination must be a non-nil pointer")
}
data, err := c.decodeSource(cfg.source)
if err != nil {
return err
}
if cfg.trim {
data = bytes.TrimSpace(data)
}
if err := decoder.Decode(data, dst); err != nil {
return fmt.Errorf("decode %s: %w", decodeSourceName(cfg.source), err)
}
return nil
}
func (c *Cmd) decodeSource(source decodeSource) ([]byte, error) {
switch source {
case decodeCombined:
out, err := c.CombinedOutput()
if err != nil {
return nil, err
}
return []byte(out), nil
case decodeStderr:
res, err := c.Run()
if err != nil {
return nil, err
}
return []byte(res.Stderr), nil
case decodeStdout:
fallthrough
default:
return c.OutputBytes()
}
}
func decodeSourceName(source decodeSource) string {
switch source {
case decodeCombined:
return "combined output"
case decodeStderr:
return "stderr"
default:
return "stdout"
}
}