OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library json_rpc_2.server; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection'; | |
9 import 'dart:convert'; | |
10 | |
11 import 'package:stack_trace/stack_trace.dart'; | |
12 | |
13 import '../error_code.dart' as error_code; | |
14 import 'exception.dart'; | |
15 import 'parameters.dart'; | |
16 import 'utils.dart'; | |
17 | |
18 /// A JSON-RPC 2.0 server. | |
19 /// | |
20 /// A server exposes methods that are called by requests, to which it provides | |
21 /// responses. Methods can be registered using [registerMethod] and | |
22 /// [registerFallback]. Requests can be handled using [handleRequest] and | |
23 /// [parseRequest]. | |
24 class Server { | |
Bob Nystrom
2014/03/20 18:25:58
I feel like "Server" will lead people to believe t
nweiz
2014/03/20 22:55:41
I think it's most important to follow the spec's n
| |
25 /// The methods registered for this server. | |
26 final _methods = new Map<String, Function>(); | |
27 | |
28 /// The fallback methods for this server. | |
29 /// | |
30 /// These are tried in order until one of their [_Fallback.test] functions | |
31 /// returns `true`. | |
32 final _fallbacks = new Queue<_Fallback>(); | |
33 | |
34 Server(); | |
35 | |
36 /// Registers a method named [name] on this server. | |
37 /// | |
38 /// [callback] can take either zero or one arguments. If it takes zero, any | |
39 /// requests for that method that include parameters will be rejected. If it | |
40 /// takes one, it will be passed a [Parameters] object. | |
41 /// | |
42 /// [callback] can return either a JSON-serializable object or a Future that | |
43 /// completes to a JSON-serializable object. Any errors in [callback] will be | |
44 /// reported to the client as JSON-RPC 2.0 errors. | |
45 void registerMethod(String name, Function callback) { | |
46 if (_methods.containsKey(name)) { | |
47 throw new ArgumentError('There\'s already a method named "$name".'); | |
Bob Nystrom
2014/03/20 18:25:58
Would be good to support either explicit unregistr
nweiz
2014/03/20 22:55:41
I don't want to encourage people to design APIs wh
| |
48 } | |
49 | |
50 _methods[name] = callback; | |
51 } | |
52 | |
53 /// Registers a fallback method on this server. | |
54 /// | |
55 /// A server may have any number of fallback methods. When a request comes in | |
56 /// that doesn't match any named methods, each fallback is tried in order by | |
57 /// calling its [test] callback. The first one to return `true` will handle | |
58 /// the request. | |
59 /// | |
60 /// [callback] can return either a JSON-serializable object or a Future that | |
61 /// completes to a JSON-serializable object. Any errors in [callback] will be | |
62 /// reported to the client as JSON-RPC 2.0 errors. [callback] may send custom | |
63 /// errors by throwing an [RpcException]. | |
64 void registerFallback(callback(Parameters parameters), | |
65 {bool test(String name)}) { | |
Bob Nystrom
2014/03/20 18:25:58
Only passing the method name to test() is pretty l
nweiz
2014/03/20 22:55:41
Done. I made it check for a METHOD_NOT_FOUND error
| |
66 if (test == null) test = (_) => true; | |
67 _fallbacks.add(new _Fallback(callback, test)); | |
68 } | |
69 | |
70 /// Handle a request that's already been parsed from JSON. | |
71 /// | |
72 /// [request] is expected to be a JSON-serializable object representing a | |
73 /// request sent by a client. This calls the appropriate method or methods for | |
74 /// handling that request and returns a JSON-serializable response, or `null` | |
75 /// if no response should be sent. [callback] may send custom | |
76 /// errors by throwing an [RpcException]. | |
77 Future handleRequest(request) { | |
78 return syncFuture(() { | |
79 if (request is! List) return _handleSingleRequest(request); | |
Bob Nystrom
2014/03/20 18:25:58
Why support a list of requests? It seems like this
nweiz
2014/03/20 22:55:41
It's in the spec: http://www.jsonrpc.org/specifica
Bob Nystrom
2014/03/21 00:36:02
Well how about that.
| |
80 if (request.isEmpty) { | |
81 return new RpcException(error_code.INVALID_REQUEST, 'A batch must ' | |
Bob Nystrom
2014/03/20 18:25:58
Serializing and returning an exception instead of
nweiz
2014/03/20 22:55:41
It's important that errors be handled in [_handleS
| |
82 'contain at least one request.').serialize(request); | |
83 } | |
84 | |
85 return Future.wait(request.map(_handleSingleRequest)).then((results) { | |
Bob Nystrom
2014/03/20 18:25:58
Handling these in parallel might surprise users. D
nweiz
2014/03/20 22:55:41
It's documented in the spec, but okay, I'll add a
| |
86 var nonNull = results.where((result) => result != null); | |
87 return nonNull.isEmpty ? null : nonNull.toList(); | |
88 }); | |
89 }); | |
90 } | |
91 | |
92 /// Parses and handles a JSON serialized request. | |
93 /// | |
94 /// This calls the appropriate method or methods for handling that request and | |
95 /// returns a JSON string, or `null` if no response should be sent. | |
96 Future<String> parseRequest(String request) { | |
97 return syncFuture(() { | |
98 var decodedRequest; | |
99 try { | |
100 decodedRequest = JSON.decode(request); | |
101 } on FormatException catch (error) { | |
102 return new RpcException(error_code.PARSE_ERROR, 'Invalid JSON: ' | |
103 '${error.message}').serialize(request); | |
104 } | |
105 | |
106 return handleRequest(decodedRequest); | |
107 }).then((response) { | |
108 if (response == null) return null; | |
109 return JSON.encode(response); | |
110 }); | |
111 } | |
112 | |
113 /// Handles an individual parsed request. | |
114 Future _handleSingleRequest(request) { | |
115 var parameters; | |
116 return syncFuture(() { | |
117 _validateRequest(request); | |
118 | |
119 var name = request['method']; | |
120 var method = _methods[name]; | |
121 if (method == null) { | |
122 method = _fallbacks.firstWhere((fallback) => fallback.test(name), | |
123 orElse: () => throw new RpcException.methodNotFound(name)).callback; | |
124 } | |
125 | |
126 if (method is ZeroArgumentFunction) { | |
127 if (!request.containsKey('params')) return method(); | |
128 throw new RpcException.invalidParams('No parameters are allowed for ' | |
129 'method "$name".'); | |
130 } | |
131 | |
132 parameters = new Parameters(name, request['params']); | |
Bob Nystrom
2014/03/20 18:25:58
Unhoist the declaration of this (or just inline it
nweiz
2014/03/20 22:55:41
We can't declare it until we've validated that the
Bob Nystrom
2014/03/21 00:36:02
I just meant to declare the parameters variable he
nweiz
2014/03/21 01:24:16
Oh, got it. This code used to refer to the paramet
| |
133 return method(parameters); | |
134 }).then((result) { | |
135 // A request without an id is a notification, which should not be sent a | |
136 // response, even if one is generated on the server. | |
137 if (!request.containsKey('id')) return null; | |
138 | |
139 return { | |
140 'jsonrpc': '2.0', | |
141 'result': result, | |
142 'id': request['id'] | |
143 }; | |
144 }).catchError((error, stackTrace) { | |
145 if (error is! RpcException) { | |
146 error = new RpcException( | |
147 error_code.SERVER_ERROR, getErrorMessage(error), data: { | |
148 'full': error.toString(), | |
149 'stack': new Chain.forTrace(stackTrace).toString() | |
150 }); | |
151 } | |
152 | |
153 if (error.code != error_code.INVALID_REQUEST && | |
154 !request.containsKey('id')) { | |
155 return null; | |
156 } else { | |
157 return error.serialize(request); | |
158 } | |
159 }); | |
160 } | |
161 | |
162 /// Validates that [request] matches the JSON-RPC spec. | |
163 void _validateRequest(request) { | |
164 if (request is! Map) { | |
165 throw new RpcException(error_code.INVALID_REQUEST, 'Requests must be ' | |
166 'Arrays or Objects.'); | |
167 } | |
168 | |
169 if (!request.containsKey('jsonrpc')) { | |
170 throw new RpcException(error_code.INVALID_REQUEST, 'Requests must ' | |
171 'contain a "jsonrpc" key.'); | |
172 } | |
173 | |
174 if (request['jsonrpc'] != '2.0') { | |
175 throw new RpcException(error_code.INVALID_REQUEST, 'Invalid JSON-RPC ' | |
176 'version "${request['jsonrpc']}", expected "2.0".'); | |
177 } | |
178 | |
179 if (!request.containsKey('method')) { | |
180 throw new RpcException(error_code.INVALID_REQUEST, 'Requests must ' | |
181 'contain a "method" key.'); | |
182 } | |
183 | |
184 var method = request['method']; | |
185 if (request['method'] is! String) { | |
186 throw new RpcException(error_code.INVALID_REQUEST, 'Request method must ' | |
187 'be a string, but was "$method".'); | |
Bob Nystrom
2014/03/20 18:25:58
Instead of quoting, how about JSON encoding method
nweiz
2014/03/20 22:55:41
Done.
| |
188 } | |
189 | |
190 var params = request['params']; | |
191 if (request.containsKey('params') && params is! List && params is! Map) { | |
192 throw new RpcException(error_code.INVALID_REQUEST, 'Request params must ' | |
193 'be an Array or an Object, but was "$params".'); | |
194 } | |
195 | |
196 var id = request['id']; | |
197 if (id != null && id is! String && id is! num) { | |
198 throw new RpcException(error_code.INVALID_REQUEST, 'Request id must be a ' | |
199 'string, number, or null, but was "$id".'); | |
200 } | |
201 } | |
202 } | |
203 | |
204 /// A struct for a fallback method. | |
205 class _Fallback { | |
206 /// The callback function. | |
207 final Function callback; | |
208 | |
209 /// The function to test if a method is valid for this fallback. | |
210 final Function test; | |
211 | |
212 _Fallback(this.callback, this.test); | |
213 } | |
OLD | NEW |