forked from decred/dcrd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
register.go
293 lines (262 loc) · 8.79 KB
/
register.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright (c) 2014 The btcsuite developers
// Copyright (c) 2015 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 (
"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)
//
// 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)
}
sort.Sort(sort.StringSlice(methods))
return methods
}