Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(191)

Side by Side Diff: pkg/json_rpc_2/lib/src/server.dart

Issue 205533005: Create a package that implements a JSON-RPC 2.0 server. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698