1 package btcjson
2 3 import (
4 "bytes"
5 "fmt"
6 "reflect"
7 "strings"
8 "text/tabwriter"
9 )
10 11 // baseHelpDescs house the various help labels, types, and example values used when generating help. The per-command
12 // synopsis, field descriptions, conditions, and result descriptions are to be provided by the caller.
13 var baseHelpDescs = map[string]string{
14 // Misc help labels and output.
15 "help-arguments": "Arguments",
16 "help-arguments-none": "None",
17 "help-result": "Result",
18 "help-result-nothing": "Nothing",
19 "help-default": "default",
20 "help-optional": "optional",
21 "help-required": "required",
22 // JSON types.
23 "json-type-numeric": "numeric",
24 "json-type-string": "string",
25 "json-type-bool": "boolean",
26 "json-type-array": "array of ",
27 "json-type-object": "object",
28 "json-type-value": "value",
29 // JSON examples.
30 "json-example-string": "value",
31 "json-example-bool": "true|false",
32 "json-example-map-data": "data",
33 "json-example-unknown": "unknown",
34 }
35 36 // descLookupFunc is a function which is used to lookup a description given a key.
37 type descLookupFunc func(string) string
38 39 // reflectTypeToJSONType returns a string that represents the JSON type associated with the provided Go type.
40 func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string {
41 kind := rt.Kind()
42 if isNumeric(kind) {
43 return xT("json-type-numeric")
44 }
45 switch kind {
46 case reflect.String:
47 return xT("json-type-string")
48 case reflect.Bool:
49 return xT("json-type-bool")
50 case reflect.Array, reflect.Slice:
51 return xT("json-type-array") + reflectTypeToJSONType(xT,
52 rt.Elem(),
53 )
54 case reflect.Struct:
55 return xT("json-type-object")
56 case reflect.Map:
57 return xT("json-type-object")
58 }
59 return xT("json-type-value")
60 }
61 62 // resultStructHelp returns a slice of strings containing the result help output for a struct. Each line makes use of
63 // tabs to separate the relevant pieces so a tabwriter can be used later to line everything up. The descriptions are
64 // pulled from the active help descriptions map based on the lowercase version of the provided reflect type and json
65 // name (or the lowercase version of the field name if no json tag was specified).
66 func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string {
67 indent := strings.Repeat(" ", indentLevel)
68 typeName := strings.ToLower(rt.Name())
69 // Generate the help for each of the fields in the result struct.
70 numField := rt.NumField()
71 results := make([]string, 0, numField)
72 for i := 0; i < numField; i++ {
73 rtf := rt.Field(i)
74 // The field name to display is the json name when it's available, otherwise use the lowercase field name.
75 var fieldName string
76 if tag := rtf.Tag.Get("json"); tag != "" {
77 fieldName = strings.Split(tag, ",")[0]
78 } else {
79 fieldName = strings.ToLower(rtf.Name)
80 }
81 // Deference pointer if needed.
82 rtfType := rtf.Type
83 if rtfType.Kind() == reflect.Ptr {
84 rtfType = rtf.Type.Elem()
85 }
86 // Generate the JSON example for the result type of this struct field. When it is a complex type, examine the
87 // type and adjust the opening bracket and brace combination accordingly.
88 fieldType := reflectTypeToJSONType(xT, rtfType)
89 fieldDescKey := typeName + "-" + fieldName
90 fieldExamples, isComplex := reflectTypeToJSONExample(xT,
91 rtfType, indentLevel, fieldDescKey,
92 )
93 if isComplex {
94 var brace string
95 kind := rtfType.Kind()
96 if kind == reflect.Array || kind == reflect.Slice {
97 brace = "[{"
98 } else {
99 brace = "{"
100 }
101 result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent,
102 fieldName, brace, fieldType, xT(fieldDescKey),
103 )
104 results = append(results, result)
105 results = append(results, fieldExamples...)
106 } else {
107 result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent,
108 fieldName, fieldExamples[0], fieldType,
109 xT(fieldDescKey),
110 )
111 results = append(results, result)
112 }
113 }
114 return results
115 }
116 117 // reflectTypeToJSONExample generates example usage in the format used by the help output. It handles arrays, slices and
118 // structs recursively. The output is returned as a slice of lines so the final help can be nicely aligned via a tab
119 // writer. A bool is also returned which specifies whether or not the type results in a complex JSON object since they
120 // need to be handled differently.
121 func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool,
122 ) {
123 // Indirect pointer if needed.
124 if rt.Kind() == reflect.Ptr {
125 rt = rt.Elem()
126 }
127 kind := rt.Kind()
128 if isNumeric(kind) {
129 if kind == reflect.Float32 || kind == reflect.Float64 {
130 return []string{"n.nnn"}, false
131 }
132 return []string{"n"}, false
133 }
134 switch kind {
135 case reflect.String:
136 return []string{`"` + xT("json-example-string") + `"`}, false
137 case reflect.Bool:
138 return []string{xT("json-example-bool")}, false
139 case reflect.Struct:
140 indent := strings.Repeat(" ", indentLevel)
141 results := resultStructHelp(xT, rt, indentLevel+1)
142 // An opening brace is needed for the first indent level. For all others, it will be included as a part of the
143 // previous field.
144 if indentLevel == 0 {
145 newResults := make([]string, len(results)+1)
146 newResults[0] = "{"
147 copy(newResults[1:], results)
148 results = newResults
149 }
150 // The closing brace has a comma after it except for the first indent level. The final tabs are necessary so the
151 // tab writer lines things up properly.
152 closingBrace := indent + "}"
153 if indentLevel > 0 {
154 closingBrace += ","
155 }
156 results = append(results, closingBrace+"\t\t")
157 return results, true
158 case reflect.Array, reflect.Slice:
159 results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(),
160 indentLevel, fieldDescKey,
161 )
162 // When the result is complex, it is because this is an array of objects.
163 if isComplex {
164 // When this is at indent level zero, there is no previous field to house the opening array bracket, so
165 // replace the opening object brace with the array syntax. Also, replace the final closing object brace with
166 // the variadic array closing syntax.
167 indent := strings.Repeat(" ", indentLevel)
168 if indentLevel == 0 {
169 results[0] = indent + "[{"
170 results[len(results)-1] = indent + "},...]"
171 return results, true
172 }
173 // At this point, the indent level is greater than 0, so the opening array bracket and object brace are
174 // already a part of the previous field. However, the closing entry is a simple object brace, so replace it
175 // with the variadic array closing syntax. The final tabs are necessary so the tab writer lines things up
176 // properly.
177 results[len(results)-1] = indent + "},...],\t\t"
178 return results, true
179 }
180 // It's an array of primitives, so return the formatted text accordingly.
181 return []string{fmt.Sprintf("[%s,...]", results[0])}, false
182 case reflect.Map:
183 indent := strings.Repeat(" ", indentLevel)
184 results := make([]string, 0, 3)
185 // An opening brace is needed for the first indent level. For all others, it will be included as a part of the
186 // previous field.
187 if indentLevel == 0 {
188 results = append(results, indent+"{")
189 }
190 // Maps are a bit special in that they need to have the key, value, and description of the object entry
191 // specifically called out.
192 innerIndent := strings.Repeat(" ", indentLevel+1)
193 result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent, xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"),
194 reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc"),
195 )
196 results = append(results, result)
197 results = append(results, innerIndent+"...")
198 results = append(results, indent+"}")
199 return results, true
200 }
201 return []string{xT("json-example-unknown")}, false
202 }
203 204 // resultTypeHelp generates and returns formatted help for the provided result type.
205 func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string {
206 // Generate the JSON example for the result type.
207 results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey)
208 // When this is a primitive type, add the associated JSON type and result description into the final string, format
209 // it accordingly, and return it.
210 if !isComplex {
211 return fmt.Sprintf("%s (%s) %s", results[0], reflectTypeToJSONType(xT, rt), xT(fieldDescKey))
212 }
213 // At this point, this is a complex type that already has the JSON types and descriptions in the results. Thus, use
214 // a tab writer to nicely align the help text.
215 var formatted bytes.Buffer
216 w := new(tabwriter.Writer)
217 w.Init(&formatted, 0, 4, 1, ' ', 0)
218 for i, text := range results {
219 if i == len(results)-1 {
220 _, _ = fmt.Fprintf(w, text)
221 } else {
222 _, _ = fmt.Fprintln(w, text)
223 }
224 }
225 if e := w.Flush(); E.Chk(e) {
226 }
227 return formatted.String()
228 }
229 230 // argTypeHelp returns the type of provided command argument as a string in the format used by the help output. In
231 // particular, it includes the JSON type (boolean, numeric, string, array, object) along with optional and the default
232 // value if applicable.
233 func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string {
234 // Indirect the pointer if needed and track if it's an optional field.
235 fieldType := structField.Type
236 var isOptional bool
237 if fieldType.Kind() == reflect.Ptr {
238 fieldType = fieldType.Elem()
239 isOptional = true
240 }
241 // When there is a default value, it must also be a pointer due to the rules enforced by RegisterCmd.
242 if defaultVal != nil {
243 indirect := defaultVal.Elem()
244 defaultVal = &indirect
245 }
246 // Convert the field type to a JSON type.
247 details := make([]string, 0, 3)
248 details = append(details, reflectTypeToJSONType(xT, fieldType))
249 // Add optional and default value to the details if needed.
250 if isOptional {
251 details = append(details, xT("help-optional"))
252 // Add the default value if there is one. This is only checked when the field is optional since a non-optional
253 // field can't have a default value.
254 if defaultVal != nil {
255 val := defaultVal.Interface()
256 if defaultVal.Kind() == reflect.String {
257 val = fmt.Sprintf(`"%s"`, val)
258 }
259 str := fmt.Sprintf("%s=%v", xT("help-default"), val)
260 details = append(details, str)
261 }
262 } else {
263 details = append(details, xT("help-required"))
264 }
265 return strings.Join(details, ", ")
266 }
267 268 // argHelp generates and returns formatted help for the provided command.
269 func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
270 // Return now if the command has no arguments.
271 rt := rtp.Elem()
272 numFields := rt.NumField()
273 if numFields == 0 {
274 return ""
275 }
276 // Generate the help for each argument in the command. Several simplifying assumptions are made here because the
277 // RegisterCmd function has already rigorously enforced the layout.
278 args := make([]string, 0, numFields)
279 for i := 0; i < numFields; i++ {
280 rtf := rt.Field(i)
281 var defaultVal *reflect.Value
282 if defVal, ok := defaults[i]; ok {
283 defaultVal = &defVal
284 }
285 fieldName := strings.ToLower(rtf.Name)
286 helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName,
287 argTypeHelp(xT, rtf, defaultVal),
288 xT(method+"-"+fieldName),
289 )
290 args = append(args, helpText)
291 // For types which require a JSON object, or an array of JSON objects, generate the full syntax for the
292 // argument.
293 fieldType := rtf.Type
294 if fieldType.Kind() == reflect.Ptr {
295 fieldType = fieldType.Elem()
296 }
297 kind := fieldType.Kind()
298 switch kind {
299 case reflect.Struct:
300 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
301 resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
302 args = append(args, resultText)
303 case reflect.Map:
304 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
305 resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
306 args = append(args, resultText)
307 case reflect.Array, reflect.Slice:
308 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
309 if rtf.Type.Elem().Kind() == reflect.Struct {
310 resultText := resultTypeHelp(xT, fieldType,
311 fieldDescKey,
312 )
313 args = append(args, resultText)
314 }
315 }
316 }
317 // Add argument names, types, and descriptions if there are any. Use a tab writer to nicely align the help text.
318 var formatted bytes.Buffer
319 w := new(tabwriter.Writer)
320 w.Init(&formatted, 0, 4, 1, ' ', 0)
321 for _, text := range args {
322 _, _ = fmt.Fprintln(w, text)
323 }
324 if e := w.Flush(); E.Chk(e) {
325 }
326 return formatted.String()
327 }
328 329 // methodHelp generates and returns the help output for the provided command and method info. This is the main work
330 // horse for the exported MethodHelp function.
331 func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string,
332 resultTypes []interface{},
333 ) string {
334 // Start off with the method usage and help synopsis.
335 help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method),
336 xT(method+"--synopsis"),
337 )
338 // Generate the help for each argument in the command.
339 if argText := argHelp(xT, rtp, defaults, method); argText != "" {
340 help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"),
341 argText,
342 )
343 } else {
344 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"),
345 xT("help-arguments-none"),
346 )
347 }
348 // Generate the help text for each result type.
349 resultTexts := make([]string, 0, len(resultTypes))
350 for i := range resultTypes {
351 rtp := reflect.TypeOf(resultTypes[i])
352 fieldDescKey := fmt.Sprintf("%s--result%d", method, i)
353 if resultTypes[i] == nil {
354 resultText := xT("help-result-nothing")
355 resultTexts = append(resultTexts, resultText)
356 continue
357 }
358 resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey)
359 resultTexts = append(resultTexts, resultText)
360 }
361 // Add result types and descriptions. When there is more than one result type, also add the condition which triggers
362 // it.
363 if len(resultTexts) > 1 {
364 for i, resultText := range resultTexts {
365 condKey := fmt.Sprintf("%s--condition%d", method, i)
366 help += fmt.Sprintf("\n%s (%s):\n%s\n",
367 xT("help-result"), xT(condKey), resultText,
368 )
369 }
370 } else if len(resultTexts) > 0 {
371 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
372 resultTexts[0],
373 )
374 } else {
375 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
376 xT("help-result-nothing"),
377 )
378 }
379 return help
380 }
381 382 // isValidResultType returns whether the passed reflect kind is one of the acceptable types for results.
383 func isValidResultType(kind reflect.Kind) bool {
384 if isNumeric(kind) {
385 return true
386 }
387 switch kind {
388 case reflect.String, reflect.Struct, reflect.Array, reflect.Slice,
389 reflect.Bool, reflect.Map:
390 return true
391 }
392 return false
393 }
394 395 // GenerateHelp generates and returns help output for the provided method and result types given a map to provide the
396 // appropriate keys for the method synopsis, field descriptions, conditions, and result descriptions. The method must be
397 // associated with a registered type. All commands provided by this package are registered by default. The resultTypes
398 // must be pointer-to-types which represent the specific types of values the command returns. For example, if the
399 // command only returns a boolean value, there should only be a single entry of (*bool)(nil). Note that each type must
400 // be a single pointer to the type. Therefore, it is recommended to simply pass a nil pointer cast to the appropriate
401 // type as previously shown.
402 //
403 // The provided descriptions map must contain all of the keys or an error will be returned which includes the missing
404 // key, or the final missing key when there is more than one key missing. The generated help in the case of such an
405 // error will use the key in place of the description.
406 //
407 // The following outlines the required keys:
408 //
409 // "<method>--synopsis" Synopsis for the command
410 //
411 // "<method>-<lowerfieldname>" Title for each command argument
412 //
413 // "<typename>-<lowerfieldname>" Title for each object field
414 //
415 // "<method>--condition<#>" Title for each result condition
416 //
417 // "<method>--result<#>" Title for each primitive result num
418 //
419 // Notice that the "special" keys synopsis, condition<#>, and result<#> are preceded by a double dash to ensure they
420 // don't conflict with field names. The condition keys are only required when there is more than on result type, and the
421 // result key for a given result type is only required if it's not an object.
422 //
423 // For example, consider the 'help' command itself. There are two possible returns depending on the provided parameters.
424 // So, the help would be generated by calling the function as follows:
425 //
426 // GenerateHelp("help", descs, (*string)(nil), (*string)(nil)).
427 //
428 // The following keys would then be required in the provided descriptions map:
429 //
430 // "help--synopsis": "Returns a list of all commands or help for ...."
431 //
432 // "help-command": "The command to retrieve help for",
433 //
434 // "help--condition0": "no command provided"
435 //
436 // "help--condition1": "command specified"
437 //
438 // "help--result0": "List of commands"
439 //
440 // "help--result1": "Help for specified command"
441 func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) {
442 // Look up details about the provided method and error out if not registered.
443 registerLock.RLock()
444 rtp, ok := methodToConcreteType[method]
445 info := methodToInfo[method]
446 registerLock.RUnlock()
447 if !ok {
448 str := fmt.Sprintf("%q is not registered", method)
449 return "", makeError(ErrUnregisteredMethod, str)
450 }
451 // Validate each result type is a pointer to a supported type (or nil).
452 for i, resultType := range resultTypes {
453 if resultType == nil {
454 continue
455 }
456 rtp = reflect.TypeOf(resultType)
457 if rtp.Kind() != reflect.Ptr {
458 str := fmt.Sprintf("result #%d (%v) is not a pointer",
459 i, rtp.Kind(),
460 )
461 return "", makeError(ErrInvalidType, str)
462 }
463 elemKind := rtp.Elem().Kind()
464 if !isValidResultType(elemKind) {
465 str := fmt.Sprintf("result #%d (%v) is not an allowed "+
466 "type", i, elemKind,
467 )
468 return "", makeError(ErrInvalidType, str)
469 }
470 }
471 // Create a closure for the description lookup function which falls back to the base help descriptions map for
472 // unrecognized keys and tracks and missing keys.
473 var missingKey string
474 xT := func(key string) string {
475 if desc, ok := descs[key]; ok {
476 return desc
477 }
478 if desc, ok := baseHelpDescs[key]; ok {
479 return desc
480 }
481 missingKey = key
482 return key
483 }
484 // Generate and return the help for the method.
485 help := methodHelp(xT, rtp, info.defaults, method, resultTypes)
486 if missingKey != "" {
487 return help, makeError(ErrMissingDescription, missingKey)
488 }
489 return help, nil
490 }
491