lbcd/btcjson/register.go

293 lines
8.7 KiB
Go
Raw Normal View History

// Copyright (c) 2014 The btcsuite developers
Reimagine btcjson package with version 2. This commit implements a reimagining of the way the btcjson package functions based upon how the project has evolved and lessons learned while using it since it was first written. It therefore contains significant changes to the API. For now, it has been implemented in a v2 subdirectory to prevent breaking existing callers, but the ultimate goal is to update all callers to use the new version and then to replace the old API with the new one. This also removes the need for the btcws completely since those commands have been rolled in. The following is an overview of the changes and some reasoning behind why they were made: - The infrastructure has been completely changed to be reflection based instead of requiring thousands and thousands of lines of manual, and therefore error prone, marshal/unmarshal code - This makes it much easier to add new commands without making marshalling mistakes since it is simply a struct definition and a call to register that new struct (plus a trivial New<foo>Cmd function and tests, of course) - It also makes it much easier to gain a lot of information from simply looking at the struct definition which was previously not possible such as the order of the parameters, which parameters are required versus optional, and what the default values for optional parameters are - Each command now has usage flags associated with them that can be queried which are intended to allow classification of the commands such as for chain server and wallet server and websocket-only - The help infrastructure has been completely redone to provide automatic generation with caller provided description map and result types. This is in contrast to the previous method of providing the help directly which meant it would end up in the binary of anything that imported the package - Many of the structs have been renamed to use the terminology from the JSON-RPC specification: - RawCmd/Message is now only a single struct named Request to reflect the fact it is a JSON-RPC request - Error is now called RPCError to reflect the fact it is specifically an RPC error as opposed to many of the other errors that are possible - All RPC error codes except the standard JSON-RPC 2.0 errors have been converted from full structs to only codes since an audit of the codebase has shown that the messages are overridden the vast majority of the time with specifics (as they should be) and removing them also avoids the temptation to return non-specific, and therefore not as helpful, error messages - There is now an Error which provides a type assertable error with error codes so callers can better ascertain failure reasons programatically - The ID is no longer a part of the command and is instead specified at the time the command is marshalled into a JSON-RPC request. This aligns better with the way JSON-RPC functions since it is the caller who manages the ID that is sent with any given _request_, not the package - All <Foo>Cmd structs now treat non-pointers as required fields and pointers as optional fields - All New<Foo>Cmd functions now accept the exact number of parameters, with pointers to the appropriate type for optional parameters - This is preferrable to the old vararg syntax since it means the code will fail to compile if the optional arguments are changed now which helps prevent errors creep in over time from missed modifications to optional args - All of the connection related code has been completely eliminated since this package is not intended to used a client, rather it is intended to provide the infrastructure needed to marshal/unmarshal Bitcoin-specific JSON-RPC requests and replies from static types - The btcrpcclient package provides a robust client with connection management and higher-level types that in turn uses the primitives provided by this package - Even if the caller does not wish to use btcrpcclient for some reason, they should still be responsible for connection management since they might want to use any number of connection features which the package would not necessarily support - Synced a few of the commands that have added new optional fields that have since been added to Bitcoin Core - Includes all of the commands and notifications that were previously in btcws - Now provides 100% test coverage with parallel tests - The code is completely golint and go vet clean This has the side effect of addressing nearly everything in, and therefore closes #26. Also fixes #18 and closes #19.
2014-12-31 08:05:03 +01:00
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package btcjson
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"sync"
)
// UsageFlag define flags that specify additional properties about the
// circumstances under which a command can be used.
type UsageFlag uint32
const (
// UFWalletOnly indicates that the command can only be used with an RPC
// server that supports wallet commands.
UFWalletOnly UsageFlag = 1 << iota
// UFWebsocketOnly indicates that the command can only be used when
// communicating with an RPC server over websockets. This typically
// applies to notifications and notification registration functions
// since neiher makes since when using a single-shot HTTP-POST request.
UFWebsocketOnly
// UFNotification indicates that the command is actually a notification.
// This means when it is marshalled, the ID must be nil.
UFNotification
// highestUsageFlagBit is the maximum usage flag bit and is used in the
// stringer and tests to ensure all of the above constants have been
// tested.
highestUsageFlagBit
)
// Map of UsageFlag values back to their constant names for pretty printing.
var usageFlagStrings = map[UsageFlag]string{
UFWalletOnly: "UFWalletOnly",
UFWebsocketOnly: "UFWebsocketOnly",
UFNotification: "UFNotification",
}
// String returns the UsageFlag in human-readable form.
func (fl UsageFlag) String() string {
// No flags are set.
if fl == 0 {
return "0x0"
}
// Add individual bit flags.
s := ""
for flag := UFWalletOnly; flag < highestUsageFlagBit; flag <<= 1 {
if fl&flag == flag {
s += usageFlagStrings[flag] + "|"
fl -= flag
}
}
// Add remaining value as raw hex.
s = strings.TrimRight(s, "|")
if fl != 0 {
s += "|0x" + strconv.FormatUint(uint64(fl), 16)
}
s = strings.TrimLeft(s, "|")
return s
}
// methodInfo keeps track of information about each registered method such as
// the parameter information.
type methodInfo struct {
maxParams int
numReqParams int
numOptParams int
defaults map[int]reflect.Value
flags UsageFlag
usage string
}
var (
// These fields are used to map the registered types to method names.
registerLock sync.RWMutex
methodToConcreteType = make(map[string]reflect.Type)
methodToInfo = make(map[string]methodInfo)
concreteTypeToMethod = make(map[reflect.Type]string)
)
// baseKindString returns the base kind for a given reflect.Type after
// indirecting through all pointers.
func baseKindString(rt reflect.Type) string {
numIndirects := 0
for rt.Kind() == reflect.Ptr {
numIndirects++
rt = rt.Elem()
}
return fmt.Sprintf("%s%s", strings.Repeat("*", numIndirects), rt.Kind())
}
// isAcceptableKind returns whether or not the passed field type is a supported
// type. It is called after the first pointer indirection, so further pointers
// are not supported.
func isAcceptableKind(kind reflect.Kind) bool {
switch kind {
case reflect.Chan:
fallthrough
case reflect.Complex64:
fallthrough
case reflect.Complex128:
fallthrough
case reflect.Func:
fallthrough
case reflect.Ptr:
fallthrough
case reflect.Interface:
return false
}
return true
}
// RegisterCmd registers a new command that will automatically marshal to and
// from JSON-RPC with full type checking and positional parameter support. It
// also accepts usage flags which identify the circumstances under which the
// command can be used.
//
// This package automatically registers all of the exported commands by default
// using this function, however it is also exported so callers can easily
// register custom types.
//
// The type format is very strict since it needs to be able to automatically
// marshal to and from JSON-RPC 1.0. The following enumerates the requirements:
//
// - The provided command must be a single pointer to a struct
// - All fields must be exported
// - The order of the positional parameters in the marshalled JSON will be in
// the same order as declared in the struct definition
// - Struct embedding is not supported
// - Struct fields may NOT be channels, functions, complex, or interface
// - A field in the provided struct with a pointer is treated as optional
// - Multiple indirections (i.e **int) are not supported
// - Once the first optional field (pointer) is encountered, the remaining
// fields must also be optional fields (pointers) as required by positional
// params
// - A field that has a 'jsonrpcdefault' struct tag must be an optional field
// (pointer)
Reimagine btcjson package with version 2. This commit implements a reimagining of the way the btcjson package functions based upon how the project has evolved and lessons learned while using it since it was first written. It therefore contains significant changes to the API. For now, it has been implemented in a v2 subdirectory to prevent breaking existing callers, but the ultimate goal is to update all callers to use the new version and then to replace the old API with the new one. This also removes the need for the btcws completely since those commands have been rolled in. The following is an overview of the changes and some reasoning behind why they were made: - The infrastructure has been completely changed to be reflection based instead of requiring thousands and thousands of lines of manual, and therefore error prone, marshal/unmarshal code - This makes it much easier to add new commands without making marshalling mistakes since it is simply a struct definition and a call to register that new struct (plus a trivial New<foo>Cmd function and tests, of course) - It also makes it much easier to gain a lot of information from simply looking at the struct definition which was previously not possible such as the order of the parameters, which parameters are required versus optional, and what the default values for optional parameters are - Each command now has usage flags associated with them that can be queried which are intended to allow classification of the commands such as for chain server and wallet server and websocket-only - The help infrastructure has been completely redone to provide automatic generation with caller provided description map and result types. This is in contrast to the previous method of providing the help directly which meant it would end up in the binary of anything that imported the package - Many of the structs have been renamed to use the terminology from the JSON-RPC specification: - RawCmd/Message is now only a single struct named Request to reflect the fact it is a JSON-RPC request - Error is now called RPCError to reflect the fact it is specifically an RPC error as opposed to many of the other errors that are possible - All RPC error codes except the standard JSON-RPC 2.0 errors have been converted from full structs to only codes since an audit of the codebase has shown that the messages are overridden the vast majority of the time with specifics (as they should be) and removing them also avoids the temptation to return non-specific, and therefore not as helpful, error messages - There is now an Error which provides a type assertable error with error codes so callers can better ascertain failure reasons programatically - The ID is no longer a part of the command and is instead specified at the time the command is marshalled into a JSON-RPC request. This aligns better with the way JSON-RPC functions since it is the caller who manages the ID that is sent with any given _request_, not the package - All <Foo>Cmd structs now treat non-pointers as required fields and pointers as optional fields - All New<Foo>Cmd functions now accept the exact number of parameters, with pointers to the appropriate type for optional parameters - This is preferrable to the old vararg syntax since it means the code will fail to compile if the optional arguments are changed now which helps prevent errors creep in over time from missed modifications to optional args - All of the connection related code has been completely eliminated since this package is not intended to used a client, rather it is intended to provide the infrastructure needed to marshal/unmarshal Bitcoin-specific JSON-RPC requests and replies from static types - The btcrpcclient package provides a robust client with connection management and higher-level types that in turn uses the primitives provided by this package - Even if the caller does not wish to use btcrpcclient for some reason, they should still be responsible for connection management since they might want to use any number of connection features which the package would not necessarily support - Synced a few of the commands that have added new optional fields that have since been added to Bitcoin Core - Includes all of the commands and notifications that were previously in btcws - Now provides 100% test coverage with parallel tests - The code is completely golint and go vet clean This has the side effect of addressing nearly everything in, and therefore closes #26. Also fixes #18 and closes #19.
2014-12-31 08:05:03 +01:00
//
// NOTE: This function only needs to be able to examine the structure of the
// passed struct, so it does not need to be an actual instance. Therefore, it
// is recommended to simply pass a nil pointer cast to the appropriate type.
// For example, (*FooCmd)(nil).
func RegisterCmd(method string, cmd interface{}, flags UsageFlag) error {
registerLock.Lock()
defer registerLock.Unlock()
if _, ok := methodToConcreteType[method]; ok {
str := fmt.Sprintf("method %q is already registered", method)
return makeError(ErrDuplicateMethod, str)
}
// Ensure that no unrecognized flag bits were specified.
if ^(highestUsageFlagBit-1)&flags != 0 {
str := fmt.Sprintf("invalid usage flags specified for method "+
"%s: %v", method, flags)
return makeError(ErrInvalidUsageFlags, str)
}
rtp := reflect.TypeOf(cmd)
if rtp.Kind() != reflect.Ptr {
str := fmt.Sprintf("type must be *struct not '%s (%s)'", rtp,
rtp.Kind())
return makeError(ErrInvalidType, str)
}
rt := rtp.Elem()
if rt.Kind() != reflect.Struct {
str := fmt.Sprintf("type must be *struct not '%s (*%s)'",
rtp, rt.Kind())
return makeError(ErrInvalidType, str)
}
// Enumerate the struct fields to validate them and gather parameter
// information.
numFields := rt.NumField()
numOptFields := 0
defaults := make(map[int]reflect.Value)
for i := 0; i < numFields; i++ {
rtf := rt.Field(i)
if rtf.Anonymous {
str := fmt.Sprintf("embedded fields are not supported "+
"(field name: %q)", rtf.Name)
return makeError(ErrEmbeddedType, str)
}
if rtf.PkgPath != "" {
str := fmt.Sprintf("unexported fields are not supported "+
"(field name: %q)", rtf.Name)
return makeError(ErrUnexportedField, str)
}
// Disallow types that can't be JSON encoded. Also, determine
// if the field is optional based on it being a pointer.
var isOptional bool
switch kind := rtf.Type.Kind(); kind {
case reflect.Ptr:
isOptional = true
kind = rtf.Type.Elem().Kind()
fallthrough
default:
if !isAcceptableKind(kind) {
str := fmt.Sprintf("unsupported field type "+
"'%s (%s)' (field name %q)", rtf.Type,
baseKindString(rtf.Type), rtf.Name)
return makeError(ErrUnsupportedFieldType, str)
}
}
// Count the optional fields and ensure all fields after the
// first optional field are also optional.
if isOptional {
numOptFields++
} else {
if numOptFields > 0 {
str := fmt.Sprintf("all fields after the first "+
"optional field must also be optional "+
"(field name %q)", rtf.Name)
return makeError(ErrNonOptionalField, str)
}
}
// Ensure the default value can be unsmarshalled into the type
// and that defaults are only specified for optional fields.
if tag := rtf.Tag.Get("jsonrpcdefault"); tag != "" {
if !isOptional {
str := fmt.Sprintf("required fields must not "+
"have a default specified (field name "+
"%q)", rtf.Name)
return makeError(ErrNonOptionalDefault, str)
}
rvf := reflect.New(rtf.Type.Elem())
err := json.Unmarshal([]byte(tag), rvf.Interface())
if err != nil {
str := fmt.Sprintf("default value of %q is "+
"the wrong type (field name %q)", tag,
rtf.Name)
return makeError(ErrMismatchedDefault, str)
}
defaults[i] = rvf
}
}
// Update the registration maps.
methodToConcreteType[method] = rtp
methodToInfo[method] = methodInfo{
maxParams: numFields,
numReqParams: numFields - numOptFields,
numOptParams: numOptFields,
defaults: defaults,
flags: flags,
}
concreteTypeToMethod[rtp] = method
return nil
}
// MustRegisterCmd performs the same function as RegisterCmd except it panics
// if there is an error. This should only be called from package init
// functions.
func MustRegisterCmd(method string, cmd interface{}, flags UsageFlag) {
if err := RegisterCmd(method, cmd, flags); err != nil {
panic(fmt.Sprintf("failed to register type %q: %v\n", method,
err))
}
}
// RegisteredCmdMethods returns a sorted list of methods for all registered
// commands.
func RegisteredCmdMethods() []string {
registerLock.Lock()
defer registerLock.Unlock()
methods := make([]string, 0, len(methodToInfo))
for k := range methodToInfo {
methods = append(methods, k)
}
2020-05-13 14:58:39 +02:00
sort.Strings(methods)
Reimagine btcjson package with version 2. This commit implements a reimagining of the way the btcjson package functions based upon how the project has evolved and lessons learned while using it since it was first written. It therefore contains significant changes to the API. For now, it has been implemented in a v2 subdirectory to prevent breaking existing callers, but the ultimate goal is to update all callers to use the new version and then to replace the old API with the new one. This also removes the need for the btcws completely since those commands have been rolled in. The following is an overview of the changes and some reasoning behind why they were made: - The infrastructure has been completely changed to be reflection based instead of requiring thousands and thousands of lines of manual, and therefore error prone, marshal/unmarshal code - This makes it much easier to add new commands without making marshalling mistakes since it is simply a struct definition and a call to register that new struct (plus a trivial New<foo>Cmd function and tests, of course) - It also makes it much easier to gain a lot of information from simply looking at the struct definition which was previously not possible such as the order of the parameters, which parameters are required versus optional, and what the default values for optional parameters are - Each command now has usage flags associated with them that can be queried which are intended to allow classification of the commands such as for chain server and wallet server and websocket-only - The help infrastructure has been completely redone to provide automatic generation with caller provided description map and result types. This is in contrast to the previous method of providing the help directly which meant it would end up in the binary of anything that imported the package - Many of the structs have been renamed to use the terminology from the JSON-RPC specification: - RawCmd/Message is now only a single struct named Request to reflect the fact it is a JSON-RPC request - Error is now called RPCError to reflect the fact it is specifically an RPC error as opposed to many of the other errors that are possible - All RPC error codes except the standard JSON-RPC 2.0 errors have been converted from full structs to only codes since an audit of the codebase has shown that the messages are overridden the vast majority of the time with specifics (as they should be) and removing them also avoids the temptation to return non-specific, and therefore not as helpful, error messages - There is now an Error which provides a type assertable error with error codes so callers can better ascertain failure reasons programatically - The ID is no longer a part of the command and is instead specified at the time the command is marshalled into a JSON-RPC request. This aligns better with the way JSON-RPC functions since it is the caller who manages the ID that is sent with any given _request_, not the package - All <Foo>Cmd structs now treat non-pointers as required fields and pointers as optional fields - All New<Foo>Cmd functions now accept the exact number of parameters, with pointers to the appropriate type for optional parameters - This is preferrable to the old vararg syntax since it means the code will fail to compile if the optional arguments are changed now which helps prevent errors creep in over time from missed modifications to optional args - All of the connection related code has been completely eliminated since this package is not intended to used a client, rather it is intended to provide the infrastructure needed to marshal/unmarshal Bitcoin-specific JSON-RPC requests and replies from static types - The btcrpcclient package provides a robust client with connection management and higher-level types that in turn uses the primitives provided by this package - Even if the caller does not wish to use btcrpcclient for some reason, they should still be responsible for connection management since they might want to use any number of connection features which the package would not necessarily support - Synced a few of the commands that have added new optional fields that have since been added to Bitcoin Core - Includes all of the commands and notifications that were previously in btcws - Now provides 100% test coverage with parallel tests - The code is completely golint and go vet clean This has the side effect of addressing nearly everything in, and therefore closes #26. Also fixes #18 and closes #19.
2014-12-31 08:05:03 +01:00
return methods
}