dcrd/dcrjson/cmdinfo.go
Josh Rickmar b112df6417 dcrjson: Introduce v3 and move types to module.
This commit introduces a new major version of the dcrjson module which
removes all dcrd RPC type support, instead focusing only on method and
type registration.  The dcrd methods and types are moved to the
github.com/decred/dcrd/rpc/jsonrpc/types module.

In order to improve backwards compatibility with dcrjson/v2, the API
has been modified to register methods as interface{} instead of
string.  This allows different method string types to be used to key
parameter types during registration and lookup, and will allow
dcrjson/v2 to forward registrations of RPC methods to v3 without
causing duplicate registrations errors for incompatible types.

With the introduction of the new types package, the RPC API has been
modified to replace concatenated hash blobs to JSON string arrays of
hash strings.  The RPC API major version is bumped to reflect this
change.

A future update to dcrjson/v2 will add additional registrations,
forwarding the registrations to v3 and replacing command types with
type aliases where possible.  Unfortunately, this can not be done
entirely in a single commit due to dcrjson/v2 and dcrjson/v3 sharing
the same directory in the source tree, and a branch will need to be
used for this update.

Module replacements are temporarily used to enable the changes for the
main module, including dcrctl.  After the aforementioned update to
dcrjson/v2 and a forthcoming update to dcrwallet's RPC types package,
these replacements will be removed.
2019-07-19 08:49:49 -05:00

251 lines
7.9 KiB
Go

// Copyright (c) 2015 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package dcrjson
import (
"fmt"
"reflect"
"strings"
)
// CmdMethod returns the method for the passed command. The provided command
// type must be a registered type. All commands provided by this package are
// registered by default.
func CmdMethod(cmd interface{}) (string, error) {
// Look up the cmd type and error out if not registered.
rt := reflect.TypeOf(cmd)
registerLock.RLock()
method, ok := concreteTypeToMethod[rt]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%T is not registered", cmd)
return "", makeError(ErrUnregisteredMethod, str)
}
return reflect.ValueOf(method).String(), nil
}
// MethodUsageFlags returns the usage flags for the passed command method. The
// provided method must be associated with a registered type. All commands
// provided by this package are registered by default.
func MethodUsageFlags(method interface{}) (UsageFlag, error) {
// Look up details about the provided method and error out if not
// registered.
registerLock.RLock()
info, ok := methodToInfo[method]
registerLock.RUnlock()
if !ok {
str := fmt.Sprintf("%q is not registered", method)
return 0, makeError(ErrUnregisteredMethod, str)
}
return info.flags, nil
}
// subStructUsage returns a string for use in the one-line usage for the given
// sub struct. Note that this is specifically for fields which consist of
// structs (or an array/slice of structs) as opposed to the top-level command
// struct.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func subStructUsage(structType reflect.Type) string {
numFields := structType.NumField()
fieldUsages := make([]string, 0, numFields)
for i := 0; i < structType.NumField(); i++ {
rtf := structType.Field(i)
// When the field has a jsonrpcusage struct tag specified use
// that instead of automatically generating it.
if tag := rtf.Tag.Get("jsonrpcusage"); tag != "" {
fieldUsages = append(fieldUsages, tag)
continue
}
// Create the name/value entry for the field while considering
// the type of the field. Not all possible types are covered
// here and when one of the types not specifically covered is
// encountered, the field name is simply reused for the value.
fieldName := strings.ToLower(rtf.Name)
fieldValue := fieldName
fieldKind := rtf.Type.Kind()
switch {
case isNumeric(fieldKind):
if fieldKind == reflect.Float32 || fieldKind == reflect.Float64 {
fieldValue = "n.nnn"
} else {
fieldValue = "n"
}
case fieldKind == reflect.String:
fieldValue = `"value"`
case fieldKind == reflect.Struct:
fieldValue = subStructUsage(rtf.Type)
case fieldKind == reflect.Array || fieldKind == reflect.Slice:
fieldValue = subArrayUsage(rtf.Type, fieldName)
}
usage := fmt.Sprintf("%q:%s", fieldName, fieldValue)
fieldUsages = append(fieldUsages, usage)
}
return fmt.Sprintf("{%s}", strings.Join(fieldUsages, ","))
}
// subArrayUsage returns a string for use in the one-line usage for the given
// array or slice. It also contains logic to convert plural field names to
// singular so the generated usage string reads better.
func subArrayUsage(arrayType reflect.Type, fieldName string) string {
// Convert plural field names to singular. Only works for English.
singularFieldName := fieldName
if strings.HasSuffix(fieldName, "ies") {
singularFieldName = strings.TrimSuffix(fieldName, "ies")
singularFieldName = singularFieldName + "y"
} else if strings.HasSuffix(fieldName, "es") {
singularFieldName = strings.TrimSuffix(fieldName, "es")
} else if strings.HasSuffix(fieldName, "s") {
singularFieldName = strings.TrimSuffix(fieldName, "s")
}
elemType := arrayType.Elem()
switch elemType.Kind() {
case reflect.String:
return fmt.Sprintf("[%q,...]", singularFieldName)
case reflect.Struct:
return fmt.Sprintf("[%s,...]", subStructUsage(elemType))
}
// Fall back to simply showing the field name in array syntax.
return fmt.Sprintf(`[%s,...]`, singularFieldName)
}
// fieldUsage returns a string for use in the one-line usage for the struct
// field of a command.
//
// Any fields that include a jsonrpcusage struct tag will use that instead of
// being automatically generated.
func fieldUsage(structField reflect.StructField, defaultVal *reflect.Value) string {
// When the field has a jsonrpcusage struct tag specified use that
// instead of automatically generating it.
if tag := structField.Tag.Get("jsonrpcusage"); tag != "" {
return tag
}
// Indirect the pointer if needed.
fieldType := structField.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
// 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
}
// Handle certain types uniquely to provide nicer usage.
fieldName := strings.ToLower(structField.Name)
switch fieldType.Kind() {
case reflect.String:
if defaultVal != nil {
return fmt.Sprintf("%s=%q", fieldName,
defaultVal.Interface())
}
return fmt.Sprintf("%q", fieldName)
case reflect.Array, reflect.Slice:
return subArrayUsage(fieldType, fieldName)
case reflect.Struct:
return subStructUsage(fieldType)
}
// Simply return the field name when none of the above special cases
// apply.
if defaultVal != nil {
return fmt.Sprintf("%s=%v", fieldName, defaultVal.Interface())
}
return fieldName
}
// methodUsageText returns a one-line usage string for the provided command and
// method info. This is the main work horse for the exported MethodUsageText
// function.
func methodUsageText(rtp reflect.Type, defaults map[int]reflect.Value, method interface{}) string {
// Generate the individual usage for each field in the command. Several
// simplifying assumptions are made here because the RegisterCmd
// function has already rigorously enforced the layout.
rt := rtp.Elem()
numFields := rt.NumField()
reqFieldUsages := make([]string, 0, numFields)
optFieldUsages := make([]string, 0, numFields)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
var isOptional bool
if kind := rtf.Type.Kind(); kind == reflect.Ptr {
isOptional = true
}
var defaultVal *reflect.Value
if defVal, ok := defaults[i]; ok {
defaultVal = &defVal
}
// Add human-readable usage to the appropriate slice that is
// later used to generate the one-line usage.
usage := fieldUsage(rtf, defaultVal)
if isOptional {
optFieldUsages = append(optFieldUsages, usage)
} else {
reqFieldUsages = append(reqFieldUsages, usage)
}
}
// Generate and return the one-line usage string.
usageStr := reflect.ValueOf(method).String()
if len(reqFieldUsages) > 0 {
usageStr += " " + strings.Join(reqFieldUsages, " ")
}
if len(optFieldUsages) > 0 {
usageStr += fmt.Sprintf(" (%s)", strings.Join(optFieldUsages, " "))
}
return usageStr
}
// MethodUsageText returns a one-line usage string for the provided method. The
// provided method must be associated with a registered type. All commands
// provided by this package are registered by default.
func MethodUsageText(method 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)
}
// When the usage for this method has already been generated, simply
// return it.
if info.usage != "" {
return info.usage, nil
}
// Generate and store the usage string for future calls and return it.
usage := methodUsageText(rtp, info.defaults, method)
registerLock.Lock()
info.usage = usage
methodToInfo[method] = info
registerLock.Unlock()
return usage, nil
}