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 // 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 } 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 ) var ( concreteTypeToMethod = make(map[reflect.Type]string) methodToConcreteType = make(map[string]reflect.Type) methodToInfo = make(map[string]MethodInfo) // These fields are used to map the registered types to method names. registerLock sync.RWMutex // Map of UsageFlag values back to their constant names for pretty printing. 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 } // 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 e := RegisterCmd(method, cmd, flags); E.Chk(e) { panic(fmt.Sprintf("failed to register type %q: %v\n", method, e, ), ) } RegisteredCommands[method] = cmd } var RegisteredCommands = make(map[string]interface{}) /* 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 netparams - A field that has a 'jsonrpcdefault' struct tag must be an optional field (pointer) 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) (e 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 unmarshalled 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()) e := json.Unmarshal([]byte(tag), rvf.Interface()) if e != nil { E.Ln(e) 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 } // 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) } sort.Strings(methods) return methods } // 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 }