cmdinfo.go raw

   1  package btcjson
   2  
   3  import (
   4  	"fmt"
   5  	"reflect"
   6  	"strings"
   7  )
   8  
   9  // CmdMethod returns the method for the passed command. The provided command type must be a registered type. All
  10  // commands provided by this package are registered by default.
  11  func CmdMethod(cmd interface{}) (string, error) {
  12  	// Look up the cmd type and error out if not registered.
  13  	rt := reflect.TypeOf(cmd)
  14  	registerLock.RLock()
  15  	method, ok := concreteTypeToMethod[rt]
  16  	registerLock.RUnlock()
  17  	if !ok {
  18  		str := fmt.Sprintf("%q is not registered", method)
  19  		return "", makeError(ErrUnregisteredMethod, str)
  20  	}
  21  	return method, nil
  22  }
  23  
  24  // MethodUsageFlags returns the usage flags for the passed command method. The provided method must be associated with a
  25  // registered type. All commands provided by this package are registered by default.
  26  func MethodUsageFlags(method string) (UsageFlag, error) {
  27  	// Look up details about the provided method and error out if not registered.
  28  	registerLock.RLock()
  29  	info, ok := methodToInfo[method]
  30  	registerLock.RUnlock()
  31  	if !ok {
  32  		str := fmt.Sprintf("%q is not registered", method)
  33  		return 0, makeError(ErrUnregisteredMethod, str)
  34  	}
  35  	return info.flags, nil
  36  }
  37  
  38  // subStructUsage returns a string for use in the one-line usage for the given sub struct. Note that this is
  39  // specifically for fields which consist of structs (or an array/slice of structs) as opposed to the top-level command
  40  // struct. Any fields that include a jsonrpcusage struct tag will use that instead of being automatically generated.
  41  func subStructUsage(structType reflect.Type) string {
  42  	numFields := structType.NumField()
  43  	fieldUsages := make([]string, 0, numFields)
  44  	for i := 0; i < structType.NumField(); i++ {
  45  		rtf := structType.Field(i)
  46  		// When the field has a jsonrpcusage struct tag specified use that instead of automatically generating it.
  47  		if tag := rtf.Tag.Get("jsonrpcusage"); tag != "" {
  48  			fieldUsages = append(fieldUsages, tag)
  49  			continue
  50  		}
  51  		// Create the name/value entry for the field while considering the type of the field. Not all possible types are
  52  		// covered here and when one of the types not specifically covered is encountered, the field name is simply
  53  		// reused for the value.
  54  		fieldName := strings.ToLower(rtf.Name)
  55  		fieldValue := fieldName
  56  		fieldKind := rtf.Type.Kind()
  57  		switch {
  58  		case isNumeric(fieldKind):
  59  			if fieldKind == reflect.Float32 || fieldKind == reflect.Float64 {
  60  				fieldValue = "n.nnn"
  61  			} else {
  62  				fieldValue = "n"
  63  			}
  64  		case fieldKind == reflect.String:
  65  			fieldValue = `"value"`
  66  		case fieldKind == reflect.Struct:
  67  			fieldValue = subStructUsage(rtf.Type)
  68  		case fieldKind == reflect.Array || fieldKind == reflect.Slice:
  69  			fieldValue = subArrayUsage(rtf.Type, fieldName)
  70  		}
  71  		usage := fmt.Sprintf("%q:%s", fieldName, fieldValue)
  72  		fieldUsages = append(fieldUsages, usage)
  73  	}
  74  	return fmt.Sprintf("{%s}", strings.Join(fieldUsages, ","))
  75  }
  76  
  77  // subArrayUsage returns a string for use in the one-line usage for the given array or slice. It also contains logic to
  78  // convert plural field names to singular so the generated usage string reads better.
  79  func subArrayUsage(arrayType reflect.Type, fieldName string) string {
  80  	// Convert plural field names to singular.  Only works for English.
  81  	singularFieldName := fieldName
  82  	if strings.HasSuffix(fieldName, "ies") {
  83  		singularFieldName = strings.TrimSuffix(fieldName, "ies")
  84  		singularFieldName = singularFieldName + "y"
  85  	} else if strings.HasSuffix(fieldName, "es") {
  86  		singularFieldName = strings.TrimSuffix(fieldName, "es")
  87  	} else if strings.HasSuffix(fieldName, "s") {
  88  		singularFieldName = strings.TrimSuffix(fieldName, "s")
  89  	}
  90  	elemType := arrayType.Elem()
  91  	switch elemType.Kind() {
  92  	case reflect.String:
  93  		return fmt.Sprintf("[%q,...]", singularFieldName)
  94  	case reflect.Struct:
  95  		return fmt.Sprintf("[%s,...]", subStructUsage(elemType))
  96  	}
  97  	// Fall back to simply showing the field name in array syntax.
  98  	return fmt.Sprintf(`[%s,...]`, singularFieldName)
  99  }
 100  
 101  // fieldUsage returns a string for use in the one-line usage for the struct field of a command. Any fields that include
 102  // a jsonrpcusage struct tag will use that instead of being automatically generated.
 103  func fieldUsage(structField reflect.StructField, defaultVal *reflect.Value) string {
 104  	// When the field has a jsonrpcusage struct tag specified use that instead of automatically generating it.
 105  	if tag := structField.Tag.Get("jsonrpcusage"); tag != "" {
 106  		return tag
 107  	}
 108  	// Indirect the pointer if needed.
 109  	fieldType := structField.Type
 110  	if fieldType.Kind() == reflect.Ptr {
 111  		fieldType = fieldType.Elem()
 112  	}
 113  	// When there is a default value, it must also be a pointer due to the rules enforced by RegisterCmd.
 114  	if defaultVal != nil {
 115  		indirect := defaultVal.Elem()
 116  		defaultVal = &indirect
 117  	}
 118  	// Handle certain types uniquely to provide nicer usage.
 119  	fieldName := strings.ToLower(structField.Name)
 120  	switch fieldType.Kind() {
 121  	case reflect.String:
 122  		if defaultVal != nil {
 123  			return fmt.Sprintf("%s=%q", fieldName,
 124  				defaultVal.Interface(),
 125  			)
 126  		}
 127  		return fmt.Sprintf("%q", fieldName)
 128  	case reflect.Array, reflect.Slice:
 129  		return subArrayUsage(fieldType, fieldName)
 130  	case reflect.Struct:
 131  		return subStructUsage(fieldType)
 132  	}
 133  	// Simply return the field name when none of the above special cases apply.
 134  	if defaultVal != nil {
 135  		return fmt.Sprintf("%s=%v", fieldName, defaultVal.Interface())
 136  	}
 137  	return fieldName
 138  }
 139  
 140  // methodUsageText returns a one-line usage string for the provided command and method info. This is the main work horse
 141  // for the exported MethodUsageText function.
 142  func methodUsageText(rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
 143  	// Generate the individual usage for each field in the command. Several simplifying assumptions are made here
 144  	// because the RegisterCmd function has already rigorously enforced the layout.
 145  	rt := rtp.Elem()
 146  	numFields := rt.NumField()
 147  	reqFieldUsages := make([]string, 0, numFields)
 148  	optFieldUsages := make([]string, 0, numFields)
 149  	for i := 0; i < numFields; i++ {
 150  		rtf := rt.Field(i)
 151  		var isOptional bool
 152  		if kind := rtf.Type.Kind(); kind == reflect.Ptr {
 153  			isOptional = true
 154  		}
 155  		var defaultVal *reflect.Value
 156  		if defVal, ok := defaults[i]; ok {
 157  			defaultVal = &defVal
 158  		}
 159  		// Add human-readable usage to the appropriate slice that is later used to generate the one-line usage.
 160  		usage := fieldUsage(rtf, defaultVal)
 161  		if isOptional {
 162  			optFieldUsages = append(optFieldUsages, usage)
 163  		} else {
 164  			reqFieldUsages = append(reqFieldUsages, usage)
 165  		}
 166  	}
 167  	// Generate and return the one-line usage string.
 168  	usageStr := method
 169  	if len(reqFieldUsages) > 0 {
 170  		usageStr += " " + strings.Join(reqFieldUsages, " ")
 171  	}
 172  	if len(optFieldUsages) > 0 {
 173  		usageStr += fmt.Sprintf(" (%s)", strings.Join(optFieldUsages, " "))
 174  	}
 175  	return usageStr
 176  }
 177  
 178  // MethodUsageText returns a one-line usage string for the provided method. The provided method must be associated with
 179  // a registered type. All commands provided by this package are registered by default.
 180  func MethodUsageText(method string) (string, error) {
 181  	// Look up details about the provided method and error out if not registered.
 182  	registerLock.RLock()
 183  	rtp, ok := methodToConcreteType[method]
 184  	info := methodToInfo[method]
 185  	registerLock.RUnlock()
 186  	if !ok {
 187  		str := fmt.Sprintf("%q is not registered", method)
 188  		return "", makeError(ErrUnregisteredMethod, str)
 189  	}
 190  	// When the usage for this method has already been generated, simply return it.
 191  	if info.usage != "" {
 192  		return info.usage, nil
 193  	}
 194  	// Generate and store the usage string for future calls and return it.
 195  	usage := methodUsageText(rtp, info.defaults, method)
 196  	registerLock.Lock()
 197  	info.usage = usage
 198  	methodToInfo[method] = info
 199  	registerLock.Unlock()
 200  	return usage, nil
 201  }
 202