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  }