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