diff --git a/pkgs/code_builder/CHANGELOG.md b/pkgs/code_builder/CHANGELOG.md index 471f72f..1d86e5c 100644 --- a/pkgs/code_builder/CHANGELOG.md +++ b/pkgs/code_builder/CHANGELOG.md
@@ -2,6 +2,7 @@ * Upgrade `dart_style` and `source_gen` to remove `package:macros` dependency. * Require Dart `^3.6.0` due to the upgrades. +* Support `Expression.newInstanceNamed` with empty name ## 4.10.1
diff --git a/pkgs/code_builder/lib/src/specs/expression.dart b/pkgs/code_builder/lib/src/specs/expression.dart index b9193e6..aa06de2 100644 --- a/pkgs/code_builder/lib/src/specs/expression.dart +++ b/pkgs/code_builder/lib/src/specs/expression.dart
@@ -578,10 +578,10 @@ final out = output ??= StringBuffer(); return _writeConstExpression(out, expression.isConst, () { expression.target.accept(this, out); - if (expression.name != null) { + if (expression.name case final name? when name.isNotEmpty) { out ..write('.') - ..write(expression.name); + ..write(name); } if (expression.typeArguments.isNotEmpty) { out.write('<');
diff --git a/pkgs/code_builder/test/specs/code/expression_test.dart b/pkgs/code_builder/test/specs/code/expression_test.dart index 4ce9eba..7a424fd 100644 --- a/pkgs/code_builder/test/specs/code/expression_test.dart +++ b/pkgs/code_builder/test/specs/code/expression_test.dart
@@ -251,6 +251,13 @@ ); }); + test('should emit invoking unnamed constructor when name is empty', () { + expect( + refer('Foo').newInstanceNamed('', []), + equalsDart('Foo()'), + ); + }); + test('should emit invoking const Type()', () { expect( refer('Object').constInstance([]),
diff --git a/pkgs/json_rpc_2/CHANGELOG.md b/pkgs/json_rpc_2/CHANGELOG.md index 1f2cf8e..0ab99da 100644 --- a/pkgs/json_rpc_2/CHANGELOG.md +++ b/pkgs/json_rpc_2/CHANGELOG.md
@@ -1,3 +1,10 @@ +## 4.0.0 + +* Add custom ID generator option to clients, which allows for `String` ids. +* **Breaking**: When `String` ids are present in a response, we no longer + automatically try to parse them as integers. This behavior was never a part + of the spec, and is not compatible with allowing custom ID generators. + ## 3.0.3 * Require Dart 3.4
diff --git a/pkgs/json_rpc_2/lib/src/client.dart b/pkgs/json_rpc_2/lib/src/client.dart index 182f945..388c601 100644 --- a/pkgs/json_rpc_2/lib/src/client.dart +++ b/pkgs/json_rpc_2/lib/src/client.dart
@@ -18,8 +18,8 @@ class Client { final StreamChannel<dynamic> _channel; - /// The next request id. - var _id = 0; + /// A function to generate the next request id. + Object Function() _idGenerator; /// The current batch of requests to be sent together. /// @@ -27,7 +27,9 @@ List<Map<String, dynamic>>? _batch; /// The map of request ids to pending requests. - final _pendingRequests = <int, _Request>{}; + /// + /// Keys must be of type `int` or `String`. + final _pendingRequests = <Object, _Request>{}; final _done = Completer<void>(); @@ -49,9 +51,14 @@ /// /// Note that the client won't begin listening to [channel] until /// [Client.listen] is called. - Client(StreamChannel<String> channel) + /// + /// If [idGenerator] is passed, it will be called to generate an ID for each + /// request. Defaults to an auto-incrementing `int`. The value returned must + /// be either an `int` or `String`. + Client(StreamChannel<String> channel, {Object Function()? idGenerator}) : this.withoutJson( - jsonDocument.bind(channel).transformStream(ignoreFormatExceptions)); + jsonDocument.bind(channel).transformStream(ignoreFormatExceptions), + idGenerator: idGenerator); /// Creates a [Client] that communicates using decoded messages over /// [_channel]. @@ -61,7 +68,12 @@ /// /// Note that the client won't begin listening to [_channel] until /// [Client.listen] is called. - Client.withoutJson(this._channel) { + /// + /// If [_idGenerator] is passed, it will be called to generate an ID for each + /// request. Defaults to an auto-incrementing `int`. The value returned must + /// be either an `int` or `String`. + Client.withoutJson(this._channel, {Object Function()? idGenerator}) + : _idGenerator = idGenerator ?? _createIncrementingIdGenerator() { done.whenComplete(() { for (var request in _pendingRequests.values) { request.completer.completeError(StateError( @@ -115,7 +127,7 @@ /// 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++; + var id = _idGenerator(); _send(method, parameters, id); var completer = Completer<Object?>.sync(); @@ -142,7 +154,7 @@ /// /// 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]) { + void _send(String method, Object? parameters, [Object? 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 ' @@ -201,7 +213,6 @@ 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']); @@ -218,7 +229,6 @@ 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; @@ -244,3 +254,11 @@ _Request(this.method, this.completer, this.chain); } + +/// The default ID generator, uses an auto incrementing integer. +/// +/// Each call returns a new function which starts back a `0`. +int Function() _createIncrementingIdGenerator() { + var nextId = 0; + return () => nextId++; +}
diff --git a/pkgs/json_rpc_2/lib/src/peer.dart b/pkgs/json_rpc_2/lib/src/peer.dart index 677b6e1..71d9093 100644 --- a/pkgs/json_rpc_2/lib/src/peer.dart +++ b/pkgs/json_rpc_2/lib/src/peer.dart
@@ -59,12 +59,20 @@ /// 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( + /// + /// If [idGenerator] is passed, it will be called to generate an ID for each + /// request. Defaults to an auto-incrementing `int`. The value returned must + /// be either an `int` or `String`. + Peer( + StreamChannel<String> channel, { + ErrorCallback? onUnhandledError, + bool strictProtocolChecks = true, + Object Function()? idGenerator, + }) : this.withoutJson( jsonDocument.bind(channel).transform(respondToFormatExceptions), onUnhandledError: onUnhandledError, - strictProtocolChecks: strictProtocolChecks); + strictProtocolChecks: strictProtocolChecks, + idGenerator: idGenerator); /// Creates a [Peer] that communicates using decoded messages over [_channel]. /// @@ -81,14 +89,24 @@ /// some requests which are not conformant with the JSON-RPC 2.0 /// specification. In particular, requests missing the `jsonrpc` parameter /// will be accepted. + /// + /// If [idGenerator] is passed, it will be called to generate an ID for each + /// request. Defaults to an auto-incrementing `int`. The value returned must + /// be either an `int` or `String`. Peer.withoutJson(this._channel, - {ErrorCallback? onUnhandledError, bool strictProtocolChecks = true}) { + {ErrorCallback? onUnhandledError, + bool strictProtocolChecks = true, + Object Function()? idGenerator}) { _server = Server.withoutJson( StreamChannel(_serverIncomingForwarder.stream, _channel.sink), onUnhandledError: onUnhandledError, strictProtocolChecks: strictProtocolChecks); _client = Client.withoutJson( - StreamChannel(_clientIncomingForwarder.stream, _channel.sink)); + StreamChannel( + _clientIncomingForwarder.stream, + _channel.sink, + ), + idGenerator: idGenerator); } // Client methods.
diff --git a/pkgs/json_rpc_2/pubspec.yaml b/pkgs/json_rpc_2/pubspec.yaml index 7b42278..ad4e839 100644 --- a/pkgs/json_rpc_2/pubspec.yaml +++ b/pkgs/json_rpc_2/pubspec.yaml
@@ -1,5 +1,5 @@ name: json_rpc_2 -version: 3.0.3 +version: 4.0.0 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
diff --git a/pkgs/json_rpc_2/test/client/client_test.dart b/pkgs/json_rpc_2/test/client/client_test.dart index 1a4f65d..d907305 100644 --- a/pkgs/json_rpc_2/test/client/client_test.dart +++ b/pkgs/json_rpc_2/test/client/client_test.dart
@@ -11,16 +11,212 @@ void main() { late ClientController controller; - setUp(() => controller = ClientController()); + group('Default options', () { + setUp(() => controller = ClientController()); - test('sends a message and returns the response', () { + 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 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'))); + }); + }); + + test('with custom String ids', () { + var id = 0; + controller = ClientController(idGenerator: () => 'ID-${id++}'); controller.expectRequest((request) { expect( request, allOf([ containsPair('jsonrpc', '2.0'), containsPair('method', 'foo'), - containsPair('params', {'param': 'value'}) + containsPair('params', {'param': 'value'}), + containsPair('id', 'ID-0'), ])); return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']}; @@ -30,189 +226,23 @@ completion(equals('bar'))); }); - test('sends a message and returns the response with String id', () { + test('String ids are not parsed as ints', () { + var id = 0; + controller = ClientController(idGenerator: () => '${id++}'); controller.expectRequest((request) { expect( request, allOf([ containsPair('jsonrpc', '2.0'), containsPair('method', 'foo'), - containsPair('params', {'param': 'value'}) + containsPair('params', {'param': 'value'}), + containsPair('id', '0'), ])); - return { - 'jsonrpc': '2.0', - 'result': 'bar', - 'id': request['id'].toString() - }; + return {'jsonrpc': '2.0', 'result': 'bar', 'id': request['id']}; }); 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/utils.dart b/pkgs/json_rpc_2/test/client/utils.dart index 38e187f..ed308e5 100644 --- a/pkgs/json_rpc_2/test/client/utils.dart +++ b/pkgs/json_rpc_2/test/client/utils.dart
@@ -20,9 +20,10 @@ /// The client. late final json_rpc.Client client; - ClientController() { + ClientController({Object Function()? idGenerator}) { client = json_rpc.Client( - StreamChannel(_responseController.stream, _requestController.sink)); + StreamChannel(_responseController.stream, _requestController.sink), + idGenerator: idGenerator); client.listen(); }