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