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  }