help.go raw

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