diff --git a/.analysis_options.yaml b/.analysis_options similarity index 100% rename from .analysis_options.yaml rename to .analysis_options diff --git a/CHANGELOG.md b/CHANGELOG.md index d50c32f..79fcea3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.4.0 + +- **BREAKING** `readAs` to return a `Stream` and `Future` instead + - This is due to the potentially streaming nature of network and file i/o + - For example, HTTP servers commonly send chunked responses +- Added `readAsBytesAll` to auto-concatenate buffers together +- Fix various strong-mode warnings + ## 0.3.0 - Use `WebSocketChannel` as the backing implementation for sockets diff --git a/README.md b/README.md index 655fa4a..a256615 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Seltzer +[![pub package](https://img.shields.io/pub/v/seltzer.svg)](https://pub.dartlang.org/packages/seltzer) [![Build Status](https://travis-ci.org/matanlurey/seltzer.svg?branch=master)](https://travis-ci.org/matanlurey/seltzer) An elegant and rich cross-platform HTTP library for Dart. @@ -49,11 +50,10 @@ are expecting once: import 'package:seltzer/seltzer.dart' as seltzer; import 'package:seltzer/platform/browser.dart'; -void main() { +main() async { useSeltzerInTheBrowser(); - seltzer.get('some/url.json').send().first.then((response) { - print('Retrieved: ${response.payload}'); - }); + final response = await seltzer.get('some/url.json').send().first; + print('Retrieved: ${await response.readAsString()}'); } ``` diff --git a/lib/platform/testing.dart b/lib/platform/testing.dart index ffed98b..1856dd6 100644 --- a/lib/platform/testing.dart +++ b/lib/platform/testing.dart @@ -7,7 +7,7 @@ export 'package:seltzer/src/testing/replay.dart' show ReplaySeltzerHttp; /// Initializes `package:seltzer/seltzer.dart` to use [implementation]. /// /// This is appropriate for test implementations that want to use an existing -/// implementation, such as a [ReplaySeltzerHttp]. +/// implementation, such as a replay-based HTTP mock or server. void useSeltzerForTesting(SeltzerHttp implementation) { setHttpPlatform(implementation); } diff --git a/lib/platform/vm.dart b/lib/platform/vm.dart index c758a98..aceecd4 100644 --- a/lib/platform/vm.dart +++ b/lib/platform/vm.dart @@ -30,23 +30,20 @@ class VmSeltzerHttp extends SeltzerHttp { const VmSeltzerHttp._(); @override - Stream handle(SeltzerHttpRequest request, - [Object data]) { + Stream handle( + SeltzerHttpRequest request, [ + Object data, + ]) { return new HttpClient() .openUrl(request.method, Uri.parse(request.url)) .then((r) async { request.headers.forEach(r.headers.add); final response = await r.close(); - final payload = await response.first; final headers = {}; response.headers.forEach((name, value) { headers[name] = value.join(' '); }); - if (payload is String) { - return new SeltzerHttpResponse.fromString(payload, headers: headers); - } else { - return new SeltzerHttpResponse.fromBytes(payload, headers: headers); - } + return new SeltzerHttpResponse.fromBytes(response, headers: headers); }).asStream(); } } diff --git a/lib/src/interface/http_request.dart b/lib/src/interface/http_request.dart index 5f8ceee..5b72d5c 100644 --- a/lib/src/interface/http_request.dart +++ b/lib/src/interface/http_request.dart @@ -109,7 +109,7 @@ abstract class SeltzerHttpRequestBase extends SeltzerHtpRequestMixin { /// A reusable [Equality] implementation for [SeltzerHttpRequest]. /// -/// The default implementation of equality for [SeltzerHttpRequestMixin]. +/// The default implementation of equality for requests. class SeltzerHttpRequestEquality implements Equality { static const Equality _mapEquality = const MapEquality(); @@ -152,5 +152,7 @@ class _DefaultSeltzerHttpRequest extends SeltzerHttpRequestBase { : super(headers: headers, method: method, url: url); @override - Stream send([Object payload]) => _handler.handle(this, payload); + Stream send([Object payload]) { + return _handler.handle(this, payload); + } } diff --git a/lib/src/interface/http_response.dart b/lib/src/interface/http_response.dart index 1ff3641..187bac1 100644 --- a/lib/src/interface/http_response.dart +++ b/lib/src/interface/http_response.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'socket_message.dart'; /// An HTTP response object. @@ -16,7 +17,7 @@ class SeltzerHttpResponse extends SeltzerMessage { /// Create a new HTTP response with binary data. SeltzerHttpResponse.fromBytes( - List bytes, { + Stream> bytes, { Map headers: const {}, }) : this.headers = new Map.unmodifiable(headers), @@ -30,6 +31,7 @@ class SeltzerHttpResponse extends SeltzerMessage { : this.headers = new Map.unmodifiable(headers), super.fromString(string); + @override toJson() { return { 'data': super.toJson(), diff --git a/lib/src/interface/socket_message.dart b/lib/src/interface/socket_message.dart index a39e002..f69dd41 100644 --- a/lib/src/interface/socket_message.dart +++ b/lib/src/interface/socket_message.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'http.dart'; import 'socket.dart'; @@ -6,8 +8,10 @@ import 'socket.dart'; /// A single message response by a [SeltzerWebSocket] or [SeltzerHttp] client. class SeltzerMessage { final Encoding _encoding; - final List _bytes; - final String _string; + final int _length; + final Stream> _bytes; + + String _string; /// Create a new [SeltzerMessage] from string or binary data. factory SeltzerMessage(data) => data is String @@ -15,29 +19,42 @@ class SeltzerMessage { : new SeltzerMessage.fromBytes(data); /// Create a new [SeltzerMessage] from binary data. - SeltzerMessage.fromBytes(this._bytes, {Encoding encoding: UTF8}) + SeltzerMessage.fromBytes(this._bytes, {int length, Encoding encoding: UTF8}) : _encoding = encoding, + _length = length, _string = null; /// Create a new [SeltzerMessage] from text data. SeltzerMessage.fromString(this._string) : _encoding = null, + _length = null, _bytes = null; - /// Returns as bytes representing the the message's payload. - List readAsBytes() { - if (_string != null) { + /// Returns as a stream of bytes representing the message's payload. + /// + /// Some responses may be streamed into multiple chunks, which means that + /// listening to `stream.first` is not enough. To read the entire chunk in a + /// single call, use [readAsBytesAll]. + Stream> readAsBytes() => _bytes ?? readAsBytesAll().asStream(); + + /// Returns bytes representing the message's payload. + Future> readAsBytesAll() async { + if (_bytes == null) { return _string.codeUnits; } - return _bytes; + var offset = 0; + return _bytes.fold/*>*/( + new Uint8List(_length), + (buffer, value) { + buffer.setRange(offset, value.length, value); + offset += value.length; + }, + ); } /// Returns a string representing this message's payload. - String readAsString() { - if (_string != null) { - return _string; - } - return _encoding.decode(_bytes); + Future readAsString() async { + return _string ??= await _encoding.decodeStream(_bytes); } toJson() => _string ?? _bytes; diff --git a/lib/src/socket_impl.dart b/lib/src/socket_impl.dart index b872f9c..fbbe104 100644 --- a/lib/src/socket_impl.dart +++ b/lib/src/socket_impl.dart @@ -56,10 +56,14 @@ class ChannelWebSocket implements SeltzerWebSocket { static Future _decodeSocketMessage(payload) async { if (payload is ByteBuffer) { - return new SeltzerMessage.fromBytes(payload.asUint8List()); + return new SeltzerMessage.fromBytes(new Stream.fromIterable([ + payload.asUint8List(), + ])); } if (payload is TypedData) { - return new SeltzerMessage.fromBytes(payload.buffer.asUint8List()); + return new SeltzerMessage.fromBytes(new Stream.fromIterable([ + payload.buffer.asUint8List(), + ])); } return new SeltzerMessage.fromString(payload); } diff --git a/lib/src/testing/record.dart b/lib/src/testing/record.dart index 56c10bc..02e4ed2 100644 --- a/lib/src/testing/record.dart +++ b/lib/src/testing/record.dart @@ -26,7 +26,8 @@ class SeltzerHttpRecorder extends SeltzerHttpHandler { Object payload, ]) { SeltzerHttpResponse last; - final transformer = new StreamTransformer.fromHandlers( + final transformer = new StreamTransformer.fromHandlers( handleData: (event, sink) { last = event; sink.add(event); diff --git a/lib/src/testing/replay.dart b/lib/src/testing/replay.dart index cefaf2a..47ad9a9 100644 --- a/lib/src/testing/replay.dart +++ b/lib/src/testing/replay.dart @@ -13,7 +13,7 @@ class ReplaySeltzerHttp extends SeltzerHttp { Recording recording, ) = ReplaySeltzerHttp._; - /// Creates a new [ReplySeltzerHttp] from [pairs] of request/responses. + /// Creates a new [ReplaySeltzerHttp] from [pairs] of request/responses. factory ReplaySeltzerHttp.fromMap( Map pairs, ) { diff --git a/pubspec.yaml b/pubspec.yaml index b929dee..dac8b3f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: seltzer -version: 0.3.0 +version: 0.4.0 description: An elegant and rich cross-platform HTTP library for Dart. authors: - Matan Lurey diff --git a/test/common/http.dart b/test/common/http.dart index bdbd83e..be8b09f 100644 --- a/test/common/http.dart +++ b/test/common/http.dart @@ -9,7 +9,7 @@ const _echoUrl = 'http://localhost:9090'; void runHttpTests() { test('should make a valid DELETE request', () async { var response = await delete('$_echoUrl/das/fridge/lacroix').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': {}, 'method': 'DELETE', 'url': '/das/fridge/lacroix', @@ -19,7 +19,7 @@ void runHttpTests() { test('should make a valid GET request', () async { var response = await get('$_echoUrl/flags.json').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': {}, 'method': 'GET', 'url': '/flags.json', @@ -29,7 +29,7 @@ void runHttpTests() { test('should make a valid PATCH request', () async { var response = await patch('$_echoUrl/pants/up').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': {}, 'method': 'PATCH', 'url': '/pants/up', @@ -39,7 +39,7 @@ void runHttpTests() { test('should make a valid POST request', () async { var response = await post('$_echoUrl/users/clear').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': {}, 'method': 'POST', 'url': '/users/clear', @@ -49,7 +49,7 @@ void runHttpTests() { test('should make a valid PUT request', () async { var response = await put('$_echoUrl/pants/on').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': {}, 'method': 'PUT', 'url': '/pants/on', @@ -60,7 +60,7 @@ void runHttpTests() { test('should send an HTTP header', () async { var response = await (get(_echoUrl)..headers['Authorization'] = 'abc123').send().first; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'headers': { 'Authorization': 'abc123', }, diff --git a/test/common/ws.dart b/test/common/ws.dart index 878846d..12ef7b8 100644 --- a/test/common/ws.dart +++ b/test/common/ws.dart @@ -11,21 +11,20 @@ void runSocketTests() { group('$SeltzerWebSocket', () { SeltzerWebSocket webSocket; - setUp(() async { - webSocket = await connect(_echoUrl); + setUp(() { + webSocket = connect(_echoUrl); }); test('onClose should emit an event when the stream closes.', () async { webSocket.close(); - expect(webSocket.onClose, completion(isNotNull)); + expect(await webSocket.onClose, isNotNull); }); test('sendString should send string data.', () async { var payload = 'string data'; var completer = new Completer(); - - webSocket.onMessage.listen(((message) { - expect(message.readAsString(), payload); + webSocket.onMessage.listen(((message) async { + expect(await message.readAsString(), payload); completer.complete(); })); webSocket.sendString(payload); @@ -35,8 +34,8 @@ void runSocketTests() { test('sendBytes should send byte data.', () async { var payload = new Int8List.fromList([1, 2]); var completer = new Completer(); - webSocket.onMessage.listen((message) { - expect(message.readAsBytes(), payload); + webSocket.onMessage.listen((message) async { + expect(await message.readAsBytes().first, payload); completer.complete(); }); webSocket.sendBytes(payload.buffer); diff --git a/test/runtime/record_test.dart b/test/runtime/record_test.dart index 81c6442..19e791a 100644 --- a/test/runtime/record_test.dart +++ b/test/runtime/record_test.dart @@ -18,7 +18,7 @@ main() { runPingTest() async { final response = await http.post('$_echoUrl/ping').send().last; - expect(JSON.decode(response.readAsString()), { + expect(JSON.decode(await response.readAsString()), { 'data': '', 'headers': {}, 'method': 'POST', diff --git a/tool/presubmit.sh b/tool/presubmit.sh index f089ff0..947a9bf 100755 --- a/tool/presubmit.sh +++ b/tool/presubmit.sh @@ -14,7 +14,7 @@ echo "PASSED" # Make sure we pass the analyzer echo "Checking dartanalyzer..." -FAILS_ANALYZER="$(find lib test tool -name "*.dart" | xargs dartanalyzer --options analysis_options.yaml)" +FAILS_ANALYZER="$(find lib test tool -name "*.dart" | xargs dartanalyzer --options .analysis_options)" if [[ $FAILS_ANALYZER == *"[error]"* ]] then echo "FAILED" @@ -34,6 +34,6 @@ dart tool/echo/ws.dart & export SOCKET_ECHO_PID=$! # Run all of our tests # If anything fails, we kill the ECHO_PID, otherwise kill at the end. echo "Running all tests..." -pub run test -p "content-shell,vm" || kill $HTTP_ECHO_PID $SOCKET_ECHO_PID +pub run test -p "content-shell,vm,chrome" || kill $HTTP_ECHO_PID $SOCKET_ECHO_PID kill $HTTP_ECHO_PID $SOCKET_ECHO_PID