mirror of
https://github.com/btcsuite/btcd.git
synced 2025-01-18 21:32:30 +01:00
ba5407615d
The doc formatting changes introduced in the recent go version is increasing the diff for all of the new commits. Formatting it all in this commit will help the readability of future PRs by reducing the diff.
563 lines
18 KiB
Go
563 lines
18 KiB
Go
// 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)
|
|
results = append(results, fieldExamples...)
|
|
} 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 descriptions 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
|
|
}
|