-
Notifications
You must be signed in to change notification settings - Fork 2.4k
/
Copy pathhelp.go
562 lines (495 loc) · 18.4 KB
/
help.go
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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"bytes"
"fmt"
"reflect"
"strings"
"text/tabwriter"
)
// baseHelpDescs house the various help labels, types, and example values used
// when generating help. The per-command synopsis, field descriptions,
// conditions, and result descriptions are to be provided by the caller.
var baseHelpDescs = map[string]string{
// Misc help labels and output.
"help-arguments": "Arguments",
"help-arguments-none": "None",
"help-result": "Result",
"help-result-nothing": "Nothing",
"help-default": "default",
"help-optional": "optional",
"help-required": "required",
// JSON types.
"json-type-numeric": "numeric",
"json-type-string": "string",
"json-type-bool": "boolean",
"json-type-array": "array of ",
"json-type-object": "object",
"json-type-value": "value",
// JSON examples.
"json-example-string": "value",
"json-example-bool": "true|false",
"json-example-map-data": "data",
"json-example-unknown": "unknown",
}
// descLookupFunc is a function which is used to lookup a description given
// a key.
type descLookupFunc func(string) string
// reflectTypeToJSONType returns a string that represents the JSON type
// associated with the provided Go type.
func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string {
kind := rt.Kind()
if isNumeric(kind) {
return xT("json-type-numeric")
}
switch kind {
case reflect.String:
return xT("json-type-string")
case reflect.Bool:
return xT("json-type-bool")
case reflect.Array, reflect.Slice:
return xT("json-type-array") + reflectTypeToJSONType(xT,
rt.Elem())
case reflect.Struct:
return xT("json-type-object")
case reflect.Map:
return xT("json-type-object")
}
return xT("json-type-value")
}
// resultStructHelp returns a slice of strings containing the result help output
// for a struct. Each line makes use of tabs to separate the relevant pieces so
// a tabwriter can be used later to line everything up. The descriptions are
// pulled from the active help descriptions map based on the lowercase version
// of the provided reflect type and json name (or the lowercase version of the
// field name if no json tag was specified).
func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string {
indent := strings.Repeat(" ", indentLevel)
typeName := strings.ToLower(rt.Name())
// Generate the help for each of the fields in the result struct.
numField := rt.NumField()
results := make([]string, 0, numField)
for i := 0; i < numField; i++ {
rtf := rt.Field(i)
// The field name to display is the json name when it's
// available, otherwise use the lowercase field name.
var fieldName string
if tag := rtf.Tag.Get("json"); tag != "" {
fieldName = strings.Split(tag, ",")[0]
} else {
fieldName = strings.ToLower(rtf.Name)
}
// Deference pointer if needed.
rtfType := rtf.Type
if rtfType.Kind() == reflect.Ptr {
rtfType = rtf.Type.Elem()
}
// Generate the JSON example for the result type of this struct
// field. When it is a complex type, examine the type and
// adjust the opening bracket and brace combination accordingly.
fieldType := reflectTypeToJSONType(xT, rtfType)
fieldDescKey := typeName + "-" + fieldName
fieldExamples, isComplex := reflectTypeToJSONExample(xT,
rtfType, indentLevel, fieldDescKey)
if isComplex {
var brace string
kind := rtfType.Kind()
if kind == reflect.Array || kind == reflect.Slice {
brace = "[{"
} else {
brace = "{"
}
result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent,
fieldName, brace, fieldType, xT(fieldDescKey))
results = append(results, result)
for _, example := range fieldExamples {
results = append(results, example)
}
} else {
result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent,
fieldName, fieldExamples[0], fieldType,
xT(fieldDescKey))
results = append(results, result)
}
}
return results
}
// reflectTypeToJSONExample generates example usage in the format used by the
// help output. It handles arrays, slices and structs recursively. The output
// is returned as a slice of lines so the final help can be nicely aligned via
// a tab writer. A bool is also returned which specifies whether or not the
// type results in a complex JSON object since they need to be handled
// differently.
func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool) {
// Indirect pointer if needed.
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
kind := rt.Kind()
if isNumeric(kind) {
if kind == reflect.Float32 || kind == reflect.Float64 {
return []string{"n.nnn"}, false
}
return []string{"n"}, false
}
switch kind {
case reflect.String:
return []string{`"` + xT("json-example-string") + `"`}, false
case reflect.Bool:
return []string{xT("json-example-bool")}, false
case reflect.Struct:
indent := strings.Repeat(" ", indentLevel)
results := resultStructHelp(xT, rt, indentLevel+1)
// An opening brace is needed for the first indent level. For
// all others, it will be included as a part of the previous
// field.
if indentLevel == 0 {
newResults := make([]string, len(results)+1)
newResults[0] = "{"
copy(newResults[1:], results)
results = newResults
}
// The closing brace has a comma after it except for the first
// indent level. The final tabs are necessary so the tab writer
// lines things up properly.
closingBrace := indent + "}"
if indentLevel > 0 {
closingBrace += ","
}
results = append(results, closingBrace+"\t\t")
return results, true
case reflect.Array, reflect.Slice:
results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(),
indentLevel, fieldDescKey)
// When the result is complex, it is because this is an array of
// objects.
if isComplex {
// When this is at indent level zero, there is no
// previous field to house the opening array bracket, so
// replace the opening object brace with the array
// syntax. Also, replace the final closing object brace
// with the variadiac array closing syntax.
indent := strings.Repeat(" ", indentLevel)
if indentLevel == 0 {
results[0] = indent + "[{"
results[len(results)-1] = indent + "},...]"
return results, true
}
// At this point, the indent level is greater than 0, so
// the opening array bracket and object brace are
// already a part of the previous field. However, the
// closing entry is a simple object brace, so replace it
// with the variadiac array closing syntax. The final
// tabs are necessary so the tab writer lines things up
// properly.
results[len(results)-1] = indent + "},...],\t\t"
return results, true
}
// It's an array of primitives, so return the formatted text
// accordingly.
return []string{fmt.Sprintf("[%s,...]", results[0])}, false
case reflect.Map:
indent := strings.Repeat(" ", indentLevel)
results := make([]string, 0, 3)
// An opening brace is needed for the first indent level. For
// all others, it will be included as a part of the previous
// field.
if indentLevel == 0 {
results = append(results, indent+"{")
}
// Maps are a bit special in that they need to have the key,
// value, and description of the object entry specifically
// called out.
innerIndent := strings.Repeat(" ", indentLevel+1)
result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent,
xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"),
reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc"))
results = append(results, result)
results = append(results, innerIndent+"...")
results = append(results, indent+"}")
return results, true
}
return []string{xT("json-example-unknown")}, false
}
// resultTypeHelp generates and returns formatted help for the provided result
// type.
func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string {
// Generate the JSON example for the result type.
results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey)
// When this is a primitive type, add the associated JSON type and
// result description into the final string, format it accordingly,
// and return it.
if !isComplex {
return fmt.Sprintf("%s (%s) %s", results[0],
reflectTypeToJSONType(xT, rt), xT(fieldDescKey))
}
// At this point, this is a complex type that already has the JSON types
// and descriptions in the results. Thus, use a tab writer to nicely
// align the help text.
var formatted bytes.Buffer
w := new(tabwriter.Writer)
w.Init(&formatted, 0, 4, 1, ' ', 0)
for i, text := range results {
if i == len(results)-1 {
fmt.Fprintf(w, text)
} else {
fmt.Fprintln(w, text)
}
}
w.Flush()
return formatted.String()
}
// argTypeHelp returns the type of provided command argument as a string in the
// format used by the help output. In particular, it includes the JSON type
// (boolean, numeric, string, array, object) along with optional and the default
// value if applicable.
func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string {
// Indirect the pointer if needed and track if it's an optional field.
fieldType := structField.Type
var isOptional bool
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
isOptional = true
}
// When there is a default value, it must also be a pointer due to the
// rules enforced by RegisterCmd.
if defaultVal != nil {
indirect := defaultVal.Elem()
defaultVal = &indirect
}
// Convert the field type to a JSON type.
details := make([]string, 0, 3)
details = append(details, reflectTypeToJSONType(xT, fieldType))
// Add optional and default value to the details if needed.
if isOptional {
details = append(details, xT("help-optional"))
// Add the default value if there is one. This is only checked
// when the field is optional since a non-optional field can't
// have a default value.
if defaultVal != nil {
val := defaultVal.Interface()
if defaultVal.Kind() == reflect.String {
val = fmt.Sprintf(`"%s"`, val)
}
str := fmt.Sprintf("%s=%v", xT("help-default"), val)
details = append(details, str)
}
} else {
details = append(details, xT("help-required"))
}
return strings.Join(details, ", ")
}
// argHelp generates and returns formatted help for the provided command.
func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
// Return now if the command has no arguments.
rt := rtp.Elem()
numFields := rt.NumField()
if numFields == 0 {
return ""
}
// Generate the help for each argument in the command. Several
// simplifying assumptions are made here because the RegisterCmd
// function has already rigorously enforced the layout.
args := make([]string, 0, numFields)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
var defaultVal *reflect.Value
if defVal, ok := defaults[i]; ok {
defaultVal = &defVal
}
fieldName := strings.ToLower(rtf.Name)
helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName,
argTypeHelp(xT, rtf, defaultVal),
xT(method+"-"+fieldName))
args = append(args, helpText)
// For types which require a JSON object, or an array of JSON
// objects, generate the full syntax for the argument.
fieldType := rtf.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
kind := fieldType.Kind()
switch kind {
case reflect.Struct:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
args = append(args, resultText)
case reflect.Map:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
args = append(args, resultText)
case reflect.Array, reflect.Slice:
fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
if rtf.Type.Elem().Kind() == reflect.Struct {
resultText := resultTypeHelp(xT, fieldType,
fieldDescKey)
args = append(args, resultText)
}
}
}
// Add argument names, types, and descriptions if there are any. Use a
// tab writer to nicely align the help text.
var formatted bytes.Buffer
w := new(tabwriter.Writer)
w.Init(&formatted, 0, 4, 1, ' ', 0)
for _, text := range args {
fmt.Fprintln(w, text)
}
w.Flush()
return formatted.String()
}
// methodHelp generates and returns the help output for the provided command
// and method info. This is the main work horse for the exported MethodHelp
// function.
func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string, resultTypes []interface{}) string {
// Start off with the method usage and help synopsis.
help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method),
xT(method+"--synopsis"))
// Generate the help for each argument in the command.
if argText := argHelp(xT, rtp, defaults, method); argText != "" {
help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"),
argText)
} else {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"),
xT("help-arguments-none"))
}
// Generate the help text for each result type.
resultTexts := make([]string, 0, len(resultTypes))
for i := range resultTypes {
rtp := reflect.TypeOf(resultTypes[i])
fieldDescKey := fmt.Sprintf("%s--result%d", method, i)
if resultTypes[i] == nil {
resultText := xT("help-result-nothing")
resultTexts = append(resultTexts, resultText)
continue
}
resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey)
resultTexts = append(resultTexts, resultText)
}
// Add result types and descriptions. When there is more than one
// result type, also add the condition which triggers it.
if len(resultTexts) > 1 {
for i, resultText := range resultTexts {
condKey := fmt.Sprintf("%s--condition%d", method, i)
help += fmt.Sprintf("\n%s (%s):\n%s\n",
xT("help-result"), xT(condKey), resultText)
}
} else if len(resultTexts) > 0 {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
resultTexts[0])
} else {
help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
xT("help-result-nothing"))
}
return help
}
// isValidResultType returns whether the passed reflect kind is one of the
// acceptable types for results.
func isValidResultType(kind reflect.Kind) bool {
if isNumeric(kind) {
return true
}
switch kind {
case reflect.String, reflect.Struct, reflect.Array, reflect.Slice,
reflect.Bool, reflect.Map:
return true
}
return false
}
// GenerateHelp generates and returns help output for the provided method and
// result types given a map to provide the appropriate keys for the method
// synopsis, field descriptions, conditions, and result descriptions. The
// method must be associated with a registered type. All commands provided by
// this package are registered by default.
//
// The resultTypes must be pointer-to-types which represent the specific types
// of values the command returns. For example, if the command only returns a
// boolean value, there should only be a single entry of (*bool)(nil). Note
// that each type must be a single pointer to the type. Therefore, it is
// recommended to simply pass a nil pointer cast to the appropriate type as
// previously shown.
//
// The provided descriptions map must contain all of the keys or an error will
// be returned which includes the missing key, or the final missing key when
// there is more than one key missing. The generated help in the case of such
// an error will use the key in place of the description.
//
// The following outlines the required keys:
// "<method>--synopsis" Synopsis for the command
// "<method>-<lowerfieldname>" Description for each command argument
// "<typename>-<lowerfieldname>" Description for each object field
// "<method>--condition<#>" Description for each result condition
// "<method>--result<#>" Description for each primitive result num
//
// Notice that the "special" keys synopsis, condition<#>, and result<#> are
// preceded by a double dash to ensure they don't conflict with field names.
//
// The condition keys are only required when there is more than on result type,
// and the result key for a given result type is only required if it's not an
// object.
//
// For example, consider the 'help' command itself. There are two possible
// returns depending on the provided parameters. So, the help would be
// generated by calling the function as follows:
// GenerateHelp("help", descs, (*string)(nil), (*string)(nil)).
//
// The following keys would then be required in the provided descriptions map:
//
// "help--synopsis": "Returns a list of all commands or help for ...."
// "help-command": "The command to retrieve help for",
// "help--condition0": "no command provided"
// "help--condition1": "command specified"
// "help--result0": "List of commands"
// "help--result1": "Help for specified command"
func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) {
// Look up details about the provided method and error out if not
// registered.
registerLock.RLock()
rtp, ok := methodToConcreteType[method]
info := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return "", makeError(ErrUnregisteredMethod, str)
}
// Validate each result type is a pointer to a supported type (or nil).
for i, resultType := range resultTypes {
if resultType == nil {
continue
}
rtp := reflect.TypeOf(resultType)
if rtp.Kind() != reflect.Ptr {
str := fmt.Sprintf("result #%d (%v) is not a pointer",
i, rtp.Kind())
return "", makeError(ErrInvalidType, str)
}
elemKind := rtp.Elem().Kind()
if !isValidResultType(elemKind) {
str := fmt.Sprintf("result #%d (%v) is not an allowed "+
"type", i, elemKind)
return "", makeError(ErrInvalidType, str)
}
}
// Create a closure for the description lookup function which falls back
// to the base help descritptions map for unrecognized keys and tracks
// and missing keys.
var missingKey string
xT := func(key string) string {
if desc, ok := descs[key]; ok {
return desc
}
if desc, ok := baseHelpDescs[key]; ok {
return desc
}
missingKey = key
return key
}
// Generate and return the help for the method.
help := methodHelp(xT, rtp, info.defaults, method, resultTypes)
if missingKey != "" {
return help, makeError(ErrMissingDescription, missingKey)
}
return help, nil
}