Merge pull request #696 from dart-lang/merge-json_rpc_2-package
Merge `package:json_rpc_2`
diff --git a/.github/ISSUE_TEMPLATE/json_rpc_2.md b/.github/ISSUE_TEMPLATE/json_rpc_2.md
new file mode 100644
index 0000000..29310dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/json_rpc_2.md
@@ -0,0 +1,5 @@
+---
+name: "package:json_rpc_2"
+about: "Create a bug or file a feature request against package:json_rpc_2."
+labels: "package:json_rpc_2"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index a6939c7..bcd3e5a 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -36,6 +36,10 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/graphs/**'
+'package:json_rpc_2':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/json_rpc_2/**'
+
'package:mime':
- changed-files:
- any-glob-to-any-file: 'pkgs/mime/**'
diff --git a/.github/workflows/json_rpc_2.yaml b/.github/workflows/json_rpc_2.yaml
new file mode 100644
index 0000000..cd1ba68
--- /dev/null
+++ b/.github/workflows/json_rpc_2.yaml
@@ -0,0 +1,75 @@
+name: package:json_rpc_2
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/json_rpc_2.yml'
+ - 'pkgs/json_rpc_2/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/json_rpc_2.yml'
+ - 'pkgs/json_rpc_2/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/json_rpc_2/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
+ - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938
+ - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run browser tests
+ run: dart test --platform chrome --compiler dart2wasm,dart2js
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index f797289..d97682b 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@
| [file](pkgs/file/) | A pluggable, mockable file system abstraction for Dart. | [](https://pub.dev/packages/file) |
| [file_testing](pkgs/file_testing/) | Testing utilities for package:file (published but unlisted). | [](https://pub.dev/packages/file_testing) |
| [graphs](pkgs/graphs/) | Graph algorithms that operate on graphs in any representation | [](https://pub.dev/packages/graphs) |
+| [json_rpc_2](pkgs/json_rpc_2/) | Utilities to write a client or server using the JSON-RPC 2.0 spec. | [](https://pub.dev/packages/json_rpc_2) |
| [mime](pkgs/mime/) | Utilities for handling media (MIME) types. | [](https://pub.dev/packages/mime) |
| [oauth2](pkgs/oauth2/) | A client library for authenticatingand making requests via OAuth2. | [](https://pub.dev/packages/oauth2) |
| [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [](https://pub.dev/packages/source_map_stack_trace) |
diff --git a/pkgs/json_rpc_2/.gitignore b/pkgs/json_rpc_2/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/json_rpc_2/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/json_rpc_2/.test_config b/pkgs/json_rpc_2/.test_config
new file mode 100644
index 0000000..412fc5c
--- /dev/null
+++ b/pkgs/json_rpc_2/.test_config
@@ -0,0 +1,3 @@
+{
+ "test_package": true
+}
\ No newline at end of file
diff --git a/pkgs/json_rpc_2/CHANGELOG.md b/pkgs/json_rpc_2/CHANGELOG.md
new file mode 100644
index 0000000..1f2cf8e
--- /dev/null
+++ b/pkgs/json_rpc_2/CHANGELOG.md
@@ -0,0 +1,151 @@
+## 3.0.3
+
+* Require Dart 3.4
+* Move to `dart-lang/tools` monorepo.
+
+## 3.0.2
+
+* Switch to using `package:lints`.
+* Address a few analysis hint violations.
+* Populate the pubspec `repository` field.
+
+## 3.0.1
+
+* Fix a bug where a `null` result to a request caused an exception.
+
+## 3.0.0
+
+* Migrate to null safety.
+* Accept responses even if the server converts the ID to a String.
+
+## 2.2.2
+
+* Fix `Peer.close()` throwing `Bad state: Future already completed`.
+
+## 2.2.1
+
+* Fix `Peer` requests not terminating when the underlying channel is closed.
+
+## 2.2.0
+
+* Added `strictProtocolChecks` named parameter to `Server` and `Peer`
+ constructors. Setting this parameter to false will result in the server not
+ rejecting requests missing the `jsonrpc` parameter.
+
+## 2.1.1
+
+* Fixed issue where throwing `RpcException.methodNotFound` in an asynchronous
+ fallback handler would not result in the next fallback being executed.
+* Updated minimum SDK to Dart `2.2.0`.
+
+## 2.1.0
+
+* `Server` and related classes can now take an `onUnhandledError` callback to
+ notify callers of unhandled exceptions.
+
+## 2.0.10
+
+* Allow `stream_channel` version 2.x
+
+## 2.0.8
+
+* Updated SDK version to 2.0.0-dev.17.0
+
+## 2.0.7
+
+* When a `Client` is closed before a request completes, the error sent to that
+ request's `Future` now includes the request method to aid in debugging.
+
+## 2.0.6
+
+* Internal changes only.
+
+## 2.0.5
+
+* Internal changes only.
+
+## 2.0.4
+
+* `Client.sendRequest()` now throws a `StateError` if the client is closed while
+ the request is in-flight. This avoids dangling `Future`s that will never be
+ completed.
+
+* Both `Client.sendRequest()` and `Client.sendNotification()` now throw
+ `StateError`s if they're called after the client is closed.
+
+## 2.0.3
+
+* Fix new strong-mode warnings.
+
+## 2.0.2
+
+* Fix all strong-mode warnings.
+
+## 2.0.1
+
+* Fix a race condition in which a `StateError` could be top-leveled if
+ `Peer.close()` was called before the underlying channel closed.
+
+## 2.0.0
+
+* **Breaking change:** all constructors now take a `StreamChannel` rather than a
+ `Stream`/`StreamSink` pair.
+
+* `Client.sendRequest()` and `Client.sendNotification()` no longer throw
+ `StateError`s after the connection has been closed but before `Client.close()`
+ has been called.
+
+* The various `close()` methods may now be called before their corresponding
+ `listen()` methods.
+
+* The various `close()` methods now wait on the result of closing the underlying
+ `StreamSink`. Be aware that [in some circumstances][issue 19095]
+ `StreamController`s' `Sink.close()` futures may never complete.
+
+[issue 19095]: https://github.com/dart-lang/sdk/issues/19095
+
+## 1.2.0
+
+* Add `Client.isClosed` and `Server.isClosed`, which make it possible to
+ synchronously determine whether the connection is open. In particular, this
+ makes it possible to reliably tell whether it's safe to call
+ `Client.sendRequest`.
+
+* Fix a race condition in `Server` where a `StateError` could be thrown if the
+ connection was closed in the middle of handling a request.
+
+* Improve stack traces for error responses.
+
+## 1.1.1
+
+* Update the README to match the current API.
+
+## 1.1.0
+
+* Add a `done` getter to `Client`, `Server`, and `Peer`.
+
+## 1.0.0
+
+* Add a `Client` class for communicating with external JSON-RPC 2.0 servers.
+
+* Add a `Peer` class that's both a `Client` and a `Server`.
+
+## 0.1.0
+
+* Remove `Server.handleRequest()` and `Server.parseRequest()`. Instead, `new
+ Server()` takes a `Stream` and a `StreamSink` and uses those behind-the-scenes
+ for its communication.
+
+* Add `Server.listen()`, which causes the server to begin listening to the
+ underlying request stream.
+
+* Add `Server.close()`, which closes the underlying request stream and response
+ sink.
+
+## 0.0.2+3
+
+* Widen the version constraint for `stack_trace`.
+
+## 0.0.2+2
+
+* Fix error response to include data from `RpcException` when not a map.
diff --git a/pkgs/json_rpc_2/LICENSE b/pkgs/json_rpc_2/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/json_rpc_2/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors.
+
+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 LLC 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/pkgs/json_rpc_2/README.md b/pkgs/json_rpc_2/README.md
new file mode 100644
index 0000000..a7dda4a
--- /dev/null
+++ b/pkgs/json_rpc_2/README.md
@@ -0,0 +1,150 @@
+[](https://github.com/dart-lang/tools/actions/workflows/json_rpc_2.yaml)
+[](https://pub.dev/packages/json_rpc_2)
+[](https://pub.dev/packages/json_rpc_2/publisher)
+
+A library that implements the [JSON-RPC 2.0 spec][spec].
+
+[spec]: https://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 'dart:io';
+
+import 'package:json_rpc_2/json_rpc_2.dart';
+import 'package:web_socket_channel/io.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() async {
+ var httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 4321);
+ var connectedChannels =
+ httpServer.transform(WebSocketTransformer()).map(IOWebSocketChannel.new);
+ connectedChannels.listen(handleClient);
+}
+
+void handleClient(WebSocketChannel socket) {
+ // The socket is a `StreamChannel<dynamic>` because it might emit binary
+ // `List<int>`, but JSON RPC 2 only works with Strings so we assert it only
+ // emits those by casting it.
+ var server = Server(socket.cast<String>());
+
+ // 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', (Parameters 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['message'].value;
+ });
+
+ // `Parameters` has methods for verifying argument types.
+ server.registerMethod('subtract', (Parameters params) {
+ // If "minuend" or "subtrahend" aren't numbers, this will reject the
+ // request.
+ return params['minuend'].asNum - params['subtrahend'].asNum;
+ });
+
+ // [Parameters] also supports optional arguments.
+ server.registerMethod('sort', (Parameters params) {
+ var list = params['list'].asList;
+ list.sort();
+ if (params['descendint'].asBoolOr(false)) {
+ return list.reversed;
+ } else {
+ return list;
+ }
+ });
+
+ // A method can send an error response by throwing a `RpcException`.
+ // Any positive number may be used as an application- defined error code.
+ const dividByZero = 1;
+ server.registerMethod('divide', (Parameters params) {
+ var divisor = params['divisor'].asNum;
+ if (divisor == 0) {
+ throw RpcException(dividByZero, 'Cannot divide by zero.');
+ }
+
+ return params['dividend'].asNum / divisor;
+ });
+
+ // To give you time to register all your methods, the server won't start
+ // listening for requests until you call `listen`. Messages are buffered until
+ // listen is called. The returned Future won't complete until the connection
+ // is closed.
+ server.listen();
+}
+```
+
+## Client
+
+A JSON-RPC 2.0 client calls methods on a server and handles the server's
+responses to those method calls. These methods can be called using
+`Client.sendRequest`:
+
+```dart
+import 'package:json_rpc_2/json_rpc_2.dart';
+import 'package:pedantic/pedantic.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() async {
+ var socket = WebSocketChannel.connect(Uri.parse('ws://localhost:4321'));
+ var client = Client(socket.cast<String>());
+
+ // The client won't subscribe to the input stream until you call `listen`.
+ // The returned Future won't complete until the connection is closed.
+ unawaited(client.listen());
+
+ // This calls the "count" method on the server. A Future is returned that
+ // will complete to the value contained in the server's response.
+ var count = await client.sendRequest('count');
+ print('Count is $count');
+
+ // Parameters are passed as a simple Map or, for positional parameters, an
+ // Iterable. Make sure they're JSON-serializable!
+ var echo = await client.sendRequest('echo', {'message': 'hello'});
+ print('Echo says "$echo"!');
+
+ // A notification is a way to call a method that tells the server that no
+ // result is expected. Its return type is `void`; even if it causes an
+ // error, you won't hear back.
+ client.sendNotification('count');
+
+ // If the server sends an error response, the returned Future will complete
+ // with an RpcException. You can catch this error and inspect its error
+ // code, message, and any data that the server sent along with it.
+ try {
+ await client.sendRequest('divide', {'dividend': 2, 'divisor': 0});
+ } on RpcException catch (error) {
+ print('RPC error ${error.code}: ${error.message}');
+ }
+
+ await client.close();
+}
+```
+
+## Peer
+
+Although JSON-RPC 2.0 only explicitly describes clients and servers, it also
+mentions that two-way communication can be supported by making each endpoint
+both a client and a server. This package supports this directly using the `Peer`
+class, which implements both `Client` and `Server`. It supports the same methods
+as those classes, and automatically makes sure that every message from the other
+endpoint is routed and handled correctly.
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/json_rpc_2/analysis_options.yaml b/pkgs/json_rpc_2/analysis_options.yaml
new file mode 100644
index 0000000..a91d5ca
--- /dev/null
+++ b/pkgs/json_rpc_2/analysis_options.yaml
@@ -0,0 +1,13 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: false
+ errors:
+ avoid_dynamic_calls: ignore
+
+linter:
+ rules:
+ - avoid_unused_constructor_parameters
+ - cancel_subscriptions
+ - package_api_docs
diff --git a/pkgs/json_rpc_2/example/client.dart b/pkgs/json_rpc_2/example/client.dart
new file mode 100644
index 0000000..aa8f7ed
--- /dev/null
+++ b/pkgs/json_rpc_2/example/client.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2020, 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.
+
+import 'dart:async';
+
+import 'package:json_rpc_2/json_rpc_2.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() async {
+ var socket = WebSocketChannel.connect(Uri.parse('ws://localhost:4321'));
+ var client = Client(socket.cast<String>());
+
+ // The client won't subscribe to the input stream until you call `listen`.
+ // The returned Future won't complete until the connection is closed.
+ unawaited(client.listen());
+
+ // This calls the "count" method on the server. A Future is returned that
+ // will complete to the value contained in the server's response.
+ var count = await client.sendRequest('count');
+ print('Count is $count');
+
+ // Parameters are passed as a simple Map or, for positional parameters, an
+ // Iterable. Make sure they're JSON-serializable!
+ var echo = await client.sendRequest('echo', {'message': 'hello'});
+ print('Echo says "$echo"!');
+
+ // A notification is a way to call a method that tells the server that no
+ // result is expected. Its return type is `void`; even if it causes an
+ // error, you won't hear back.
+ client.sendNotification('count');
+
+ // If the server sends an error response, the returned Future will complete
+ // with an RpcException. You can catch this error and inspect its error
+ // code, message, and any data that the server sent along with it.
+ try {
+ await client.sendRequest('divide', {'dividend': 2, 'divisor': 0});
+ } on RpcException catch (error) {
+ print('RPC error ${error.code}: ${error.message}');
+ }
+
+ await client.close();
+}
diff --git a/pkgs/json_rpc_2/example/main.dart b/pkgs/json_rpc_2/example/main.dart
new file mode 100644
index 0000000..7d5ab73
--- /dev/null
+++ b/pkgs/json_rpc_2/example/main.dart
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, 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.
+
+import 'dart:io';
+
+import 'package:json_rpc_2/json_rpc_2.dart';
+import 'package:web_socket_channel/io.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+void main() async {
+ var httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, 4321);
+ var connectedChannels =
+ httpServer.transform(WebSocketTransformer()).map(IOWebSocketChannel.new);
+ connectedChannels.listen(handleClient);
+}
+
+void handleClient(WebSocketChannel socket) {
+ // The socket is a `StreamChannel<dynamic>` because it might emit binary
+ // `List<int>`, but JSON RPC 2 only works with Strings so we assert it only
+ // emits those by casting it.
+ var server = Server(socket.cast<String>());
+
+ // 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', (Parameters 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['message'].value;
+ });
+
+ // `Parameters` has methods for verifying argument types.
+ server.registerMethod('subtract', (Parameters params) {
+ // If "minuend" or "subtrahend" aren't numbers, this will reject the
+ // request.
+ return params['minuend'].asNum - params['subtrahend'].asNum;
+ });
+
+ // [Parameters] also supports optional arguments.
+ server.registerMethod('sort', (Parameters params) {
+ var list = params['list'].asList;
+ list.sort();
+ if (params['descendint'].asBoolOr(false)) {
+ return list.reversed;
+ } else {
+ return list;
+ }
+ });
+
+ // A method can send an error response by throwing a `RpcException`.
+ // Any positive number may be used as an application- defined error code.
+ const dividByZero = 1;
+ server.registerMethod('divide', (Parameters params) {
+ var divisor = params['divisor'].asNum;
+ if (divisor == 0) {
+ throw RpcException(dividByZero, 'Cannot divide by zero.');
+ }
+
+ return params['dividend'].asNum / divisor;
+ });
+
+ // To give you time to register all your methods, the server won't start
+ // listening for requests until you call `listen`. Messages are buffered until
+ // listen is called. The returned Future won't complete until the connection
+ // is closed.
+ server.listen();
+}
diff --git a/pkgs/json_rpc_2/lib/error_code.dart b/pkgs/json_rpc_2/lib/error_code.dart
new file mode 100644
index 0000000..5f90791
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/error_code.dart
@@ -0,0 +1,51 @@
+// 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.
+
+// ignore_for_file: constant_identifier_names
+
+import 'src/exception.dart';
+
+/// Error codes defined in the [JSON-RPC 2.0 specification][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
+/// 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 parameters 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;
+
+/// Returns a human-readable name for [errorCode] if it's one specified by the
+/// JSON-RPC 2.0 spec.
+///
+/// If [errorCode] isn't defined in the JSON-RPC 2.0 spec, returns `null`.
+String? name(int errorCode) => switch (errorCode) {
+ PARSE_ERROR => 'parse error',
+ INVALID_REQUEST => 'invalid request',
+ METHOD_NOT_FOUND => 'method not found',
+ INVALID_PARAMS => 'invalid parameters',
+ INTERNAL_ERROR => 'internal error',
+ _ => null
+ };
diff --git a/pkgs/json_rpc_2/lib/json_rpc_2.dart b/pkgs/json_rpc_2/lib/json_rpc_2.dart
new file mode 100644
index 0000000..33e5f49
--- /dev/null
+++ b/pkgs/json_rpc_2/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.
+
+export 'src/client.dart';
+export 'src/exception.dart';
+export 'src/parameters.dart';
+export 'src/peer.dart';
+export 'src/server.dart';
diff --git a/pkgs/json_rpc_2/lib/src/client.dart b/pkgs/json_rpc_2/lib/src/client.dart
new file mode 100644
index 0000000..182f945
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/client.dart
@@ -0,0 +1,246 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import 'exception.dart';
+import 'utils.dart';
+
+/// A JSON-RPC 2.0 client.
+///
+/// A client calls methods on a server and handles the server's responses to
+/// those method calls. Methods can be called with [sendRequest], or with
+/// [sendNotification] if no response is expected.
+class Client {
+ final StreamChannel<dynamic> _channel;
+
+ /// The next request id.
+ var _id = 0;
+
+ /// The current batch of requests to be sent together.
+ ///
+ /// Each element is a JSON RPC spec compliant message.
+ List<Map<String, dynamic>>? _batch;
+
+ /// The map of request ids to pending requests.
+ final _pendingRequests = <int, _Request>{};
+
+ final _done = Completer<void>();
+
+ /// Returns a [Future] that completes when the underlying connection is
+ /// closed.
+ ///
+ /// This is the same future that's returned by [listen] and [close]. It may
+ /// complete before [close] is called if the remote endpoint closes the
+ /// connection.
+ Future get done => _done.future;
+
+ /// Whether the underlying connection is closed.
+ ///
+ /// Note that this will be `true` before [close] is called if the remote
+ /// endpoint closes the connection.
+ bool get isClosed => _done.isCompleted;
+
+ /// Creates a [Client] that communicates over [channel].
+ ///
+ /// Note that the client won't begin listening to [channel] until
+ /// [Client.listen] is called.
+ Client(StreamChannel<String> channel)
+ : this.withoutJson(
+ jsonDocument.bind(channel).transformStream(ignoreFormatExceptions));
+
+ /// Creates a [Client] that communicates using decoded messages over
+ /// [_channel].
+ ///
+ /// Unlike [Client.new], this doesn't read or write JSON strings. Instead, it
+ /// reads and writes decoded maps or lists.
+ ///
+ /// Note that the client won't begin listening to [_channel] until
+ /// [Client.listen] is called.
+ Client.withoutJson(this._channel) {
+ done.whenComplete(() {
+ for (var request in _pendingRequests.values) {
+ request.completer.completeError(StateError(
+ 'The client closed with pending request "${request.method}".'));
+ }
+ _pendingRequests.clear();
+ }).catchError((_) {
+ // Avoid an unhandled error.
+ });
+ }
+
+ /// Starts listening to the underlying stream.
+ ///
+ /// Returns a [Future] that will complete when the connection is closed or
+ /// when it has an error. This is the same as [done].
+ ///
+ /// [listen] may only be called once.
+ Future listen() {
+ _channel.stream.listen(_handleResponse,
+ onError: (Object error, StackTrace stackTrace) {
+ _done.completeError(error, stackTrace);
+ _channel.sink.close();
+ }, onDone: () {
+ if (!_done.isCompleted) _done.complete();
+ close();
+ });
+ return done;
+ }
+
+ /// Closes the underlying connection.
+ ///
+ /// Returns a [Future] that completes when all resources have been released.
+ /// This is the same as [done].
+ Future close() {
+ _channel.sink.close();
+ if (!_done.isCompleted) _done.complete();
+ return done;
+ }
+
+ /// Sends a JSON-RPC 2 request to invoke the given [method].
+ ///
+ /// If passed, [parameters] is the parameters for the method. This must be
+ /// either an [Iterable] (to pass parameters by position) or a [Map] with
+ /// [String] keys (to pass parameters by name). Either way, it must be
+ /// JSON-serializable.
+ ///
+ /// If the request succeeds, this returns the response result as a decoded
+ /// JSON-serializable object. If it fails, it throws an [RpcException]
+ /// describing the failure.
+ ///
+ /// Throws a [StateError] if the client is closed while the request is in
+ /// flight, or if the client is closed when this method is called.
+ Future<Object?> sendRequest(String method, [Object? parameters]) {
+ var id = _id++;
+ _send(method, parameters, id);
+
+ var completer = Completer<Object?>.sync();
+ _pendingRequests[id] = _Request(method, completer, Chain.current());
+ return completer.future;
+ }
+
+ /// Sends a JSON-RPC 2 request to invoke the given [method] without expecting
+ /// a response.
+ ///
+ /// If passed, [parameters] is the parameters for the method. This must be
+ /// either an [Iterable] (to pass parameters by position) or a [Map] with
+ /// [String] keys (to pass parameters by name). Either way, it must be
+ /// JSON-serializable.
+ ///
+ /// Since this is just a notification to which the server isn't expected to
+ /// send a response, it has no return value.
+ ///
+ /// Throws a [StateError] if the client is closed when this method is called.
+ void sendNotification(String method, [Object? parameters]) =>
+ _send(method, parameters);
+
+ /// A helper method for [sendRequest] and [sendNotification].
+ ///
+ /// Sends a request to invoke [method] with [parameters]. If [id] is given,
+ /// the request uses that id.
+ void _send(String method, Object? parameters, [int? id]) {
+ if (parameters is Iterable) parameters = parameters.toList();
+ if (parameters is! Map && parameters is! List && parameters != null) {
+ throw ArgumentError('Only maps and lists may be used as JSON-RPC '
+ 'parameters, was "$parameters".');
+ }
+ if (isClosed) throw StateError('The client is closed.');
+
+ var message = <String, dynamic>{'jsonrpc': '2.0', 'method': method};
+ if (id != null) message['id'] = id;
+ if (parameters != null) message['params'] = parameters;
+
+ if (_batch != null) {
+ _batch!.add(message);
+ } else {
+ _channel.sink.add(message);
+ }
+ }
+
+ /// Runs [callback] and batches any requests sent until it returns.
+ ///
+ /// A batch of requests is sent in a single message on the underlying stream,
+ /// and the responses are likewise sent back in a single message.
+ ///
+ /// [callback] may be synchronous or asynchronous. If it returns a [Future],
+ /// requests will be batched until that Future returns; otherwise, requests
+ /// will only be batched while synchronously executing [callback].
+ ///
+ /// If this is called in the context of another [withBatch] call, it just
+ /// invokes [callback] without creating another batch. This means that
+ /// responses are batched until the first batch ends.
+ void withBatch(FutureOr<void> Function() callback) {
+ if (_batch != null) {
+ callback();
+ return;
+ }
+
+ _batch = [];
+ return tryFinally(callback, () {
+ _channel.sink.add(_batch);
+ _batch = null;
+ });
+ }
+
+ /// Handles a decoded response from the server.
+ void _handleResponse(Object? response) {
+ if (response is List) {
+ response.forEach(_handleSingleResponse);
+ } else {
+ _handleSingleResponse(response);
+ }
+ }
+
+ /// Handles a decoded response from the server after batches have been
+ /// resolved.
+ void _handleSingleResponse(Object? response_) {
+ if (!_isResponseValid(response_)) return;
+ final response = response_ as Map;
+ var id = response['id'];
+ id = (id is String) ? int.parse(id) : id;
+ var request = _pendingRequests.remove(id)!;
+ if (response.containsKey('result')) {
+ request.completer.complete(response['result']);
+ } else {
+ request.completer.completeError(
+ RpcException(response['error']['code'], response['error']['message'],
+ data: response['error']['data']),
+ request.chain);
+ }
+ }
+
+ /// Determines whether the server's response is valid per the spec.
+ bool _isResponseValid(Object? response) {
+ if (response is! Map) return false;
+ if (response['jsonrpc'] != '2.0') return false;
+ var id = response['id'];
+ id = (id is String) ? int.parse(id) : id;
+ if (!_pendingRequests.containsKey(id)) return false;
+ if (response.containsKey('result')) return true;
+
+ if (!response.containsKey('error')) return false;
+ var error = response['error'];
+ if (error is! Map) return false;
+ if (error['code'] is! int) return false;
+ if (error['message'] is! String) return false;
+ return true;
+ }
+}
+
+/// A pending request to the server.
+class _Request {
+ /// THe method that was sent.
+ final String method;
+
+ /// The completer to use to complete the response future.
+ final Completer completer;
+
+ /// The stack chain from where the request was made.
+ final Chain chain;
+
+ _Request(this.method, this.completer, this.chain);
+}
diff --git a/pkgs/json_rpc_2/lib/src/exception.dart b/pkgs/json_rpc_2/lib/src/exception.dart
new file mode 100644
index 0000000..906a053
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/exception.dart
@@ -0,0 +1,75 @@
+// 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.
+
+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 Object? 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.
+ Map<String, dynamic> serialize(Object? request) {
+ dynamic modifiedData;
+ if (data is Map && !(data as Map).containsKey('request')) {
+ modifiedData = {
+ ...data as Map,
+ 'request': request,
+ };
+ } else if (data == null) {
+ modifiedData = {'request': request};
+ } else {
+ modifiedData = data;
+ }
+
+ 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
+ };
+ }
+
+ @override
+ String toString() {
+ var prefix = 'JSON-RPC error $code';
+ var errorName = error_code.name(code);
+ if (errorName != null) prefix += ' ($errorName)';
+ return '$prefix: $message';
+ }
+}
diff --git a/pkgs/json_rpc_2/lib/src/parameters.dart b/pkgs/json_rpc_2/lib/src/parameters.dart
new file mode 100644
index 0000000..0a18882
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/parameters.dart
@@ -0,0 +1,348 @@
+// 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.
+
+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 dynamic 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 [](Object? key) {
+ if (key is int) {
+ _assertPositional();
+ if (key < value.length) {
+ return Parameter._(method, value[key], this, key);
+ } else {
+ return _MissingParameter(method, this, key);
+ }
+ } else if (key is String) {
+ _assertNamed();
+ if (value.containsKey(key)) {
+ return Parameter._(method, value[key], this, key);
+ } else {
+ return _MissingParameter(method, this, key);
+ }
+ } else {
+ throw 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 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 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 Object _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() : jsonEncode(_key);
+ }
+
+ String quoteKey(String key) {
+ if (key.contains(RegExp(r'[^a-zA-Z0-9_-]'))) return jsonEncode(key);
+ return key;
+ }
+
+ String computePath(Parameter params) {
+ if (params._parent is! Parameter) {
+ return params._key is int
+ ? '[${params._key}]'
+ : quoteKey(params._key as String);
+ }
+
+ var path = computePath(params._parent);
+ return params._key is int
+ ? '$path[${params._key}]'
+ : '$path.${quoteKey(params._key as String)}';
+ }
+
+ return computePath(this);
+ }
+
+ /// Whether this parameter exists.
+ bool get exists => true;
+
+ Parameter._(super.method, super.value, this._parent, this._key);
+
+ /// Returns [value], or [defaultValue] if this parameter wasn't passed.
+ dynamic valueOr(Object? 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.
+ @override
+ 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.
+ ///
+ /// [asMapOr] may be used to provide a default value instead of rejecting the
+ /// request if [value] doesn't exist.
+ @override
+ 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;
+
+ /// Asserts that [value] exists, is a string, and can be parsed as a
+ /// [DateTime] and returns it.
+ ///
+ /// [asDateTimeOr] may be used to provide a default value instead of rejecting
+ /// the request if [value] doesn't exist.
+ DateTime get asDateTime => _getParsed('date/time', DateTime.parse);
+
+ /// Asserts that [value] exists, is a string, and can be parsed as a
+ /// [DateTime] and returns it.
+ ///
+ /// If [value] doesn't exist, this returns [defaultValue].
+ DateTime asDateTimeOr(DateTime defaultValue) => asDateTime;
+
+ /// Asserts that [value] exists, is a string, and can be parsed as a
+ /// [Uri] and returns it.
+ ///
+ /// [asUriOr] may be used to provide a default value instead of rejecting the
+ /// request if [value] doesn't exist.
+ Uri get asUri => _getParsed('URI', Uri.parse);
+
+ /// Asserts that [value] exists, is a string, and can be parsed as a
+ /// [Uri] and returns it.
+ ///
+ /// If [value] doesn't exist, this returns [defaultValue].
+ Uri asUriOr(Uri defaultValue) => asUri;
+
+ /// Get a parameter named [type] that matches [test].
+ ///
+ /// [type] is used for the error message. It should begin with an indefinite
+ /// article.
+ dynamic _getTyped(String type, bool Function(dynamic) test) {
+ if (test(value)) return value;
+ throw RpcException.invalidParams('Parameter $_path for method '
+ '"$method" must be $type, but was ${jsonEncode(value)}.');
+ }
+
+ dynamic _getParsed(String description, void Function(String) parse) {
+ var string = asString;
+ try {
+ return parse(string);
+ } on FormatException catch (error) {
+ // DateTime.parse doesn't actually include any useful information in the
+ // FormatException, just the string that was being parsed. There's no use
+ // in including that in the RPC exception. See issue 17753.
+ var message = error.message;
+ if (message == string) {
+ message = '';
+ } else {
+ message = '\n$message';
+ }
+
+ throw RpcException.invalidParams('Parameter $_path for method '
+ '"$method" must be a valid $description, but was '
+ '${jsonEncode(string)}.$message');
+ }
+ }
+
+ @override
+ void _assertPositional() {
+ // Throw the standard exception for a mis-typed list.
+ asList;
+ }
+
+ @override
+ void _assertNamed() {
+ // Throw the standard exception for a mis-typed map.
+ asMap;
+ }
+}
+
+/// A subclass of [Parameter] representing a missing parameter.
+class _MissingParameter extends Parameter {
+ @override
+ dynamic get value {
+ throw RpcException.invalidParams('Request for method "$method" is '
+ 'missing required parameter $_path.');
+ }
+
+ @override
+ bool get exists => false;
+
+ _MissingParameter(String method, Parameters parent, Object key)
+ : super._(method, null, parent, key);
+
+ @override
+ dynamic valueOr(Object? defaultValue) => defaultValue;
+
+ @override
+ num asNumOr(num defaultValue) => defaultValue;
+
+ @override
+ int asIntOr(int defaultValue) => defaultValue;
+
+ @override
+ bool asBoolOr(bool defaultValue) => defaultValue;
+
+ @override
+ String asStringOr(String defaultValue) => defaultValue;
+
+ @override
+ List asListOr(List defaultValue) => defaultValue;
+
+ @override
+ Map asMapOr(Map defaultValue) => defaultValue;
+
+ @override
+ DateTime asDateTimeOr(DateTime defaultValue) => defaultValue;
+
+ @override
+ Uri asUriOr(Uri defaultValue) => defaultValue;
+}
diff --git a/pkgs/json_rpc_2/lib/src/peer.dart b/pkgs/json_rpc_2/lib/src/peer.dart
new file mode 100644
index 0000000..677b6e1
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/peer.dart
@@ -0,0 +1,156 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stream_channel/stream_channel.dart';
+
+import 'client.dart';
+import 'parameters.dart';
+import 'server.dart';
+import 'utils.dart';
+
+/// A JSON-RPC 2.0 client *and* server.
+///
+/// This supports bidirectional peer-to-peer communication with another JSON-RPC
+/// 2.0 endpoint. It sends both requests and responses across the same
+/// communication channel and expects to connect to a peer that does the same.
+class Peer implements Client, Server {
+ final StreamChannel<dynamic> _channel;
+
+ /// The underlying client that handles request-sending and response-receiving
+ /// logic.
+ late final Client _client;
+
+ /// The underlying server that handles request-receiving and response-sending
+ /// logic.
+ late final Server _server;
+
+ /// A stream controller that forwards incoming messages to [_server] if
+ /// they're requests.
+ final _serverIncomingForwarder = StreamController<Object?>(sync: true);
+
+ /// A stream controller that forwards incoming messages to [_client] if
+ /// they're responses.
+ final _clientIncomingForwarder = StreamController<Object?>(sync: true);
+
+ @override
+ late final Future done = Future.wait([_client.done, _server.done]);
+
+ @override
+ bool get isClosed => _client.isClosed || _server.isClosed;
+
+ @override
+ ErrorCallback? get onUnhandledError => _server.onUnhandledError;
+
+ @override
+ bool get strictProtocolChecks => _server.strictProtocolChecks;
+
+ /// Creates a [Peer] that communicates over [channel].
+ ///
+ /// Note that the peer won't begin listening to [channel] until [Peer.listen]
+ /// is called.
+ ///
+ /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+ /// If this is not provided, unhandled exceptions will be swallowed.
+ ///
+ /// If [strictProtocolChecks] is false, the underlying [Server] will accept
+ /// some requests which are not conformant with the JSON-RPC 2.0
+ /// specification. In particular, requests missing the `jsonrpc` parameter
+ /// will be accepted.
+ Peer(StreamChannel<String> channel,
+ {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true})
+ : this.withoutJson(
+ jsonDocument.bind(channel).transform(respondToFormatExceptions),
+ onUnhandledError: onUnhandledError,
+ strictProtocolChecks: strictProtocolChecks);
+
+ /// Creates a [Peer] that communicates using decoded messages over [_channel].
+ ///
+ /// Unlike [Peer.new], this doesn't read or write JSON strings. Instead, it
+ /// reads and writes decoded maps or lists.
+ ///
+ /// Note that the peer won't begin listening to [_channel] until
+ /// [Peer.listen] is called.
+ ///
+ /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+ /// If this is not provided, unhandled exceptions will be swallowed.
+ ///
+ /// If [strictProtocolChecks] is false, the underlying [Server] will accept
+ /// some requests which are not conformant with the JSON-RPC 2.0
+ /// specification. In particular, requests missing the `jsonrpc` parameter
+ /// will be accepted.
+ Peer.withoutJson(this._channel,
+ {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true}) {
+ _server = Server.withoutJson(
+ StreamChannel(_serverIncomingForwarder.stream, _channel.sink),
+ onUnhandledError: onUnhandledError,
+ strictProtocolChecks: strictProtocolChecks);
+ _client = Client.withoutJson(
+ StreamChannel(_clientIncomingForwarder.stream, _channel.sink));
+ }
+
+ // Client methods.
+
+ @override
+ Future sendRequest(String method, [Object? parameters]) =>
+ _client.sendRequest(method, parameters);
+
+ @override
+ void sendNotification(String method, [Object? parameters]) =>
+ _client.sendNotification(method, parameters);
+
+ @override
+ void withBatch(void Function() callback) => _client.withBatch(callback);
+
+ // Server methods.
+
+ @override
+ void registerMethod(String name, Function callback) =>
+ _server.registerMethod(name, callback);
+
+ @override
+ void registerFallback(void Function(Parameters parameters) callback) =>
+ _server.registerFallback(callback);
+
+ // Shared methods.
+
+ @override
+ Future listen() {
+ _client.listen();
+ _server.listen();
+ _channel.stream.listen((message) {
+ if (message is Map) {
+ if (message.containsKey('result') || message.containsKey('error')) {
+ _clientIncomingForwarder.add(message);
+ } else {
+ _serverIncomingForwarder.add(message);
+ }
+ } else if (message is List &&
+ message.isNotEmpty &&
+ message.first is Map) {
+ if (message.first.containsKey('result') ||
+ message.first.containsKey('error')) {
+ _clientIncomingForwarder.add(message);
+ } else {
+ _serverIncomingForwarder.add(message);
+ }
+ } else {
+ // Non-Map and -List messages are ill-formed, so we pass them to the
+ // server since it knows how to send error responses.
+ _serverIncomingForwarder.add(message);
+ }
+ }, onError: (Object error, StackTrace stackTrace) {
+ _serverIncomingForwarder.addError(error, stackTrace);
+ }, onDone: close);
+ return done;
+ }
+
+ @override
+ Future close() {
+ _client.close();
+ _server.close();
+ return done;
+ }
+}
diff --git a/pkgs/json_rpc_2/lib/src/server.dart b/pkgs/json_rpc_2/lib/src/server.dart
new file mode 100644
index 0000000..2c58b79
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/server.dart
@@ -0,0 +1,319 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import '../error_code.dart' as error_code;
+import 'exception.dart';
+import 'parameters.dart';
+import 'utils.dart';
+
+/// A callback for unhandled exceptions.
+typedef ErrorCallback = void Function(dynamic error, dynamic stackTrace);
+
+/// 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].
+///
+/// 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 {
+ final StreamChannel<dynamic> _channel;
+
+ /// The methods registered for this server.
+ final _methods = <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 = Queue<Function>();
+
+ final _done = Completer<void>();
+
+ /// Returns a [Future] that completes when the underlying connection is
+ /// closed.
+ ///
+ /// This is the same future that's returned by [listen] and [close]. It may
+ /// complete before [close] is called if the remote endpoint closes the
+ /// connection.
+ Future get done => _done.future;
+
+ /// Whether the underlying connection is closed.
+ ///
+ /// Note that this will be `true` before [close] is called if the remote
+ /// endpoint closes the connection.
+ bool get isClosed => _done.isCompleted;
+
+ /// A callback that is fired on unhandled exceptions.
+ ///
+ /// In the case where a user provided callback results in an exception that
+ /// cannot be properly routed back to the client, this handler will be
+ /// invoked. If it is not set, the exception will be swallowed.
+ final ErrorCallback? onUnhandledError;
+
+ /// Whether to strictly enforce the JSON-RPC 2.0 specification for received
+ /// messages.
+ ///
+ /// If `false`, this [Server] will accept some requests which are not
+ /// conformant with the JSON-RPC 2.0 specification. In particular, requests
+ /// missing the `jsonrpc` parameter will be accepted.
+ final bool strictProtocolChecks;
+
+ /// Creates a [Server] that communicates over [channel].
+ ///
+ /// Note that the server won't begin listening to [channel] until
+ /// [Server.listen] is called.
+ ///
+ /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+ /// If this is not provided, unhandled exceptions will be swallowed.
+ ///
+ /// If [strictProtocolChecks] is false, this [Server] will accept some
+ /// requests which are not conformant with the JSON-RPC 2.0 specification. In
+ /// particular, requests missing the `jsonrpc` parameter will be accepted.
+ Server(StreamChannel<String> channel,
+ {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true})
+ : this.withoutJson(
+ jsonDocument.bind(channel).transform(respondToFormatExceptions),
+ onUnhandledError: onUnhandledError,
+ strictProtocolChecks: strictProtocolChecks);
+
+ /// Creates a [Server] that communicates using decoded messages over
+ /// [_channel].
+ ///
+ /// Unlike [Server.new], this doesn't read or write JSON strings. Instead, it
+ /// reads and writes decoded maps or lists.
+ ///
+ /// Note that the server won't begin listening to [_channel] until
+ /// [Server.listen] is called.
+ ///
+ /// Unhandled exceptions in callbacks will be forwarded to [onUnhandledError].
+ /// If this is not provided, unhandled exceptions will be swallowed.
+ ///
+ /// If [strictProtocolChecks] is false, this [Server] will accept some
+ /// requests which are not conformant with the JSON-RPC 2.0 specification. In
+ /// particular, requests missing the `jsonrpc` parameter will be accepted.
+ Server.withoutJson(this._channel,
+ {this.onUnhandledError, this.strictProtocolChecks = true});
+
+ /// Starts listening to the underlying stream.
+ ///
+ /// Returns a [Future] that will complete when the connection is closed or
+ /// when it has an error. This is the same as [done].
+ ///
+ /// [listen] may only be called once.
+ Future listen() {
+ _channel.stream.listen(_handleRequest,
+ onError: (Object error, StackTrace stackTrace) {
+ _done.completeError(error, stackTrace);
+ _channel.sink.close();
+ }, onDone: () {
+ if (!_done.isCompleted) _done.complete();
+ });
+ return done;
+ }
+
+ /// Closes the underlying connection.
+ ///
+ /// Returns a [Future] that completes when all resources have been released.
+ /// This is the same as [done].
+ Future close() {
+ _channel.sink.close();
+ if (!_done.isCompleted) _done.complete();
+ return done;
+ }
+
+ /// 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 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(void Function(Parameters parameters) callback) {
+ _fallbacks.add(callback);
+ }
+
+ /// Handle a request.
+ ///
+ /// [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.
+ Future _handleRequest(Object? request) async {
+ dynamic response;
+ if (request is List) {
+ if (request.isEmpty) {
+ response = RpcException(error_code.INVALID_REQUEST,
+ 'A batch must contain at least one request.')
+ .serialize(request);
+ } else {
+ var results = await Future.wait(request.map(_handleSingleRequest));
+ var nonNull = results.where((result) => result != null);
+ if (nonNull.isEmpty) return;
+ response = nonNull.toList();
+ }
+ } else {
+ response = await _handleSingleRequest(request);
+ if (response == null) return;
+ }
+
+ if (!isClosed) _channel.sink.add(response);
+ }
+
+ /// Handles an individual parsed request.
+ Future _handleSingleRequest(Object? request) async {
+ try {
+ _validateRequest(request);
+ request = request as Map;
+
+ var name = request['method'];
+ var method = _methods[name];
+ method ??= _tryFallbacks;
+
+ Object? result;
+ if (method is ZeroArgumentFunction) {
+ if (request.containsKey('params')) {
+ throw RpcException.invalidParams('No parameters are allowed for '
+ 'method "$name".');
+ }
+ result = await method();
+ } else {
+ result = await method(Parameters(name, request['params']));
+ }
+
+ // 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']};
+ } catch (error, stackTrace) {
+ if (error is RpcException) {
+ if (error.code == error_code.INVALID_REQUEST ||
+ (request is Map && request.containsKey('id'))) {
+ return error.serialize(request);
+ } else {
+ onUnhandledError?.call(error, stackTrace);
+ return null;
+ }
+ } else if (request is Map && !request.containsKey('id')) {
+ onUnhandledError?.call(error, stackTrace);
+ return null;
+ }
+ final chain = Chain.forTrace(stackTrace);
+ return RpcException(error_code.SERVER_ERROR, getErrorMessage(error),
+ data: {
+ 'full': '$error',
+ 'stack': '$chain',
+ }).serialize(request);
+ }
+ }
+
+ /// Validates that [request] matches the JSON-RPC spec.
+ void _validateRequest(Object? request) {
+ if (request is! Map) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request must be '
+ 'an Array or an Object.');
+ }
+
+ if (strictProtocolChecks && !request.containsKey('jsonrpc')) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request must '
+ 'contain a "jsonrpc" key.');
+ }
+
+ if ((strictProtocolChecks || request.containsKey('jsonrpc')) &&
+ request['jsonrpc'] != '2.0') {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Invalid JSON-RPC '
+ 'version ${jsonEncode(request['jsonrpc'])}, expected "2.0".');
+ }
+
+ if (!request.containsKey('method')) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request must '
+ 'contain a "method" key.');
+ }
+
+ var method = request['method'];
+ if (request['method'] is! String) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request method must '
+ 'be a string, but was ${jsonEncode(method)}.');
+ }
+
+ if (request.containsKey('params')) {
+ var params = request['params'];
+ if (params is! List && params is! Map) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request params must '
+ 'be an Array or an Object, but was ${jsonEncode(params)}.');
+ }
+ }
+
+ var id = request['id'];
+ if (id != null && id is! String && id is! num) {
+ throw RpcException(
+ error_code.INVALID_REQUEST,
+ 'Request id must be a '
+ 'string, number, or null, but was ${jsonEncode(id)}.');
+ }
+ }
+
+ /// Try all the fallback methods in order.
+ Future _tryFallbacks(Parameters params) {
+ var iterator = _fallbacks.toList().iterator;
+
+ Future tryNext() async {
+ if (!iterator.moveNext()) {
+ throw RpcException.methodNotFound(params.method);
+ }
+
+ try {
+ return await iterator.current(params);
+ } on RpcException catch (error) {
+ if (error.code != error_code.METHOD_NOT_FOUND) rethrow;
+ return tryNext();
+ }
+ }
+
+ return tryNext();
+ }
+}
diff --git a/pkgs/json_rpc_2/lib/src/utils.dart b/pkgs/json_rpc_2/lib/src/utils.dart
new file mode 100644
index 0000000..28bbf21
--- /dev/null
+++ b/pkgs/json_rpc_2/lib/src/utils.dart
@@ -0,0 +1,70 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stream_channel/stream_channel.dart';
+
+import '../error_code.dart' as error_code;
+import 'exception.dart';
+
+typedef ZeroArgumentFunction = FutureOr Function();
+
+/// A regular expression to match the exception prefix that some exceptions'
+/// [Object.toString] values contain.
+final _exceptionPrefix = 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(Object error) =>
+ error.toString().replaceFirst(_exceptionPrefix, '');
+
+/// Like `try`/`finally`, run [body] and ensure that [whenComplete] runs
+/// afterwards, regardless of whether [body] succeeded.
+///
+/// This is synchronicity-agnostic relative to [body]. If [body] returns a
+/// [Future], this wil run asynchronously; otherwise it will run synchronously.
+void tryFinally(dynamic Function() body, void Function() whenComplete) {
+ dynamic result;
+ try {
+ result = body();
+ } catch (_) {
+ whenComplete();
+ rethrow;
+ }
+
+ if (result is! Future) {
+ whenComplete();
+ } else {
+ result.whenComplete(whenComplete);
+ }
+}
+
+/// A transformer that silently drops [FormatException]s.
+final ignoreFormatExceptions = StreamTransformer<Object?, Object?>.fromHandlers(
+ handleError: (error, stackTrace, sink) {
+ if (error is FormatException) return;
+ sink.addError(error, stackTrace);
+});
+
+/// A transformer that sends error responses on [FormatException]s.
+final StreamChannelTransformer<Object?, Object?> respondToFormatExceptions =
+ _RespondToFormatExceptionsTransformer();
+
+class _RespondToFormatExceptionsTransformer
+ implements StreamChannelTransformer<Object?, Object?> {
+ @override
+ StreamChannel<Object?> bind(StreamChannel<Object?> channel) {
+ return channel.changeStream((stream) {
+ return stream.handleError((dynamic error) {
+ final formatException = error as FormatException;
+ var exception = RpcException(
+ error_code.PARSE_ERROR, 'Invalid JSON: ${formatException.message}');
+ channel.sink.add(exception.serialize(formatException.source));
+ }, test: (error) => error is FormatException);
+ });
+ }
+}
diff --git a/pkgs/json_rpc_2/pubspec.yaml b/pkgs/json_rpc_2/pubspec.yaml
new file mode 100644
index 0000000..6f53131
--- /dev/null
+++ b/pkgs/json_rpc_2/pubspec.yaml
@@ -0,0 +1,17 @@
+name: json_rpc_2
+version: 3.0.3
+description: >-
+ Utilities to write a client or server using the JSON-RPC 2.0 spec.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/json_rpc_2
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ stack_trace: ^1.10.0
+ stream_channel: ^2.1.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.25.5
+ web_socket_channel: ^3.0.0
diff --git a/pkgs/json_rpc_2/test/client/client_test.dart b/pkgs/json_rpc_2/test/client/client_test.dart
new file mode 100644
index 0000000..1a4f65d
--- /dev/null
+++ b/pkgs/json_rpc_2/test/client/client_test.dart
@@ -0,0 +1,218 @@
+// 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.
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late ClientController controller;
+
+ setUp(() => controller = ClientController());
+
+ test('sends a message and returns the response', () {
+ controller.expectRequest((request) {
+ expect(
+ request,
+ allOf([
+ containsPair('jsonrpc', '2.0'),
+ containsPair('method', 'foo'),
+ containsPair('params', {'param': 'value'})
+ ]));
+
+ return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']};
+ });
+
+ expect(controller.client.sendRequest('foo', {'param': 'value'}),
+ completion(equals('bar')));
+ });
+
+ test('sends a message and returns the response with String id', () {
+ controller.expectRequest((request) {
+ expect(
+ request,
+ allOf([
+ containsPair('jsonrpc', '2.0'),
+ containsPair('method', 'foo'),
+ containsPair('params', {'param': 'value'})
+ ]));
+
+ return {
+ 'jsonrpc': '2.0',
+ 'result': 'bar',
+ 'id': request['id'].toString()
+ };
+ });
+
+ expect(controller.client.sendRequest('foo', {'param': 'value'}),
+ completion(equals('bar')));
+ });
+
+ test('sends a notification and expects no response', () {
+ controller.expectRequest((request) {
+ expect(
+ request,
+ equals({
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'param': 'value'}
+ }));
+ });
+
+ controller.client.sendNotification('foo', {'param': 'value'});
+ });
+
+ test('sends a notification with positional parameters', () {
+ controller.expectRequest((request) {
+ expect(
+ request,
+ equals({
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': ['value1', 'value2']
+ }));
+ });
+
+ controller.client.sendNotification('foo', ['value1', 'value2']);
+ });
+
+ test('sends a notification with no parameters', () {
+ controller.expectRequest((request) {
+ expect(request, equals({'jsonrpc': '2.0', 'method': 'foo'}));
+ });
+
+ controller.client.sendNotification('foo');
+ });
+
+ test('sends a synchronous batch of requests', () {
+ controller.expectRequest((request) {
+ expect(request, isA<List>());
+ expect(request, hasLength(3));
+ expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
+ expect(
+ request[1],
+ allOf([
+ containsPair('jsonrpc', '2.0'),
+ containsPair('method', 'bar'),
+ containsPair('params', {'param': 'value'})
+ ]));
+ expect(
+ request[2],
+ allOf(
+ [containsPair('jsonrpc', '2.0'), containsPair('method', 'baz')]));
+
+ return [
+ {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
+ {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
+ ];
+ });
+
+ controller.client.withBatch(() {
+ controller.client.sendNotification('foo');
+ expect(controller.client.sendRequest('bar', {'param': 'value'}),
+ completion(equals('bar response')));
+ expect(controller.client.sendRequest('baz'),
+ completion(equals('baz response')));
+ });
+ });
+
+ test('sends an asynchronous batch of requests', () {
+ controller.expectRequest((request) {
+ expect(request, isA<List>());
+ expect(request, hasLength(3));
+ expect(request[0], equals({'jsonrpc': '2.0', 'method': 'foo'}));
+ expect(
+ request[1],
+ allOf([
+ containsPair('jsonrpc', '2.0'),
+ containsPair('method', 'bar'),
+ containsPair('params', {'param': 'value'})
+ ]));
+ expect(
+ request[2],
+ allOf(
+ [containsPair('jsonrpc', '2.0'), containsPair('method', 'baz')]));
+
+ return [
+ {'jsonrpc': '2.0', 'result': 'baz response', 'id': request[2]['id']},
+ {'jsonrpc': '2.0', 'result': 'bar response', 'id': request[1]['id']}
+ ];
+ });
+
+ controller.client.withBatch(() {
+ return Future<void>.value().then<void>((_) {
+ controller.client.sendNotification('foo');
+ }).then<void>((_) {
+ expect(controller.client.sendRequest('bar', {'param': 'value'}),
+ completion(equals('bar response')));
+ }).then<void>((_) {
+ expect(controller.client.sendRequest('baz'),
+ completion(equals('baz response')));
+ });
+ });
+ });
+
+ test('reports an error from the server', () {
+ controller.expectRequest((request) {
+ expect(
+ request,
+ allOf(
+ [containsPair('jsonrpc', '2.0'), containsPair('method', 'foo')]));
+
+ return {
+ 'jsonrpc': '2.0',
+ 'error': {
+ 'code': error_code.SERVER_ERROR,
+ 'message': 'you are bad at requests',
+ 'data': 'some junk'
+ },
+ 'id': request['id']
+ };
+ });
+
+ expect(
+ controller.client.sendRequest('foo', {'param': 'value'}),
+ throwsA(isA<json_rpc.RpcException>()
+ .having((e) => e.code, 'code', error_code.SERVER_ERROR)
+ .having((e) => e.message, 'message', 'you are bad at requests')
+ .having((e) => e.data, 'data', 'some junk')));
+ });
+
+ test('requests throw StateErrors if the client is closed', () {
+ controller.client.close();
+ expect(() => controller.client.sendRequest('foo'), throwsStateError);
+ expect(() => controller.client.sendNotification('foo'), throwsStateError);
+ });
+
+ test('ignores bogus responses', () {
+ // Make a request so we have something to respond to.
+ controller.expectRequest((request) {
+ controller.sendJsonResponse('{invalid');
+ controller.sendResponse('not a map');
+ controller.sendResponse(
+ {'jsonrpc': 'wrong version', 'result': 'wrong', 'id': request['id']});
+ controller.sendResponse({'jsonrpc': '2.0', 'result': 'wrong'});
+ controller.sendResponse({'jsonrpc': '2.0', 'id': request['id']});
+ controller.sendResponse(
+ {'jsonrpc': '2.0', 'error': 'not a map', 'id': request['id']});
+ controller.sendResponse({
+ 'jsonrpc': '2.0',
+ 'error': {'code': 'not an int', 'message': 'dang yo'},
+ 'id': request['id']
+ });
+ controller.sendResponse({
+ 'jsonrpc': '2.0',
+ 'error': {'code': 123, 'message': 0xDEADBEEF},
+ 'id': request['id']
+ });
+
+ return pumpEventQueue().then(
+ (_) => {'jsonrpc': '2.0', 'result': 'right', 'id': request['id']});
+ });
+
+ expect(controller.client.sendRequest('foo'), completion(equals('right')));
+ });
+}
diff --git a/pkgs/json_rpc_2/test/client/stream_test.dart b/pkgs/json_rpc_2/test/client/stream_test.dart
new file mode 100644
index 0000000..b33778e
--- /dev/null
+++ b/pkgs/json_rpc_2/test/client/stream_test.dart
@@ -0,0 +1,97 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamController responseController;
+ late StreamController requestController;
+ late json_rpc.Client client;
+
+ setUp(() {
+ responseController = StreamController();
+ requestController = StreamController();
+ client = json_rpc.Client.withoutJson(
+ StreamChannel(responseController.stream, requestController.sink));
+ });
+
+ test('.withoutJson supports decoded stream and sink', () {
+ client.listen();
+
+ expect(requestController.stream.first.then((request) {
+ expect(
+ request,
+ allOf(
+ [containsPair('jsonrpc', '2.0'), containsPair('method', 'foo')]));
+
+ responseController
+ .add({'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']});
+ }), completes);
+
+ client.sendRequest('foo');
+ });
+
+ test('.listen returns when the controller is closed', () {
+ var hasListenCompeted = false;
+ expect(client.listen().then((_) => hasListenCompeted = true), completes);
+
+ return pumpEventQueue().then((_) {
+ expect(hasListenCompeted, isFalse);
+
+ // This should cause listen to complete.
+ return responseController.close();
+ });
+ });
+
+ test('.listen returns a stream error', () {
+ expect(client.listen(), throwsA('oh no'));
+ responseController.addError('oh no');
+ });
+
+ test('.listen can\'t be called twice', () {
+ client.listen();
+ expect(() => client.listen(), throwsStateError);
+ });
+
+ test('.close cancels the stream subscription and closes the sink', () {
+ // Work around sdk#19095.
+ requestController.stream.listen(null);
+
+ expect(client.listen(), completes);
+
+ expect(client.isClosed, isFalse);
+ expect(client.close(), completes);
+ expect(client.isClosed, isTrue);
+
+ expect(() => responseController.stream.listen((_) {}), throwsStateError);
+ expect(requestController.isClosed, isTrue);
+ });
+
+ group('a stream error', () {
+ test('is reported through .done', () {
+ expect(client.listen(), throwsA('oh no!'));
+ expect(client.done, throwsA('oh no!'));
+ responseController.addError('oh no!');
+ });
+
+ test('cause a pending request to throw a StateError', () {
+ expect(client.listen(), throwsA('oh no!'));
+ expect(client.sendRequest('foo'), throwsStateError);
+ responseController.addError('oh no!');
+ });
+
+ test('causes future requests to throw StateErrors', () async {
+ expect(client.listen(), throwsA('oh no!'));
+ responseController.addError('oh no!');
+ await pumpEventQueue();
+
+ expect(() => client.sendRequest('foo'), throwsStateError);
+ expect(() => client.sendNotification('foo'), throwsStateError);
+ });
+ });
+}
diff --git a/pkgs/json_rpc_2/test/client/utils.dart b/pkgs/json_rpc_2/test/client/utils.dart
new file mode 100644
index 0000000..38e187f
--- /dev/null
+++ b/pkgs/json_rpc_2/test/client/utils.dart
@@ -0,0 +1,56 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+/// A controller used to test a [json_rpc.Client].
+class ClientController {
+ /// The controller for the client's response stream.
+ final _responseController = StreamController<String>();
+
+ /// The controller for the client's request sink.
+ final _requestController = StreamController<String>();
+
+ /// The client.
+ late final json_rpc.Client client;
+
+ ClientController() {
+ client = json_rpc.Client(
+ StreamChannel(_responseController.stream, _requestController.sink));
+ client.listen();
+ }
+
+ /// Expects that the client will send a request.
+ ///
+ /// The request is passed to [callback], which can return a response. If it
+ /// returns a String, that's sent as the response directly. If it returns
+ /// null, no response is sent. Otherwise, the return value is encoded and sent
+ /// as the response.
+ void expectRequest(FutureOr Function(dynamic) callback) {
+ expect(
+ _requestController.stream.first.then((request) {
+ return callback(jsonDecode(request));
+ }).then((response) {
+ if (response == null) return;
+ if (response is! String) response = jsonEncode(response);
+ _responseController.add(response);
+ }),
+ completes);
+ }
+
+ /// Sends [response], a decoded response, to [client].
+ void sendResponse(Object? response) {
+ sendJsonResponse(jsonEncode(response));
+ }
+
+ /// Sends [request], a JSON-encoded response, to [client].
+ void sendJsonResponse(String request) {
+ _responseController.add(request);
+ }
+}
diff --git a/pkgs/json_rpc_2/test/peer_test.dart b/pkgs/json_rpc_2/test/peer_test.dart
new file mode 100644
index 0000000..0df6056
--- /dev/null
+++ b/pkgs/json_rpc_2/test/peer_test.dart
@@ -0,0 +1,251 @@
+// 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.
+
+// ignore_for_file: inference_failure_on_instance_creation
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamSink incoming;
+ late Stream outgoing;
+ late json_rpc.Peer peer;
+
+ setUp(() {
+ var incomingController = StreamController();
+ incoming = incomingController.sink;
+ var outgoingController = StreamController();
+ outgoing = outgoingController.stream;
+ peer = json_rpc.Peer.withoutJson(
+ StreamChannel(incomingController.stream, outgoingController));
+ });
+
+ group('like a client,', () {
+ test('can send a message and receive a response', () {
+ expect(outgoing.first.then((request) {
+ expect(
+ request,
+ equals({
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'bar': 'baz'},
+ 'id': 0
+ }));
+ incoming.add({'jsonrpc': '2.0', 'result': 'qux', 'id': 0});
+ }), completes);
+
+ peer.listen();
+ expect(
+ peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux')));
+ });
+
+ test('can send a batch of messages and receive a batch of responses', () {
+ expect(outgoing.first.then((request) {
+ expect(
+ request,
+ equals([
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'bar': 'baz'},
+ 'id': 0
+ },
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'a',
+ 'params': {'b': 'c'},
+ 'id': 1
+ },
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'w',
+ 'params': {'x': 'y'},
+ 'id': 2
+ }
+ ]));
+
+ incoming.add([
+ {'jsonrpc': '2.0', 'result': 'qux', 'id': 0},
+ {'jsonrpc': '2.0', 'result': 'd', 'id': 1},
+ {'jsonrpc': '2.0', 'result': 'z', 'id': 2}
+ ]);
+ }), completes);
+
+ peer.listen();
+
+ peer.withBatch(() {
+ expect(
+ peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux')));
+ expect(peer.sendRequest('a', {'b': 'c'}), completion(equals('d')));
+ expect(peer.sendRequest('w', {'x': 'y'}), completion(equals('z')));
+ });
+ });
+
+ test('requests terminates when the channel is closed', () async {
+ var incomingController = StreamController<void>();
+ var channel = StreamChannel.withGuarantees(
+ incomingController.stream,
+ StreamController<void>(),
+ );
+ var peer = json_rpc.Peer.withoutJson(channel);
+ unawaited(peer.listen());
+
+ var response = peer.sendRequest('foo');
+ await incomingController.close();
+
+ expect(response, throwsStateError);
+ });
+ });
+
+ test('can be closed', () async {
+ var incomingController = StreamController();
+ var channel = StreamChannel.withGuarantees(
+ incomingController.stream,
+ StreamController(),
+ );
+ var peer = json_rpc.Peer.withoutJson(channel);
+ unawaited(peer.listen());
+ await peer.close();
+ });
+
+ test('considered closed with misbehaving StreamChannel', () async {
+ // If a StreamChannel does not enforce the guarantees stated in it's
+ // contract - specifically that "Closing the sink causes the stream to close
+ // before it emits any more events." - The `Peer` should still understand
+ // when it has been closed manually.
+ var channel = StreamChannel(
+ StreamController().stream,
+ StreamController(),
+ );
+ var peer = json_rpc.Peer.withoutJson(channel);
+ unawaited(peer.listen());
+ unawaited(peer.close());
+ expect(peer.isClosed, true);
+ });
+
+ group('like a server,', () {
+ test('can receive a call and return a response', () {
+ expect(outgoing.first,
+ completion(equals({'jsonrpc': '2.0', 'result': 'qux', 'id': 0})));
+
+ peer.registerMethod('foo', (_) => 'qux');
+ peer.listen();
+
+ incoming.add({
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'bar': 'baz'},
+ 'id': 0
+ });
+ });
+
+ test('can receive a batch of calls and return a batch of responses', () {
+ expect(
+ outgoing.first,
+ completion(equals([
+ {'jsonrpc': '2.0', 'result': 'qux', 'id': 0},
+ {'jsonrpc': '2.0', 'result': 'd', 'id': 1},
+ {'jsonrpc': '2.0', 'result': 'z', 'id': 2}
+ ])));
+
+ peer.registerMethod('foo', (_) => 'qux');
+ peer.registerMethod('a', (_) => 'd');
+ peer.registerMethod('w', (_) => 'z');
+ peer.listen();
+
+ incoming.add([
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'bar': 'baz'},
+ 'id': 0
+ },
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'a',
+ 'params': {'b': 'c'},
+ 'id': 1
+ },
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'w',
+ 'params': {'x': 'y'},
+ 'id': 2
+ }
+ ]);
+ });
+
+ test('returns a response for malformed JSON', () {
+ var incomingController = StreamController<String>();
+ var outgoingController = StreamController<String>();
+ var jsonPeer = json_rpc.Peer(
+ StreamChannel(incomingController.stream, outgoingController));
+
+ expect(
+ outgoingController.stream.first.then(jsonDecode),
+ completion({
+ 'jsonrpc': '2.0',
+ 'error': {
+ 'code': error_code.PARSE_ERROR,
+ 'message': startsWith('Invalid JSON: '),
+ // TODO(nweiz): Always expect the source when sdk#25655 is fixed.
+ 'data': {
+ 'request': anyOf([isNull, '{invalid'])
+ }
+ },
+ 'id': null
+ }));
+
+ jsonPeer.listen();
+
+ incomingController.add('{invalid');
+ });
+
+ test('returns a response for incorrectly-structured JSON', () {
+ expect(
+ outgoing.first,
+ completion({
+ 'jsonrpc': '2.0',
+ 'error': {
+ 'code': error_code.INVALID_REQUEST,
+ 'message': 'Request must contain a "jsonrpc" key.',
+ 'data': {
+ 'request': {'completely': 'wrong'}
+ }
+ },
+ 'id': null
+ }));
+
+ peer.listen();
+
+ incoming.add({'completely': 'wrong'});
+ });
+ });
+
+ test('can notify on unhandled errors for if the method throws', () async {
+ var exception = Exception('test exception');
+ var incomingController = StreamController();
+ var outgoingController = StreamController();
+ final completer = Completer<Exception>();
+ peer = json_rpc.Peer.withoutJson(
+ StreamChannel(incomingController.stream, outgoingController),
+ onUnhandledError: (error, stack) {
+ completer.complete(error);
+ },
+ );
+ peer
+ ..registerMethod('foo', () => throw exception)
+ // ignore: unawaited_futures
+ ..listen();
+
+ incomingController.add({'jsonrpc': '2.0', 'method': 'foo'});
+ var receivedException = await completer.future;
+ expect(receivedException, equals(exception));
+ });
+}
diff --git a/pkgs/json_rpc_2/test/server/batch_test.dart b/pkgs/json_rpc_2/test/server/batch_test.dart
new file mode 100644
index 0000000..af883c4
--- /dev/null
+++ b/pkgs/json_rpc_2/test/server/batch_test.dart
@@ -0,0 +1,147 @@
+// 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.
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/src/parameters.dart' show Parameters;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late ServerController controller;
+
+ setUp(() {
+ controller = ServerController();
+ controller.server
+ ..registerMethod('foo', () => 'foo')
+ ..registerMethod('id', (Parameters params) => params.value)
+ ..registerMethod('arg', (Parameters params) => params['arg'].value);
+ });
+
+ test('handles a batch of requests', () {
+ expect(
+ controller.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(
+ controller.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(
+ controller.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(
+ controller.handleRequest([
+ {'jsonrpc': '2.0', 'method': 'foo'},
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'id',
+ 'params': ['value']
+ },
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'arg',
+ 'params': {'arg': 'value'}
+ }
+ ]),
+ doesNotComplete);
+ });
+
+ test('returns an error if the batch is empty', () {
+ expectErrorResponse(controller, [], error_code.INVALID_REQUEST,
+ 'A batch must contain at least one request.');
+ });
+
+ test('disallows nested batches', () {
+ expect(
+ controller.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}
+ ]
+ }
+ }
+ }
+ ])));
+ });
+}
diff --git a/pkgs/json_rpc_2/test/server/invalid_request_test.dart b/pkgs/json_rpc_2/test/server/invalid_request_test.dart
new file mode 100644
index 0000000..4fa4de1
--- /dev/null
+++ b/pkgs/json_rpc_2/test/server/invalid_request_test.dart
@@ -0,0 +1,94 @@
+// 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.
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late ServerController controller;
+ setUp(() => controller = ServerController());
+
+ test('a non-Array/Object request is invalid', () {
+ expectErrorResponse(controller, 'foo', error_code.INVALID_REQUEST,
+ 'Request must be an Array or an Object.');
+ });
+
+ test('requests must have a jsonrpc key', () {
+ expectErrorResponse(controller, {'method': 'foo', 'id': 1234},
+ error_code.INVALID_REQUEST, 'Request must contain a "jsonrpc" key.');
+ });
+
+ test('the jsonrpc version must be 2.0', () {
+ expectErrorResponse(
+ controller,
+ {'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(controller, {'jsonrpc': '2.0', 'id': 1234},
+ error_code.INVALID_REQUEST, 'Request must contain a "method" key.');
+ });
+
+ test('request method must be a string', () {
+ expectErrorResponse(
+ controller,
+ {'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(
+ controller,
+ {'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(
+ controller.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'}
+ }
+ }
+ }
+ })));
+ });
+
+ group('strict protocol checks disabled', () {
+ setUp(() => controller = ServerController(strictProtocolChecks: false));
+
+ test('and no jsonrpc param', () {
+ expectErrorResponse(controller, {'method': 'foo', 'id': 1234},
+ error_code.METHOD_NOT_FOUND, 'Unknown method "foo".');
+ });
+
+ test('the jsonrpc version must be 2.0', () {
+ expectErrorResponse(
+ controller,
+ {'jsonrpc': '1.0', 'method': 'foo', 'id': 1234},
+ error_code.INVALID_REQUEST,
+ 'Invalid JSON-RPC version "1.0", expected "2.0".');
+ });
+ });
+}
diff --git a/pkgs/json_rpc_2/test/server/parameters_test.dart b/pkgs/json_rpc_2/test/server/parameters_test.dart
new file mode 100644
index 0000000..9ecfb1f
--- /dev/null
+++ b/pkgs/json_rpc_2/test/server/parameters_test.dart
@@ -0,0 +1,403 @@
+// 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.
+
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('with named parameters', () {
+ late json_rpc.Parameters parameters;
+ setUp(() {
+ parameters = json_rpc.Parameters('foo', {
+ 'num': 1.5,
+ 'int': 1,
+ 'bool': true,
+ 'string': 'zap',
+ 'list': [1, 2, 3],
+ 'date-time': '1990-01-01 00:00:00.000',
+ 'uri': 'https://dart.dev',
+ 'invalid-uri': 'http://[::1',
+ '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],
+ 'date-time': '1990-01-01 00:00:00.000',
+ 'uri': 'https://dart.dev',
+ 'invalid-uri': 'http://[::1',
+ '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({}));
+ });
+
+ test('[].asDateTime returns date/time parameters', () {
+ expect(parameters['date-time'].asDateTime, equals(DateTime(1990)));
+ });
+
+ test('[].asDateTimeOr returns date/time parameters', () {
+ expect(parameters['date-time'].asDateTimeOr(DateTime(2014)),
+ equals(DateTime(1990)));
+ });
+
+ test('[].asDateTime fails for non-date/time parameters', () {
+ expect(
+ () => parameters['int'].asDateTime,
+ throwsInvalidParams('Parameter "int" for method "foo" must be a '
+ 'string, but was 1.'));
+ });
+
+ test('[].asDateTimeOr succeeds for absent parameters', () {
+ expect(parameters['fblthp'].asDateTimeOr(DateTime(2014)),
+ equals(DateTime(2014)));
+ });
+
+ test('[].asDateTime fails for non-date/time parameters', () {
+ expect(
+ () => parameters['int'].asDateTime,
+ throwsInvalidParams('Parameter "int" for method "foo" must be a '
+ 'string, but was 1.'));
+ });
+
+ test('[].asDateTime fails for invalid date/times', () {
+ expect(
+ () => parameters['string'].asDateTime,
+ throwsInvalidParams('Parameter "string" for method "foo" must be a '
+ 'valid date/time, but was "zap".\n'
+ 'Invalid date format'));
+ });
+
+ test('[].asUri returns URI parameters', () {
+ expect(parameters['uri'].asUri, equals(Uri.parse('https://dart.dev')));
+ });
+
+ test('[].asUriOr returns URI parameters', () {
+ expect(parameters['uri'].asUriOr(Uri.parse('http://google.com')),
+ equals(Uri.parse('https://dart.dev')));
+ });
+
+ test('[].asUri fails for non-URI parameters', () {
+ expect(
+ () => parameters['int'].asUri,
+ throwsInvalidParams('Parameter "int" for method "foo" must be a '
+ 'string, but was 1.'));
+ });
+
+ test('[].asUriOr succeeds for absent parameters', () {
+ expect(parameters['fblthp'].asUriOr(Uri.parse('http://google.com')),
+ equals(Uri.parse('http://google.com')));
+ });
+
+ test('[].asUri fails for non-URI parameters', () {
+ expect(
+ () => parameters['int'].asUri,
+ throwsInvalidParams('Parameter "int" for method "foo" must be a '
+ 'string, but was 1.'));
+ });
+
+ test('[].asUri fails for invalid URIs', () {
+ expect(
+ () => parameters['invalid-uri'].asUri,
+ throwsInvalidParams('Parameter "invalid-uri" for method "foo" must '
+ 'be a valid URI, but was "http://[::1".\n'
+ 'Missing end `]` to match `[` in host'));
+ });
+
+ group('with a nested parameter map', () {
+ late json_rpc.Parameter 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', () {
+ late json_rpc.Parameter 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', () {
+ late json_rpc.Parameters parameters;
+ setUp(() => parameters = 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 = 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/pkgs/json_rpc_2/test/server/server_test.dart b/pkgs/json_rpc_2/test/server/server_test.dart
new file mode 100644
index 0000000..b3166ce
--- /dev/null
+++ b/pkgs/json_rpc_2/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.
+
+import 'dart:convert';
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late ServerController controller;
+
+ setUp(() => controller = ServerController());
+
+ test('calls a registered method with the given name', () {
+ controller.server.registerMethod('foo', (json_rpc.Parameters params) {
+ return {'params': params.value};
+ });
+
+ expect(
+ controller.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', () {
+ controller.server.registerMethod('foo', () => 'foo');
+
+ expect(
+ controller
+ .handleRequest({'jsonrpc': '2.0', 'method': 'foo', 'id': 1234}),
+ completion(equals({'jsonrpc': '2.0', 'result': 'foo', 'id': 1234})));
+ });
+
+ test('Allows a `null` result', () {
+ controller.server.registerMethod('foo', () => null);
+
+ expect(
+ controller
+ .handleRequest({'jsonrpc': '2.0', 'method': 'foo', 'id': 1234}),
+ completion(equals({'jsonrpc': '2.0', 'result': null, 'id': 1234})));
+ });
+
+ test('a method that takes no parameters rejects parameters', () {
+ controller.server.registerMethod('foo', () => 'foo');
+
+ expectErrorResponse(
+ controller,
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': <String, dynamic>{},
+ 'id': 1234
+ },
+ error_code.INVALID_PARAMS,
+ 'No parameters are allowed for method "foo".');
+ });
+
+ test('an unexpected error in a method is captured', () {
+ controller.server
+ .registerMethod('foo', () => throw const FormatException('bad format'));
+
+ expect(
+ controller
+ .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': isA<String>()
+ }
+ }
+ }));
+ });
+
+ test('doesn\'t return a result for a notification', () {
+ controller.server.registerMethod('foo', (args) => 'result');
+
+ expect(
+ controller.handleRequest(
+ {'jsonrpc': '2.0', 'method': 'foo', 'params': <String, dynamic>{}}),
+ doesNotComplete);
+ });
+
+ test('includes the error data in the response', () {
+ controller.server.registerMethod('foo', (params) {
+ throw json_rpc.RpcException(5, 'Error message.', data: 'data value');
+ });
+
+ expectErrorResponse(
+ controller,
+ {
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': <String, dynamic>{},
+ 'id': 1234
+ },
+ 5,
+ 'Error message.',
+ data: 'data value');
+ });
+
+ test('a JSON parse error is rejected', () {
+ return controller.handleJsonRequest('invalid json {').then((result) {
+ expect(jsonDecode(result), {
+ 'jsonrpc': '2.0',
+ 'error': {
+ 'code': error_code.PARSE_ERROR,
+ 'message': startsWith('Invalid JSON: '),
+ // TODO(nweiz): Always expect the source when sdk#25655 is fixed.
+ 'data': {
+ 'request': anyOf([isNull, 'invalid json {'])
+ }
+ },
+ 'id': null
+ });
+ });
+ });
+
+ group('fallbacks', () {
+ test('calls a fallback if no method matches', () {
+ controller.server
+ ..registerMethod('foo', () => 'foo')
+ ..registerMethod('bar', () => 'foo')
+ ..registerFallback((params) => {'fallback': params.value});
+
+ expect(
+ controller.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', () {
+ controller.server
+ ..registerFallback((params) =>
+ throw json_rpc.RpcException.methodNotFound(params.method))
+ ..registerFallback((params) => 'fallback 2')
+ ..registerFallback((params) => 'fallback 3');
+
+ expect(
+ controller.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', () {
+ controller.server
+ .registerFallback((_) => throw const FormatException('bad format'));
+
+ expect(
+ controller
+ .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': isA<String>()
+ }
+ }
+ }));
+ });
+ });
+
+ test('disallows multiple methods with the same name', () {
+ controller.server.registerMethod('foo', () => null);
+ expect(() => controller.server.registerMethod('foo', () => null),
+ throwsArgumentError);
+ });
+}
diff --git a/pkgs/json_rpc_2/test/server/stream_test.dart b/pkgs/json_rpc_2/test/server/stream_test.dart
new file mode 100644
index 0000000..832e13c
--- /dev/null
+++ b/pkgs/json_rpc_2/test/server/stream_test.dart
@@ -0,0 +1,84 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamController requestController;
+ late StreamController responseController;
+ late json_rpc.Server server;
+
+ setUp(() {
+ requestController = StreamController();
+ responseController = StreamController();
+ server = json_rpc.Server.withoutJson(
+ StreamChannel(requestController.stream, responseController.sink));
+ });
+
+ test('.withoutJson supports decoded stream and sink', () {
+ server.listen();
+
+ server.registerMethod('foo', (json_rpc.Parameters params) {
+ return {'params': params.value};
+ });
+
+ requestController.add({
+ 'jsonrpc': '2.0',
+ 'method': 'foo',
+ 'params': {'param': 'value'},
+ 'id': 1234
+ });
+
+ expect(
+ responseController.stream.first,
+ completion(equals({
+ 'jsonrpc': '2.0',
+ 'result': {
+ 'params': {'param': 'value'}
+ },
+ 'id': 1234
+ })));
+ });
+
+ test('.listen returns when the controller is closed', () {
+ var hasListenCompeted = false;
+ expect(server.listen().then((_) => hasListenCompeted = true), completes);
+
+ return pumpEventQueue().then((_) {
+ expect(hasListenCompeted, isFalse);
+
+ // This should cause listen to complete.
+ return requestController.close();
+ });
+ });
+
+ test('.listen returns a stream error', () {
+ expect(server.listen(), throwsA('oh no'));
+ requestController.addError('oh no');
+ });
+
+ test('.listen can\'t be called twice', () {
+ server.listen();
+
+ expect(() => server.listen(), throwsStateError);
+ });
+
+ test('.close cancels the stream subscription and closes the sink', () {
+ // Work around sdk#19095.
+ responseController.stream.listen(null);
+
+ expect(server.listen(), completes);
+
+ expect(server.isClosed, isFalse);
+ expect(server.close(), completes);
+ expect(server.isClosed, isTrue);
+
+ expect(() => requestController.stream.listen((_) {}), throwsStateError);
+ expect(responseController.isClosed, isTrue);
+ });
+}
diff --git a/pkgs/json_rpc_2/test/server/utils.dart b/pkgs/json_rpc_2/test/server/utils.dart
new file mode 100644
index 0000000..c94628e
--- /dev/null
+++ b/pkgs/json_rpc_2/test/server/utils.dart
@@ -0,0 +1,70 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:json_rpc_2/error_code.dart' as error_code;
+import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+/// A controller used to test a [json_rpc.Server].
+class ServerController {
+ /// The controller for the server's request stream.
+ final _requestController = StreamController<String>();
+
+ /// The controller for the server's response sink.
+ final _responseController = StreamController<String>();
+
+ /// The server.
+ late final json_rpc.Server server;
+
+ ServerController(
+ {json_rpc.ErrorCallback? onUnhandledError,
+ bool strictProtocolChecks = true}) {
+ server = json_rpc.Server(
+ StreamChannel(_requestController.stream, _responseController.sink),
+ onUnhandledError: onUnhandledError,
+ strictProtocolChecks: strictProtocolChecks);
+ server.listen();
+ }
+
+ /// Passes [request], a decoded request, to [server] and returns its decoded
+ /// response.
+ Future handleRequest(Object? request) =>
+ handleJsonRequest(jsonEncode(request)).then(jsonDecode);
+
+ /// Passes [request], a JSON-encoded request, to [server] and returns its
+ /// encoded response.
+ Future<String> handleJsonRequest(String request) {
+ _requestController.add(request);
+ return _responseController.stream.first;
+ }
+}
+
+/// Expects that [controller]'s server will return an error response to
+/// [request] with the given [errorCode], [message], and [data].
+void expectErrorResponse(
+ ServerController controller, Object? request, int errorCode, String message,
+ {Object? data}) {
+ dynamic id;
+ if (request is Map) id = request['id'];
+ data ??= {'request': request};
+
+ expect(
+ controller.handleRequest(request),
+ completion(equals({
+ 'jsonrpc': '2.0',
+ 'id': id,
+ 'error': {'code': errorCode, 'message': message, 'data': data}
+ })));
+}
+
+/// Returns a matcher that matches a [json_rpc.RpcException] with an
+/// `invalid_params` error code.
+Matcher throwsInvalidParams(String message) =>
+ throwsA(isA<json_rpc.RpcException>()
+ .having((e) => e.code, 'code', error_code.INVALID_PARAMS)
+ .having((e) => e.message, 'message', message));