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 /// |
| 25 /// Note that since requests can arrive asynchronously and methods can run |
| 26 /// asynchronously, it's possible for multiple methods to be invoked at the same |
| 27 /// time, or even for a single method to be invoked multiple times at once. |
| 28 class Server { |
| 29 /// The methods registered for this server. |
| 30 final _methods = new Map<String, Function>(); |
| 31 |
| 32 /// The fallback methods for this server. |
| 33 /// |
| 34 /// These are tried in order until one of them doesn't throw a |
| 35 /// [RpcException.methodNotFound] exception. |
| 36 final _fallbacks = new Queue<Function>(); |
| 37 |
| 38 Server(); |
| 39 |
| 40 /// Registers a method named [name] on this server. |
| 41 /// |
| 42 /// [callback] can take either zero or one arguments. If it takes zero, any |
| 43 /// requests for that method that include parameters will be rejected. If it |
| 44 /// takes one, it will be passed a [Parameters] object. |
| 45 /// |
| 46 /// [callback] can return either a JSON-serializable object or a Future that |
| 47 /// completes to a JSON-serializable object. Any errors in [callback] will be |
| 48 /// reported to the client as JSON-RPC 2.0 errors. |
| 49 void registerMethod(String name, Function callback) { |
| 50 if (_methods.containsKey(name)) { |
| 51 throw new ArgumentError('There\'s already a method named "$name".'); |
| 52 } |
| 53 |
| 54 _methods[name] = callback; |
| 55 } |
| 56 |
| 57 /// Registers a fallback method on this server. |
| 58 /// |
| 59 /// A server may have any number of fallback methods. When a request comes in |
| 60 /// that doesn't match any named methods, each fallback is tried in order. A |
| 61 /// fallback can pass on handling a request by throwing a |
| 62 /// [RpcException.methodNotFound] exception. |
| 63 /// |
| 64 /// [callback] can return either a JSON-serializable object or a Future that |
| 65 /// completes to a JSON-serializable object. Any errors in [callback] will be |
| 66 /// reported to the client as JSON-RPC 2.0 errors. [callback] may send custom |
| 67 /// errors by throwing an [RpcException]. |
| 68 void registerFallback(callback(Parameters parameters)) { |
| 69 _fallbacks.add(callback); |
| 70 } |
| 71 |
| 72 /// Handle a request that's already been parsed from JSON. |
| 73 /// |
| 74 /// [request] is expected to be a JSON-serializable object representing a |
| 75 /// request sent by a client. This calls the appropriate method or methods for |
| 76 /// handling that request and returns a JSON-serializable response, or `null` |
| 77 /// if no response should be sent. [callback] may send custom |
| 78 /// errors by throwing an [RpcException]. |
| 79 Future handleRequest(request) { |
| 80 return syncFuture(() { |
| 81 if (request is! List) return _handleSingleRequest(request); |
| 82 if (request.isEmpty) { |
| 83 return new RpcException(error_code.INVALID_REQUEST, 'A batch must ' |
| 84 'contain at least one request.').serialize(request); |
| 85 } |
| 86 |
| 87 return Future.wait(request.map(_handleSingleRequest)).then((results) { |
| 88 var nonNull = results.where((result) => result != null); |
| 89 return nonNull.isEmpty ? null : nonNull.toList(); |
| 90 }); |
| 91 }); |
| 92 } |
| 93 |
| 94 /// Parses and handles a JSON serialized request. |
| 95 /// |
| 96 /// This calls the appropriate method or methods for handling that request and |
| 97 /// returns a JSON string, or `null` if no response should be sent. |
| 98 Future<String> parseRequest(String request) { |
| 99 return syncFuture(() { |
| 100 var decodedRequest; |
| 101 try { |
| 102 decodedRequest = JSON.decode(request); |
| 103 } on FormatException catch (error) { |
| 104 return new RpcException(error_code.PARSE_ERROR, 'Invalid JSON: ' |
| 105 '${error.message}').serialize(request); |
| 106 } |
| 107 |
| 108 return handleRequest(decodedRequest); |
| 109 }).then((response) { |
| 110 if (response == null) return null; |
| 111 return JSON.encode(response); |
| 112 }); |
| 113 } |
| 114 |
| 115 /// Handles an individual parsed request. |
| 116 Future _handleSingleRequest(request) { |
| 117 return syncFuture(() { |
| 118 _validateRequest(request); |
| 119 |
| 120 var name = request['method']; |
| 121 var method = _methods[name]; |
| 122 if (method == null) method = _tryFallbacks; |
| 123 |
| 124 if (method is ZeroArgumentFunction) { |
| 125 if (!request.containsKey('params')) return method(); |
| 126 throw new RpcException.invalidParams('No parameters are allowed for ' |
| 127 'method "$name".'); |
| 128 } |
| 129 |
| 130 return method(new Parameters(name, request['params'])); |
| 131 }).then((result) { |
| 132 // A request without an id is a notification, which should not be sent a |
| 133 // response, even if one is generated on the server. |
| 134 if (!request.containsKey('id')) return null; |
| 135 |
| 136 return { |
| 137 'jsonrpc': '2.0', |
| 138 'result': result, |
| 139 'id': request['id'] |
| 140 }; |
| 141 }).catchError((error, stackTrace) { |
| 142 if (error is! RpcException) { |
| 143 error = new RpcException( |
| 144 error_code.SERVER_ERROR, getErrorMessage(error), data: { |
| 145 'full': error.toString(), |
| 146 'stack': new Chain.forTrace(stackTrace).toString() |
| 147 }); |
| 148 } |
| 149 |
| 150 if (error.code != error_code.INVALID_REQUEST && |
| 151 !request.containsKey('id')) { |
| 152 return null; |
| 153 } else { |
| 154 return error.serialize(request); |
| 155 } |
| 156 }); |
| 157 } |
| 158 |
| 159 /// Validates that [request] matches the JSON-RPC spec. |
| 160 void _validateRequest(request) { |
| 161 if (request is! Map) { |
| 162 throw new RpcException(error_code.INVALID_REQUEST, 'Request must be ' |
| 163 'an Array or an Object.'); |
| 164 } |
| 165 |
| 166 if (!request.containsKey('jsonrpc')) { |
| 167 throw new RpcException(error_code.INVALID_REQUEST, 'Request must ' |
| 168 'contain a "jsonrpc" key.'); |
| 169 } |
| 170 |
| 171 if (request['jsonrpc'] != '2.0') { |
| 172 throw new RpcException(error_code.INVALID_REQUEST, 'Invalid JSON-RPC ' |
| 173 'version ${JSON.encode(request['jsonrpc'])}, expected "2.0".'); |
| 174 } |
| 175 |
| 176 if (!request.containsKey('method')) { |
| 177 throw new RpcException(error_code.INVALID_REQUEST, 'Request must ' |
| 178 'contain a "method" key.'); |
| 179 } |
| 180 |
| 181 var method = request['method']; |
| 182 if (request['method'] is! String) { |
| 183 throw new RpcException(error_code.INVALID_REQUEST, 'Request method must ' |
| 184 'be a string, but was ${JSON.encode(method)}.'); |
| 185 } |
| 186 |
| 187 var params = request['params']; |
| 188 if (request.containsKey('params') && params is! List && params is! Map) { |
| 189 throw new RpcException(error_code.INVALID_REQUEST, 'Request params must ' |
| 190 'be an Array or an Object, but was ${JSON.encode(params)}.'); |
| 191 } |
| 192 |
| 193 var id = request['id']; |
| 194 if (id != null && id is! String && id is! num) { |
| 195 throw new RpcException(error_code.INVALID_REQUEST, 'Request id must be a ' |
| 196 'string, number, or null, but was ${JSON.encode(id)}.'); |
| 197 } |
| 198 } |
| 199 |
| 200 /// Try all the fallback methods in order. |
| 201 Future _tryFallbacks(Parameters params) { |
| 202 var iterator = _fallbacks.toList().iterator; |
| 203 |
| 204 _tryNext() { |
| 205 if (!iterator.moveNext()) { |
| 206 return new Future.error( |
| 207 new RpcException.methodNotFound(params.method), |
| 208 new Chain.current()); |
| 209 } |
| 210 |
| 211 return syncFuture(() => iterator.current(params)).catchError((error) { |
| 212 if (error is! RpcException) throw error; |
| 213 if (error.code != error_code.METHOD_NOT_FOUND) throw error; |
| 214 return _tryNext(); |
| 215 }); |
| 216 } |
| 217 |
| 218 return _tryNext(); |
| 219 } |
| 220 } |
OLD | NEW |