Create a package that implements a JSON-RPC 2.0 server.

R=rnystrom@google.com
BUG=17492

Review URL: https://codereview.chromium.org//205533005

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/json_rpc_2@34223 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3d09d0f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+A library that implements the [JSON-RPC 2.0 spec][spec].
+
+[spec]: http://www.jsonrpc.org/specification
+
+## Server
+
+A JSON-RPC 2.0 server exposes a set of methods that can be called by clients.
+These methods can be registered using `Server.registerMethod`:
+
+```dart
+import "package:json_rpc_2/json_rpc_2.dart" as json_rpc;
+
+var server = new json_rpc.Server();
+
+// Any string may be used as a method name. JSON-RPC 2.0 methods are
+// case-sensitive.
+var i = 0;
+server.registerMethod("count", () {
+  // Just return the value to be sent as a response to the client. This can be
+  // anything JSON-serializable, or a Future that completes to something
+  // JSON-serializable.
+  return i++;
+});
+
+// Methods can take parameters. They're presented as a [Parameters] object which
+// makes it easy to validate that the expected parameters exist.
+server.registerMethod("echo", (params) {
+  // If the request doesn't have a "message" parameter, this will automatically
+  // send a response notifying the client that the request was invalid.
+  return params.getNamed("message");
+});
+
+// [Parameters] has methods for verifying argument types.
+server.registerMethod("subtract", (params) {
+  // If "minuend" or "subtrahend" aren't numbers, this will reject the request.
+  return params.getNum("minuend") - params.getNum("subtrahend");
+});
+
+// [Parameters] also supports optional arguments.
+server.registerMethod("sort", (params) {
+  var list = params.getList("list");
+  list.sort();
+  if (params.getBool("descending", orElse: () => false)) {
+    return params.list.reversed;
+  } else {
+    return params.list;
+  }
+});
+
+// A method can send an error response by throwing a `json_rpc.RpcException`.
+// Any positive number may be used as an application-defined error code.
+const DIVIDE_BY_ZERO = 1;
+server.registerMethod("divide", (params) {
+  var divisor = params.getNum("divisor");
+  if (divisor == 0) {
+    throw new json_rpc.RpcException(DIVIDE_BY_ZERO, "Cannot divide by zero.");
+  }
+
+  return params.getNum("dividend") / divisor;
+});
+```
+
+Once you've registered your methods, you can handle requests with
+`Server.parseRequest`:
+
+```dart
+import 'dart:io';
+
+WebSocket.connect('ws://localhost:4321').then((socket) {
+  socket.listen((message) {
+    server.parseRequest(message).then((response) {
+      if (response != null) socket.add(response);
+    });
+  });
+});
+```
+
+If you're communicating with objects that haven't been serialized to a string,
+you can also call `Server.handleRequest` directly:
+
+```dart
+import 'dart:isolate';
+
+var receive = new ReceivePort();
+Isolate.spawnUri('path/to/client.dart', [], receive.sendPort).then((_) {
+  receive.listen((message) {
+    server.handleRequest(message['request']).then((response) {
+      if (response != null) message['respond'].send(response);
+    });
+  });
+})
+```
+
+## Client
+
+Currently this package does not contain an implementation of a JSON-RPC 2.0
+client.
+
diff --git a/lib/error_code.dart b/lib/error_code.dart
new file mode 100644
index 0000000..40d6733
--- /dev/null
+++ b/lib/error_code.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Error codes defined in the [JSON-RPC 2.0 specificiation][spec].
+///
+/// These codes are generally used for protocol-level communication. Most of
+/// them shouldn't be used by the application. Those that should have
+/// convenience constructors in [RpcException].
+///
+/// [spec]: http://www.jsonrpc.org/specification#error_object
+library json_rpc_2.error_code;
+
+/// An error code indicating that invalid JSON was received by the server.
+const PARSE_ERROR = -32700;
+
+/// An error code indicating that the request JSON was invalid according to the
+/// JSON-RPC 2.0 spec.
+const INVALID_REQUEST = -32600;
+
+/// An error code indicating that the requested method does not exist or is
+/// unavailable.
+const METHOD_NOT_FOUND = -32601;
+
+/// An error code indicating that the request paramaters are invalid for the
+/// requested method.
+const INVALID_PARAMS = -32602;
+
+/// An internal JSON-RPC error.
+const INTERNAL_ERROR = -32603;
+
+/// An unexpected error occurred on the server.
+///
+/// The spec reserves the range from -32000 to -32099 for implementation-defined
+/// server exceptions, but for now we only use one of those values.
+const SERVER_ERROR = -32000;
diff --git a/lib/json_rpc_2.dart b/lib/json_rpc_2.dart
new file mode 100644
index 0000000..04e4a52
--- /dev/null
+++ b/lib/json_rpc_2.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2;
+
+export 'src/exception.dart';
+export 'src/parameters.dart';
+export 'src/server.dart';
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
new file mode 100644
index 0000000..fb1cd2f
--- /dev/null
+++ b/lib/src/exception.dart
@@ -0,0 +1,65 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.exception;
+
+import '../error_code.dart' as error_code;
+
+/// An exception from a JSON-RPC server that can be translated into an error
+/// response.
+class RpcException implements Exception {
+  /// The error code.
+  ///
+  /// All non-negative error codes are available for use by application
+  /// developers.
+  final int code;
+
+  /// The error message.
+  ///
+  /// This should be limited to a concise single sentence. Further information
+  /// should be supplied via [data].
+  final String message;
+
+  /// Extra application-defined information about the error.
+  ///
+  /// This must be a JSON-serializable object. If it's a [Map] without a
+  /// `"request"` key, a copy of the request that caused the error will
+  /// automatically be injected.
+  final data;
+
+  RpcException(this.code, this.message, {this.data});
+
+  /// An exception indicating that the method named [methodName] was not found.
+  ///
+  /// This should usually be used only by fallback handlers.
+  RpcException.methodNotFound(String methodName)
+      : this(error_code.METHOD_NOT_FOUND, 'Unknown method "$methodName".');
+
+  /// An exception indicating that the parameters for the requested method were
+  /// invalid.
+  ///
+  /// Methods can use this to reject requests with invalid parameters.
+  RpcException.invalidParams(String message)
+      : this(error_code.INVALID_PARAMS, message);
+
+  /// Converts this exception into a JSON-serializable object that's a valid
+  /// JSON-RPC 2.0 error response.
+  serialize(request) {
+    var modifiedData;
+    if (data is Map && !data.containsKey('request')) {
+      modifiedData = new Map.from(data);
+      modifiedData['request'] = request;
+    } else if (data == null) {
+      modifiedData = {'request': request};
+    }
+
+    var id = request is Map ? request['id'] : null;
+    if (id is! String && id is! num) id = null;
+    return {
+      'jsonrpc': '2.0',
+      'error': {'code': code, 'message': message, 'data': modifiedData},
+      'id': id
+    };
+  }
+}
diff --git a/lib/src/parameters.dart b/lib/src/parameters.dart
new file mode 100644
index 0000000..afc4a40
--- /dev/null
+++ b/lib/src/parameters.dart
@@ -0,0 +1,283 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.parameters;
+
+import 'dart:convert';
+
+import 'exception.dart';
+
+/// A wrapper for the parameters to a server method.
+///
+/// JSON-RPC 2.0 allows parameters that are either a list or a map. This class
+/// provides functions that not only assert that the parameters object is the
+/// correct type, but also that the expected arguments exist and are themselves
+/// the correct type.
+///
+/// Example usage:
+///
+///     server.registerMethod("subtract", (params) {
+///       return params["minuend"].asNum - params["subtrahend"].asNum;
+///     });
+class Parameters {
+  /// The name of the method that this request called.
+  final String method;
+
+  /// The underlying value of the parameters object.
+  ///
+  /// If this is accessed for a [Parameter] that was not passed, the request
+  /// will be automatically rejected. To avoid this, use [Parameter.valueOr].
+  final value;
+
+  Parameters(this.method, this.value);
+
+  /// Returns a single parameter.
+  ///
+  /// If [key] is a [String], the request is expected to provide named
+  /// parameters. If it's an [int], the request is expected to provide
+  /// positional parameters. Requests that don't do so will be rejected
+  /// automatically.
+  ///
+  /// Whether or not the given parameter exists, this returns a [Parameter]
+  /// object. If a parameter's value is accessed through a getter like [value]
+  /// or [Parameter.asNum], the request will be rejected if that parameter
+  /// doesn't exist. On the other hand, if it's accessed through a method with a
+  /// default value like [Parameter.valueOr] or [Parameter.asNumOr], the default
+  /// value will be returned.
+  Parameter operator [](key) {
+    if (key is int) {
+      _assertPositional();
+      if (key < value.length) {
+        return new Parameter._(method, value[key], this, key);
+      } else {
+        return new _MissingParameter(method, this, key);
+      }
+    } else if (key is String) {
+      _assertNamed();
+      if (value.containsKey(key)) {
+        return new Parameter._(method, value[key], this, key);
+      } else {
+        return new _MissingParameter(method, this, key);
+      }
+    } else {
+      throw new ArgumentError('Parameters[] only takes an int or a string, was '
+          '"$key".');
+    }
+  }
+
+  /// Asserts that [value] exists and is a [List] and returns it.
+  List get asList {
+    _assertPositional();
+    return value;
+  }
+
+  /// Asserts that [value] exists and is a [Map] and returns it.
+  Map get asMap {
+    _assertNamed();
+    return value;
+  }
+
+  /// Asserts that [value] is a positional argument list.
+  void _assertPositional() {
+    if (value is List) return;
+    throw new RpcException.invalidParams('Parameters for method "$method" '
+        'must be passed by position.');
+  }
+
+  /// Asserts that [value] is a named argument map.
+  void _assertNamed() {
+    if (value is Map) return;
+    throw new RpcException.invalidParams('Parameters for method "$method" '
+        'must be passed by name.');
+  }
+}
+
+/// A wrapper for a single parameter to a server method.
+///
+/// This provides numerous functions for asserting the type of the parameter in
+/// question. These functions each have a version that asserts that the
+/// parameter exists (for example, [asNum] and [asString]) and a version that
+/// returns a default value if the parameter doesn't exist (for example,
+/// [asNumOr] and [asStringOr]). If an assertion fails, the request is
+/// automatically rejected.
+///
+/// This extends [Parameters] to make it easy to access nested parameters. For
+/// example:
+///
+///     // "params.value" is "{'scores': {'home': [5, 10, 17]}}"
+///     params['scores']['home'][2].asInt // => 17
+class Parameter extends Parameters {
+  // The parent parameters, used to construct [_path].
+  final Parameters _parent;
+
+  /// The key used to access [this], used to construct [_path].
+  final _key;
+
+  /// A human-readable representation of the path of getters used to get this.
+  ///
+  /// Named parameters are represented as `.name`, whereas positional parameters
+  /// are represented as `[index]`. For example: `"foo[0].bar.baz"`. Named
+  /// parameters that contain characters that are neither alphanumeric,
+  /// underscores, or hyphens will be JSON-encoded. For example: `"foo
+  /// bar"."baz.bang"`. If quotes are used for an individual component, they
+  /// won't be used for the entire string.
+  ///
+  /// An exception is made for single-level parameters. A single-level
+  /// positional parameter is just represented by the index plus one, because
+  /// "parameter 1" is clearer than "parameter [0]". A single-level named
+  /// parameter is represented by that name in quotes.
+  String get _path {
+    if (_parent is! Parameter) {
+      return _key is int ? (_key + 1).toString() : JSON.encode(_key);
+    }
+
+    quoteKey(key) {
+      if (key.contains(new RegExp(r'[^a-zA-Z0-9_-]'))) return JSON.encode(key);
+      return key;
+    }
+
+    computePath(params) {
+      if (params._parent is! Parameter) {
+        return params._key is int ? "[${params._key}]" : quoteKey(params._key);
+      }
+
+      var path = computePath(params._parent);
+      return params._key is int ?
+          "$path[${params._key}]" : "$path.${quoteKey(params._key)}";
+    }
+
+    return computePath(this);
+  }
+
+  /// Whether this parameter exists.
+  final exists = true;
+
+  Parameter._(String method, value, this._parent, this._key)
+      : super(method, value);
+
+  /// Returns [value], or [defaultValue] if this parameter wasn't passed.
+  valueOr(defaultValue) => value;
+
+  /// Asserts that [value] exists and is a number and returns it.
+  ///
+  /// [asNumOr] may be used to provide a default value instead of rejecting the
+  /// request if [value] doesn't exist.
+  num get asNum => _getTyped('a number', (value) => value is num);
+
+  /// Asserts that [value] is a number and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  num asNumOr(num defaultValue) => asNum;
+
+  /// Asserts that [value] exists and is an integer and returns it.
+  ///
+  /// [asIntOr] may be used to provide a default value instead of rejecting the
+  /// request if [value] doesn't exist.
+  ///
+  /// Note that which values count as integers varies between the Dart VM and
+  /// dart2js. The value `1.0` will be considered an integer under dart2js but
+  /// not under the VM.
+  int get asInt => _getTyped('an integer', (value) => value is int);
+
+  /// Asserts that [value] is an integer and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  ///
+  /// Note that which values count as integers varies between the Dart VM and
+  /// dart2js. The value `1.0` will be considered an integer under dart2js but
+  /// not under the VM.
+  int asIntOr(int defaultValue) => asInt;
+
+  /// Asserts that [value] exists and is a boolean and returns it.
+  ///
+  /// [asBoolOr] may be used to provide a default value instead of rejecting the
+  /// request if [value] doesn't exist.
+  bool get asBool => _getTyped('a boolean', (value) => value is bool);
+
+  /// Asserts that [value] is a boolean and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  bool asBoolOr(bool defaultValue) => asBool;
+
+  /// Asserts that [value] exists and is a string and returns it.
+  ///
+  /// [asStringOr] may be used to provide a default value instead of rejecting
+  /// the request if [value] doesn't exist.
+  String get asString => _getTyped('a string', (value) => value is String);
+
+  /// Asserts that [value] is a string and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  String asStringOr(String defaultValue) => asString;
+
+  /// Asserts that [value] exists and is a [List] and returns it.
+  ///
+  /// [asListOr] may be used to provide a default value instead of rejecting the
+  /// request if [value] doesn't exist.
+  List get asList => _getTyped('an Array', (value) => value is List);
+
+  /// Asserts that [value] is a [List] and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  List asListOr(List defaultValue) => asList;
+
+  /// Asserts that [value] exists and is a [Map] and returns it.
+  ///
+  /// [asListOr] may be used to provide a default value instead of rejecting the
+  /// request if [value] doesn't exist.
+  Map get asMap => _getTyped('an Object', (value) => value is Map);
+
+  /// Asserts that [value] is a [Map] and returns it.
+  ///
+  /// If [value] doesn't exist, this returns [defaultValue].
+  Map asMapOr(Map defaultValue) => asMap;
+
+  /// Get a parameter named [named] that matches [test], or the value of calling
+  /// [orElse].
+  ///
+  /// [type] is used for the error message. It should begin with an indefinite
+  /// article.
+  _getTyped(String type, bool test(value)) {
+    if (test(value)) return value;
+    throw new RpcException.invalidParams('Parameter $_path for method '
+        '"$method" must be $type, but was ${JSON.encode(value)}.');
+  }
+
+  void _assertPositional() {
+    // Throw the standard exception for a mis-typed list.
+    asList;
+  }
+
+  void _assertNamed() {
+    // Throw the standard exception for a mis-typed map.
+    asMap;
+  }
+}
+
+/// A subclass of [Parameter] representing a missing parameter.
+class _MissingParameter extends Parameter {
+  get value {
+    throw new RpcException.invalidParams('Request for method "$method" is '
+        'missing required parameter $_path.');
+  }
+
+  final exists = false;
+
+  _MissingParameter(String method, Parameters parent, key)
+      : super._(method, null, parent, key);
+
+  valueOr(defaultValue) => defaultValue;
+
+  num asNumOr(num defaultValue) => defaultValue;
+
+  int asIntOr(int defaultValue) => defaultValue;
+
+  bool asBoolOr(bool defaultValue) => defaultValue;
+
+  String asStringOr(String defaultValue) => defaultValue;
+
+  List asListOr(List defaultValue) => defaultValue;
+
+  Map asMapOr(Map defaultValue) => defaultValue;
+}
diff --git a/lib/src/server.dart b/lib/src/server.dart
new file mode 100644
index 0000000..d05c54f
--- /dev/null
+++ b/lib/src/server.dart
@@ -0,0 +1,220 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.server;
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import '../error_code.dart' as error_code;
+import 'exception.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// A JSON-RPC 2.0 server.
+///
+/// A server exposes methods that are called by requests, to which it provides
+/// responses. Methods can be registered using [registerMethod] and
+/// [registerFallback]. Requests can be handled using [handleRequest] and
+/// [parseRequest].
+///
+/// Note that since requests can arrive asynchronously and methods can run
+/// asynchronously, it's possible for multiple methods to be invoked at the same
+/// time, or even for a single method to be invoked multiple times at once.
+class Server {
+  /// The methods registered for this server.
+  final _methods = new Map<String, Function>();
+
+  /// The fallback methods for this server.
+  ///
+  /// These are tried in order until one of them doesn't throw a
+  /// [RpcException.methodNotFound] exception.
+  final _fallbacks = new Queue<Function>();
+
+  Server();
+
+  /// Registers a method named [name] on this server.
+  ///
+  /// [callback] can take either zero or one arguments. If it takes zero, any
+  /// requests for that method that include parameters will be rejected. If it
+  /// takes one, it will be passed a [Parameters] object.
+  ///
+  /// [callback] can return either a JSON-serializable object or a Future that
+  /// completes to a JSON-serializable object. Any errors in [callback] will be
+  /// reported to the client as JSON-RPC 2.0 errors.
+  void registerMethod(String name, Function callback) {
+    if (_methods.containsKey(name)) {
+      throw new ArgumentError('There\'s already a method named "$name".');
+    }
+
+    _methods[name] = callback;
+  }
+
+  /// Registers a fallback method on this server.
+  ///
+  /// A server may have any number of fallback methods. When a request comes in
+  /// that doesn't match any named methods, each fallback is tried in order. A
+  /// fallback can pass on handling a request by throwing a
+  /// [RpcException.methodNotFound] exception.
+  ///
+  /// [callback] can return either a JSON-serializable object or a Future that
+  /// completes to a JSON-serializable object. Any errors in [callback] will be
+  /// reported to the client as JSON-RPC 2.0 errors. [callback] may send custom
+  /// errors by throwing an [RpcException].
+  void registerFallback(callback(Parameters parameters)) {
+    _fallbacks.add(callback);
+  }
+
+  /// Handle a request that's already been parsed from JSON.
+  ///
+  /// [request] is expected to be a JSON-serializable object representing a
+  /// request sent by a client. This calls the appropriate method or methods for
+  /// handling that request and returns a JSON-serializable response, or `null`
+  /// if no response should be sent. [callback] may send custom
+  /// errors by throwing an [RpcException].
+  Future handleRequest(request) {
+    return syncFuture(() {
+      if (request is! List) return _handleSingleRequest(request);
+      if (request.isEmpty) {
+        return new RpcException(error_code.INVALID_REQUEST, 'A batch must '
+            'contain at least one request.').serialize(request);
+      }
+
+      return Future.wait(request.map(_handleSingleRequest)).then((results) {
+        var nonNull = results.where((result) => result != null);
+        return nonNull.isEmpty ? null : nonNull.toList();
+      });
+    });
+  }
+
+  /// Parses and handles a JSON serialized request.
+  ///
+  /// This calls the appropriate method or methods for handling that request and
+  /// returns a JSON string, or `null` if no response should be sent.
+  Future<String> parseRequest(String request) {
+    return syncFuture(() {
+      var decodedRequest;
+      try {
+        decodedRequest = JSON.decode(request);
+      } on FormatException catch (error) {
+        return new RpcException(error_code.PARSE_ERROR, 'Invalid JSON: '
+            '${error.message}').serialize(request);
+      }
+
+      return handleRequest(decodedRequest);
+    }).then((response) {
+      if (response == null) return null;
+      return JSON.encode(response);
+    });
+  }
+
+  /// Handles an individual parsed request.
+  Future _handleSingleRequest(request) {
+    return syncFuture(() {
+      _validateRequest(request);
+
+      var name = request['method'];
+      var method = _methods[name];
+      if (method == null) method = _tryFallbacks;
+
+      if (method is ZeroArgumentFunction) {
+        if (!request.containsKey('params')) return method();
+        throw new RpcException.invalidParams('No parameters are allowed for '
+            'method "$name".');
+      }
+
+      return method(new Parameters(name, request['params']));
+    }).then((result) {
+      // A request without an id is a notification, which should not be sent a
+      // response, even if one is generated on the server.
+      if (!request.containsKey('id')) return null;
+
+      return {
+        'jsonrpc': '2.0',
+        'result': result,
+        'id': request['id']
+      };
+    }).catchError((error, stackTrace) {
+      if (error is! RpcException) {
+        error = new RpcException(
+            error_code.SERVER_ERROR, getErrorMessage(error), data: {
+          'full': error.toString(),
+          'stack': new Chain.forTrace(stackTrace).toString()
+        });
+      }
+
+      if (error.code != error_code.INVALID_REQUEST &&
+          !request.containsKey('id')) {
+        return null;
+      } else {
+        return error.serialize(request);
+      }
+    });
+  }
+
+  /// Validates that [request] matches the JSON-RPC spec.
+  void _validateRequest(request) {
+    if (request is! Map) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request must be '
+          'an Array or an Object.');
+    }
+
+    if (!request.containsKey('jsonrpc')) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request must '
+          'contain a "jsonrpc" key.');
+    }
+
+    if (request['jsonrpc'] != '2.0') {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Invalid JSON-RPC '
+          'version ${JSON.encode(request['jsonrpc'])}, expected "2.0".');
+    }
+
+    if (!request.containsKey('method')) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request must '
+          'contain a "method" key.');
+    }
+
+    var method = request['method'];
+    if (request['method'] is! String) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request method must '
+          'be a string, but was ${JSON.encode(method)}.');
+    }
+
+    var params = request['params'];
+    if (request.containsKey('params') && params is! List && params is! Map) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request params must '
+          'be an Array or an Object, but was ${JSON.encode(params)}.');
+    }
+
+    var id = request['id'];
+    if (id != null && id is! String && id is! num) {
+      throw new RpcException(error_code.INVALID_REQUEST, 'Request id must be a '
+          'string, number, or null, but was ${JSON.encode(id)}.');
+    }
+  }
+
+  /// Try all the fallback methods in order.
+  Future _tryFallbacks(Parameters params) {
+    var iterator = _fallbacks.toList().iterator;
+
+    _tryNext() {
+      if (!iterator.moveNext()) {
+        return new Future.error(
+            new RpcException.methodNotFound(params.method),
+            new Chain.current());
+      }
+
+      return syncFuture(() => iterator.current(params)).catchError((error) {
+        if (error is! RpcException) throw error;
+        if (error.code != error_code.METHOD_NOT_FOUND) throw error;
+        return _tryNext();
+      });
+    }
+
+    return _tryNext();
+  }
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
new file mode 100644
index 0000000..1eff004
--- /dev/null
+++ b/lib/src/utils.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.utils;
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+typedef ZeroArgumentFunction();
+
+/// Like [new Future.sync], but automatically wraps the future in a
+/// [Chain.track] call.
+Future syncFuture(callback()) => Chain.track(new Future.sync(callback));
+
+/// Returns a sentence fragment listing the elements of [iter].
+///
+/// This converts each element of [iter] to a string and separates them with
+/// commas and/or "and" where appropriate.
+String toSentence(Iterable iter) {
+  if (iter.length == 1) return iter.first.toString();
+  return iter.take(iter.length - 1).join(", ") + " and ${iter.last}";
+}
+
+/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
+///
+/// By default, this just adds "s" to the end of [name] to get the plural. If
+/// [plural] is passed, that's used instead.
+String pluralize(String name, int number, {String plural}) {
+  if (number == 1) return name;
+  if (plural != null) return plural;
+  return '${name}s';
+}
+
+/// A regular expression to match the exception prefix that some exceptions'
+/// [Object.toString] values contain.
+final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): ');
+
+/// Get a string description of an exception.
+///
+/// Many exceptions include the exception class name at the beginning of their
+/// [toString], so we remove that if it exists.
+String getErrorMessage(error) =>
+    error.toString().replaceFirst(_exceptionPrefix, '');
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..0919ac1
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,13 @@
+name: json_rpc_2
+version: 0.0.1
+author: Dart Team <misc@dartlang.org>
+description: An implementation of the JSON-RPC 2.0 spec.
+homepage: http://www.dartlang.org
+documentation: http://api.dartlang.org/docs/pkg/json_rpc_2
+dependencies:
+  stack_trace: '>=0.9.1 <0.10.0'
+dev_dependencies:
+  unittest: ">=0.9.0 <0.10.0"
+environment:
+  sdk: ">=1.2.0 <2.0.0"
+
diff --git a/test/server/batch_test.dart b/test/server/batch_test.dart
new file mode 100644
index 0000000..441df58
--- /dev/null
+++ b/test/server/batch_test.dart
@@ -0,0 +1,105 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.test.server.batch_test;
+
+import 'dart:convert';
+
+import 'package:unittest/unittest.dart';
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+
+import 'utils.dart';
+
+void main() {
+  var server;
+  setUp(() {
+    server = new json_rpc.Server()
+        ..registerMethod('foo', () => 'foo')
+        ..registerMethod('id', (params) => params.value)
+        ..registerMethod('arg', (params) => params['arg'].value);
+  });
+
+  test('handles a batch of requests', () {
+    expect(server.handleRequest([
+      {'jsonrpc': '2.0', 'method': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'method': 'id', 'params': ['value'], 'id': 2},
+      {'jsonrpc': '2.0', 'method': 'arg', 'params': {'arg': 'value'}, 'id': 3}
+    ]), completion(equals([
+      {'jsonrpc': '2.0', 'result': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'result': ['value'], 'id': 2},
+      {'jsonrpc': '2.0', 'result': 'value', 'id': 3}
+    ])));
+  });
+
+  test('handles errors individually', () {
+    expect(server.handleRequest([
+      {'jsonrpc': '2.0', 'method': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'method': 'zap', 'id': 2},
+      {'jsonrpc': '2.0', 'method': 'arg', 'params': {'arg': 'value'}, 'id': 3}
+    ]), completion(equals([
+      {'jsonrpc': '2.0', 'result': 'foo', 'id': 1},
+      {
+        'jsonrpc': '2.0',
+        'id': 2,
+        'error': {
+          'code': error_code.METHOD_NOT_FOUND,
+          'message': 'Unknown method "zap".',
+          'data': {'request': {'jsonrpc': '2.0', 'method': 'zap', 'id': 2}},
+        }
+      },
+      {'jsonrpc': '2.0', 'result': 'value', 'id': 3}
+    ])));
+  });
+
+  test('handles notifications individually', () {
+    expect(server.handleRequest([
+      {'jsonrpc': '2.0', 'method': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'method': 'id', 'params': ['value']},
+      {'jsonrpc': '2.0', 'method': 'arg', 'params': {'arg': 'value'}, 'id': 3}
+    ]), completion(equals([
+      {'jsonrpc': '2.0', 'result': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'result': 'value', 'id': 3}
+    ])));
+  });
+
+  test('returns nothing if every request is a notification', () {
+    expect(server.handleRequest([
+      {'jsonrpc': '2.0', 'method': 'foo'},
+      {'jsonrpc': '2.0', 'method': 'id', 'params': ['value']},
+      {'jsonrpc': '2.0', 'method': 'arg', 'params': {'arg': 'value'}}
+    ]), completion(isNull));
+  });
+
+  test('returns an error if the batch is empty', () {
+    expectErrorResponse(server, [], error_code.INVALID_REQUEST,
+        'A batch must contain at least one request.');
+  });
+
+  test('handles a batch of requests parsed from JSON', () {
+    expect(server.parseRequest(JSON.encode([
+      {'jsonrpc': '2.0', 'method': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'method': 'id', 'params': ['value'], 'id': 2},
+      {'jsonrpc': '2.0', 'method': 'arg', 'params': {'arg': 'value'}, 'id': 3}
+    ])), completion(equals(JSON.encode([
+      {'jsonrpc': '2.0', 'result': 'foo', 'id': 1},
+      {'jsonrpc': '2.0', 'result': ['value'], 'id': 2},
+      {'jsonrpc': '2.0', 'result': 'value', 'id': 3}
+    ]))));
+  });
+
+  test('disallows nested batches', () {
+    expect(server.handleRequest([
+      [{'jsonrpc': '2.0', 'method': 'foo', 'id': 1}]
+    ]), completion(equals([{
+      'jsonrpc': '2.0',
+      'id': null,
+      'error': {
+        'code': error_code.INVALID_REQUEST,
+        'message': 'Request must be an Array or an Object.',
+        'data': {'request': [{'jsonrpc': '2.0', 'method': 'foo', 'id': 1}]}
+      }
+    }])));
+  });
+}
\ No newline at end of file
diff --git a/test/server/invalid_request_test.dart b/test/server/invalid_request_test.dart
new file mode 100644
index 0000000..feeefea
--- /dev/null
+++ b/test/server/invalid_request_test.dart
@@ -0,0 +1,86 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.test.server.invalid_request_test;
+
+import 'dart:convert';
+
+import 'package:unittest/unittest.dart';
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+
+import 'utils.dart';
+
+void main() {
+  var server;
+  setUp(() => server = new json_rpc.Server());
+
+  test("a non-Array/Object request is invalid", () {
+    expectErrorResponse(server, 'foo', error_code.INVALID_REQUEST,
+        'Request must be an Array or an Object.');
+  });
+
+  test("requests must have a jsonrpc key", () {
+    expectErrorResponse(server, {
+      'method': 'foo',
+      'id': 1234
+    }, error_code.INVALID_REQUEST, 'Request must contain a "jsonrpc" key.');
+  });
+
+  test("the jsonrpc version must be 2.0", () {
+    expectErrorResponse(server, {
+      'jsonrpc': '1.0',
+      'method': 'foo',
+      'id': 1234
+    }, error_code.INVALID_REQUEST,
+        'Invalid JSON-RPC version "1.0", expected "2.0".');
+  });
+
+  test("requests must have a method key", () {
+    expectErrorResponse(server, {
+      'jsonrpc': '2.0',
+      'id': 1234
+    }, error_code.INVALID_REQUEST, 'Request must contain a "method" key.');
+  });
+
+  test("request method must be a string", () {
+    expectErrorResponse(server, {
+      'jsonrpc': '2.0',
+      'method': 1234,
+      'id': 1234
+    }, error_code.INVALID_REQUEST,
+        'Request method must be a string, but was 1234.');
+  });
+
+  test("request params must be an Array or Object", () {
+    expectErrorResponse(server, {
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'params': 1234,
+      'id': 1234
+    }, error_code.INVALID_REQUEST,
+        'Request params must be an Array or an Object, but was 1234.');
+  });
+
+  test("request id may not be an Array or Object", () {
+    expect(server.handleRequest({
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'id': {'bad': 'id'}
+    }), completion(equals({
+      'jsonrpc': '2.0',
+      'id': null,
+      'error': {
+        'code': error_code.INVALID_REQUEST,
+        'message': 'Request id must be a string, number, or null, but was '
+            '{"bad":"id"}.',
+        'data': {'request': {
+          'jsonrpc': '2.0',
+          'method': 'foo',
+          'id': {'bad': 'id'}
+        }}
+      }
+    })));
+  });
+}
diff --git a/test/server/parameters_test.dart b/test/server/parameters_test.dart
new file mode 100644
index 0000000..9219475
--- /dev/null
+++ b/test/server/parameters_test.dart
@@ -0,0 +1,305 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.test.server.parameters_test;
+
+import 'package:unittest/unittest.dart';
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+
+import 'utils.dart';
+
+void main() {
+  group("with named parameters", () {
+    var parameters;
+    setUp(() {
+      parameters = new json_rpc.Parameters("foo", {
+        "num": 1.5,
+        "int": 1,
+        "bool": true,
+        "string": "zap",
+        "list": [1, 2, 3],
+        "map": {
+          "num": 4.2,
+          "bool": false
+        }
+      });
+    });
+
+    test("value returns the wrapped value", () {
+      expect(parameters.value, equals({
+        "num": 1.5,
+        "int": 1,
+        "bool": true,
+        "string": "zap",
+        "list": [1, 2, 3],
+        "map": {
+          "num": 4.2,
+          "bool": false
+        }
+      }));
+    });
+
+    test("[int] throws a parameter error", () {
+      expect(() => parameters[0],
+          throwsInvalidParams('Parameters for method "foo" must be passed by '
+              'position.'));
+    });
+
+    test("[].value returns existing parameters", () {
+      expect(parameters['num'].value, equals(1.5));
+    });
+
+    test("[].valueOr returns existing parameters", () {
+      expect(parameters['num'].valueOr(7), equals(1.5));
+    });
+
+    test("[].value fails for absent parameters", () {
+      expect(() => parameters['fblthp'].value,
+          throwsInvalidParams('Request for method "foo" is missing required '
+              'parameter "fblthp".'));
+    });
+
+    test("[].valueOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].valueOr(7), equals(7));
+    });
+
+    test("[].exists returns true for existing parameters", () {
+      expect(parameters['num'].exists, isTrue);
+    });
+
+    test("[].exists returns false for missing parameters", () {
+      expect(parameters['fblthp'].exists, isFalse);
+    });
+
+    test("[].asNum returns numeric parameters", () {
+      expect(parameters['num'].asNum, equals(1.5));
+      expect(parameters['int'].asNum, equals(1));
+    });
+
+    test("[].asNumOr returns numeric parameters", () {
+      expect(parameters['num'].asNumOr(7), equals(1.5));
+    });
+
+    test("[].asNum fails for non-numeric parameters", () {
+      expect(() => parameters['bool'].asNum,
+          throwsInvalidParams('Parameter "bool" for method "foo" must be a '
+              'number, but was true.'));
+    });
+
+    test("[].asNumOr fails for non-numeric parameters", () {
+      expect(() => parameters['bool'].asNumOr(7),
+          throwsInvalidParams('Parameter "bool" for method "foo" must be a '
+              'number, but was true.'));
+    });
+
+    test("[].asNum fails for absent parameters", () {
+      expect(() => parameters['fblthp'].asNum,
+          throwsInvalidParams('Request for method "foo" is missing required '
+              'parameter "fblthp".'));
+    });
+
+    test("[].asNumOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asNumOr(7), equals(7));
+    });
+
+    test("[].asInt returns integer parameters", () {
+      expect(parameters['int'].asInt, equals(1));
+    });
+
+    test("[].asIntOr returns integer parameters", () {
+      expect(parameters['int'].asIntOr(7), equals(1));
+    });
+
+    test("[].asInt fails for non-integer parameters", () {
+      expect(() => parameters['bool'].asInt,
+          throwsInvalidParams('Parameter "bool" for method "foo" must be an '
+              'integer, but was true.'));
+    });
+
+    test("[].asIntOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asIntOr(7), equals(7));
+    });
+
+    test("[].asBool returns boolean parameters", () {
+      expect(parameters['bool'].asBool, isTrue);
+    });
+
+    test("[].asBoolOr returns boolean parameters", () {
+      expect(parameters['bool'].asBoolOr(false), isTrue);
+    });
+
+    test("[].asBoolOr fails for non-boolean parameters", () {
+      expect(() => parameters['int'].asBool,
+          throwsInvalidParams('Parameter "int" for method "foo" must be a '
+              'boolean, but was 1.'));
+    });
+
+    test("[].asBoolOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asBoolOr(false), isFalse);
+    });
+
+    test("[].asString returns string parameters", () {
+      expect(parameters['string'].asString, equals("zap"));
+    });
+
+    test("[].asStringOr returns string parameters", () {
+      expect(parameters['string'].asStringOr("bap"), equals("zap"));
+    });
+
+    test("[].asString fails for non-string parameters", () {
+      expect(() => parameters['int'].asString,
+          throwsInvalidParams('Parameter "int" for method "foo" must be a '
+              'string, but was 1.'));
+    });
+
+    test("[].asStringOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asStringOr("bap"), equals("bap"));
+    });
+
+    test("[].asList returns list parameters", () {
+      expect(parameters['list'].asList, equals([1, 2, 3]));
+    });
+
+    test("[].asListOr returns list parameters", () {
+      expect(parameters['list'].asListOr([5, 6, 7]), equals([1, 2, 3]));
+    });
+
+    test("[].asList fails for non-list parameters", () {
+      expect(() => parameters['int'].asList,
+          throwsInvalidParams('Parameter "int" for method "foo" must be an '
+              'Array, but was 1.'));
+    });
+
+    test("[].asListOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asListOr([5, 6, 7]), equals([5, 6, 7]));
+    });
+
+    test("[].asMap returns map parameters", () {
+      expect(parameters['map'].asMap, equals({"num": 4.2, "bool": false}));
+    });
+
+    test("[].asMapOr returns map parameters", () {
+      expect(parameters['map'].asMapOr({}),
+          equals({"num": 4.2, "bool": false}));
+    });
+
+    test("[].asMap fails for non-map parameters", () {
+      expect(() => parameters['int'].asMap,
+          throwsInvalidParams('Parameter "int" for method "foo" must be an '
+              'Object, but was 1.'));
+    });
+
+    test("[].asMapOr succeeds for absent parameters", () {
+      expect(parameters['fblthp'].asMapOr({}), equals({}));
+    });
+
+    group("with a nested parameter map", () {
+      var nested;
+      setUp(() => nested = parameters['map']);
+
+      test("[int] fails with a type error", () {
+        expect(() => nested[0],
+            throwsInvalidParams('Parameter "map" for method "foo" must be an '
+                'Array, but was {"num":4.2,"bool":false}.'));
+      });
+
+      test("[].value returns existing parameters", () {
+        expect(nested['num'].value, equals(4.2));
+        expect(nested['bool'].value, isFalse);
+      });
+
+      test("[].value fails for absent parameters", () {
+        expect(() => nested['fblthp'].value,
+            throwsInvalidParams('Request for method "foo" is missing required '
+                'parameter map.fblthp.'));
+      });
+
+      test("typed getters return correctly-typed parameters", () {
+        expect(nested['num'].asNum, equals(4.2));
+      });
+
+      test("typed getters fail for incorrectly-typed parameters", () {
+        expect(() => nested['bool'].asNum,
+            throwsInvalidParams('Parameter map.bool for method "foo" must be '
+                'a number, but was false.'));
+      });
+    });
+
+    group("with a nested parameter list", () {
+      var nested;
+      setUp(() => nested = parameters['list']);
+
+      test("[string] fails with a type error", () {
+        expect(() => nested['foo'],
+            throwsInvalidParams('Parameter "list" for method "foo" must be an '
+                'Object, but was [1,2,3].'));
+      });
+
+      test("[].value returns existing parameters", () {
+        expect(nested[0].value, equals(1));
+        expect(nested[1].value, equals(2));
+      });
+
+      test("[].value fails for absent parameters", () {
+        expect(() => nested[5].value,
+            throwsInvalidParams('Request for method "foo" is missing required '
+                'parameter list[5].'));
+      });
+
+      test("typed getters return correctly-typed parameters", () {
+        expect(nested[0].asInt, equals(1));
+      });
+
+      test("typed getters fail for incorrectly-typed parameters", () {
+        expect(() => nested[0].asBool,
+            throwsInvalidParams('Parameter list[0] for method "foo" must be '
+                'a boolean, but was 1.'));
+      });
+    });
+  });
+
+  group("with positional parameters", () {
+    var parameters;
+    setUp(() => parameters = new json_rpc.Parameters("foo", [1, 2, 3, 4, 5]));
+
+    test("value returns the wrapped value", () {
+      expect(parameters.value, equals([1, 2, 3, 4, 5]));
+    });
+
+    test("[string] throws a parameter error", () {
+      expect(() => parameters['foo'],
+          throwsInvalidParams('Parameters for method "foo" must be passed by '
+              'name.'));
+    });
+
+    test("[].value returns existing parameters", () {
+      expect(parameters[2].value, equals(3));
+    });
+
+    test("[].value fails for out-of-range parameters", () {
+      expect(() => parameters[10].value,
+          throwsInvalidParams('Request for method "foo" is missing required '
+              'parameter 11.'));
+    });
+
+    test("[].exists returns true for existing parameters", () {
+      expect(parameters[0].exists, isTrue);
+    });
+
+    test("[].exists returns false for missing parameters", () {
+      expect(parameters[10].exists, isFalse);
+    });
+  });
+
+  test("with a complex parameter path", () {
+    var parameters = new json_rpc.Parameters("foo", {
+      'bar baz': [0, 1, 2, {'bang.zap': {'\n': 'qux'}}]
+    });
+
+    expect(() => parameters['bar baz'][3]['bang.zap']['\n']['bip'],
+        throwsInvalidParams('Parameter "bar baz"[3]."bang.zap"."\\n" for '
+            'method "foo" must be an Object, but was "qux".'));
+  });
+}
diff --git a/test/server/server_test.dart b/test/server/server_test.dart
new file mode 100644
index 0000000..f1e0af5
--- /dev/null
+++ b/test/server/server_test.dart
@@ -0,0 +1,203 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.test.server.server_test;
+
+import 'dart:convert';
+
+import 'package:unittest/unittest.dart';
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+
+import 'utils.dart';
+
+void main() {
+  var server;
+  setUp(() => server = new json_rpc.Server());
+
+  test("calls a registered method with the given name", () {
+    server.registerMethod('foo', (params) {
+      return {'params': params.value};
+    });
+
+    expect(server.handleRequest({
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'params': {'param': 'value'},
+      'id': 1234
+    }), completion(equals({
+      'jsonrpc': '2.0',
+      'result': {'params': {'param': 'value'}},
+      'id': 1234
+    })));
+  });
+
+  test("calls a method that takes no parameters", () {
+    server.registerMethod('foo', () => 'foo');
+
+    expect(server.handleRequest({
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'id': 1234
+    }), completion(equals({
+      'jsonrpc': '2.0',
+      'result': 'foo',
+      'id': 1234
+    })));
+  });
+
+  test("a method that takes no parameters rejects parameters", () {
+    server.registerMethod('foo', () => 'foo');
+
+    expectErrorResponse(server, {
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'params': {},
+      'id': 1234
+    },
+        error_code.INVALID_PARAMS,
+        'No parameters are allowed for method "foo".');
+  });
+
+  test("an unexpected error in a method is captured", () {
+    server.registerMethod('foo', () => throw new FormatException('bad format'));
+
+    expect(server.handleRequest({
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'id': 1234
+    }), completion({
+      'jsonrpc': '2.0',
+      'id': 1234,
+      'error': {
+        'code': error_code.SERVER_ERROR,
+        'message': 'bad format',
+        'data': {
+          'request': {'jsonrpc': '2.0', 'method': 'foo', 'id': 1234},
+          'full': 'FormatException: bad format',
+          'stack': contains('server_test.dart')
+        }
+      }
+    }));
+  });
+
+  test("doesn't return a result for a notification", () {
+    server.registerMethod('foo', (args) => 'result');
+
+    expect(server.handleRequest({
+      'jsonrpc': '2.0',
+      'method': 'foo',
+      'params': {}
+    }), completion(isNull));
+  });
+
+  group("JSON", () {
+    test("handles a request parsed from JSON", () {
+      server.registerMethod('foo', (params) {
+        return {'params': params.value};
+      });
+
+      expect(server.parseRequest(JSON.encode({
+        'jsonrpc': '2.0',
+        'method': 'foo',
+        'params': {'param': 'value'},
+        'id': 1234
+      })), completion(equals(JSON.encode({
+        'jsonrpc': '2.0',
+        'result': {'params': {'param': 'value'}},
+        'id': 1234
+      }))));
+    });
+
+    test("handles a notification parsed from JSON", () {
+      server.registerMethod('foo', (params) {
+        return {'params': params};
+      });
+
+      expect(server.parseRequest(JSON.encode({
+        'jsonrpc': '2.0',
+        'method': 'foo',
+        'params': {'param': 'value'}
+      })), completion(isNull));
+    });
+
+    test("a JSON parse error is rejected", () {
+      expect(server.parseRequest('invalid json {'),
+          completion(equals(JSON.encode({
+        'jsonrpc': '2.0',
+        'error': {
+          'code': error_code.PARSE_ERROR,
+          'message': "Invalid JSON: Unexpected character at 0: 'invalid json "
+                     "{'",
+          'data': {'request': 'invalid json {'}
+        },
+        'id': null
+      }))));
+    });
+  });
+
+  group("fallbacks", () {
+    test("calls a fallback if no method matches", () {
+      server.registerMethod('foo', () => 'foo');
+      server.registerMethod('bar', () => 'foo');
+      server.registerFallback((params) => {'fallback': params.value});
+
+      expect(server.handleRequest({
+        'jsonrpc': '2.0',
+        'method': 'baz',
+        'params': {'param': 'value'},
+        'id': 1234
+      }), completion(equals({
+        'jsonrpc': '2.0',
+        'result': {'fallback': {'param': 'value'}},
+        'id': 1234
+      })));
+    });
+
+    test("calls the first matching fallback", () {
+      server.registerFallback((params) =>
+          throw new json_rpc.RpcException.methodNotFound(params.method));
+
+      server.registerFallback((params) => 'fallback 2');
+      server.registerFallback((params) => 'fallback 3');
+
+      expect(server.handleRequest({
+        'jsonrpc': '2.0',
+        'method': 'fallback 2',
+        'id': 1234
+      }), completion(equals({
+        'jsonrpc': '2.0',
+        'result': 'fallback 2',
+        'id': 1234
+      })));
+    });
+
+    test("an unexpected error in a fallback is captured", () {
+      server.registerFallback((_) => throw new FormatException('bad format'));
+
+      expect(server.handleRequest({
+        'jsonrpc': '2.0',
+        'method': 'foo',
+        'id': 1234
+      }), completion({
+        'jsonrpc': '2.0',
+        'id': 1234,
+        'error': {
+          'code': error_code.SERVER_ERROR,
+          'message': 'bad format',
+          'data': {
+            'request': {'jsonrpc': '2.0', 'method': 'foo', 'id': 1234},
+            'full': 'FormatException: bad format',
+            'stack': contains('server_test.dart')
+          }
+        }
+      }));
+    });
+  });
+
+  test("disallows multiple methods with the same name", () {
+    server.registerMethod('foo', () => null);
+    expect(() => server.registerMethod('foo', () => null), throwsArgumentError);
+  });
+}
diff --git a/test/server/utils.dart b/test/server/utils.dart
new file mode 100644
index 0000000..07f571c
--- /dev/null
+++ b/test/server/utils.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library json_rpc_2.test.server.util;
+
+import 'package:unittest/unittest.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+
+void expectErrorResponse(json_rpc.Server server, request, int errorCode,
+    String message) {
+  var id;
+  if (request is Map) id = request['id'];
+
+  expect(server.handleRequest(request), completion(equals({
+    'jsonrpc': '2.0',
+    'id': id,
+    'error': {
+      'code': errorCode,
+      'message': message,
+      'data': {'request': request}
+    }
+  })));
+}
+
+Matcher throwsInvalidParams(String message) {
+  return throwsA(predicate((error) {
+    expect(error, new isInstanceOf<json_rpc.RpcException>());
+    expect(error.message, equals(message));
+    return true;
+  }));
+}