From e10091d5dedb2c62b38bd002c44296841affa25d Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 21 Jun 2017 20:08:07 -0700 Subject: [PATCH 1/2] Add Cascade implementation --- lib/src/cascade.dart | 101 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 lib/src/cascade.dart diff --git a/lib/src/cascade.dart b/lib/src/cascade.dart new file mode 100644 index 0000000000..6095761731 --- /dev/null +++ b/lib/src/cascade.dart @@ -0,0 +1,101 @@ +// Copyright (c) 2017, 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 'client.dart'; +import 'response.dart'; + +/// A typedef for [Cascade._shouldCascade]. +typedef bool _ShouldCascade(Response response); + +/// A helper that calls several clients in sequence and returns the first +/// acceptable response. +/// +/// By default, a response is considered acceptable if it has a status other +/// than 404 or 405; other statuses indicate that the client understood the +/// request. +/// +/// If all clients return unacceptable responses, the final response will be +/// returned. +/// +/// var client = new Cascade() +/// .add(webSocketHandler) +/// .add(staticFileHandler) +/// .add(application) +/// .client; +class Cascade { + /// The function used to determine whether the cascade should continue on to + /// the next client. + final _ShouldCascade _shouldCascade; + + final Cascade _parent; + final Client _client; + + /// Creates a new, empty cascade. + /// + /// If [statusCodes] is passed, responses with those status codes are + /// considered unacceptable. If [shouldCascade] is passed, responses for which + /// it returns `true` are considered unacceptable. [statusCode] and + /// [shouldCascade] may not both be passed. + Cascade({Iterable statusCodes, bool shouldCascade(Response response)}) + : _shouldCascade = _computeShouldCascade(statusCodes, shouldCascade), + _parent = null, + _client = null { + if (statusCodes != null && shouldCascade != null) { + throw new ArgumentError("statusCodes and shouldCascade may not both be " + "passed."); + } + } + + Cascade._(this._parent, this._client, this._shouldCascade); + + /// Returns a new cascade with [client] added to the end. + /// + /// [client] will only be called if all previous clients in the cascade + /// return unacceptable responses. + Cascade add(Client client) => new Cascade._(this, client, _shouldCascade); + + /// Exposes this cascade as a single client. + /// + /// This client will call each inner client in the cascade until one returns + /// an acceptable response, and return that. If no inner clients return an + /// acceptable response, this will return the final response. + Client get client { + if (_client == null) { + throw new StateError("Can't get a client for a cascade with no inner " + "clients."); + } + + return new Client.handler((request) async { + if (_parent._client == null) return _client.send(request); + + return _parent.client.send(request).then((response) => + _shouldCascade(response) + ? _client.send(request) + : new Future.value(response)); + }, onClose: () { + _client.close(); + + // Go up the chain closing the individual clients + var parent = _parent; + + while (parent != null) { + parent._client?.close(); + + parent = parent._parent; + } + }); + } +} + +/// Computes the [Cascade._shouldCascade] function based on the user's +/// parameters. +_ShouldCascade _computeShouldCascade( + Iterable statusCodes, bool shouldCascade(Response response)) { + if (shouldCascade != null) return shouldCascade; + if (statusCodes == null) statusCodes = [404, 405]; + statusCodes = statusCodes.toSet(); + return (response) => statusCodes.contains(response.statusCode); +} From 9e558aaec778524a62747c117c4885e2da8e9a5f Mon Sep 17 00:00:00 2001 From: Don Date: Wed, 21 Jun 2017 20:08:16 -0700 Subject: [PATCH 2/2] Add Cascade tests --- test/cascade_test.dart | 157 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 test/cascade_test.dart diff --git a/test/cascade_test.dart b/test/cascade_test.dart new file mode 100644 index 0000000000..9b5fb0cd77 --- /dev/null +++ b/test/cascade_test.dart @@ -0,0 +1,157 @@ +// 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:http/http.dart'; +import 'package:test/test.dart'; +// \TODO REMOVE +import 'package:http/src/cascade.dart'; + +final Uri _uri = Uri.parse('dart:http'); + +Client _handlerClient(int statusCode, String body) => + new Client.handler((_) async => new Response(_uri, statusCode, body: body)); + +void main() { + group('a cascade with several handlers', () { + Client client; + setUp(() { + client = new Cascade().add(new Client.handler((request) async { + var statusCode = request.headers['one'] == 'false' ? 404 : 200; + + return new Response(_uri, statusCode, body: 'handler 1'); + })).add(new Client.handler((request) async { + var statusCode = request.headers['two'] == 'false' ? 404 : 200; + + return new Response(_uri, statusCode, body: 'handler 2'); + })).add(new Client.handler((request) async { + var statusCode = request.headers['three'] == 'false' ? 404 : 200; + return new Response(_uri, statusCode, body: 'handler 3'); + })).client; + }); + + test('the first response should be returned if it matches', () async { + var response = await client.get(_uri); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('handler 1'))); + }); + + test( + "the second response should be returned if it matches and the first " + "doesn't", () async { + var request = new Request('GET', _uri, headers: {'one': 'false'}); + + var response = await client.send(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('handler 2'))); + }); + + test( + "the third response should be returned if it matches and the first " + "two don't", () async { + var request = + new Request('GET', _uri, headers: {'one': 'false', 'two': 'false'}); + + var response = await client.send(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('handler 3'))); + }); + + test('the third response should be returned if no response matches', + () async { + var request = new Request('GET', _uri, + headers: {'one': 'false', 'two': 'false', 'three': 'false'}); + + var response = await client.send(request); + expect(response.statusCode, equals(404)); + expect(response.readAsString(), completion(equals('handler 3'))); + }); + }); + + test('a 404 response triggers a cascade by default', () async { + var client = new Cascade() + .add(_handlerClient(404, 'handler 1')) + .add(_handlerClient(200, 'handler 2')) + .client; + + var response = await client.get(_uri); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('handler 2'))); + }); + + test('a 405 response triggers a cascade by default', () async { + var client = new Cascade() + .add(_handlerClient(405, '')) + .add(_handlerClient(200, 'handler 2')) + .client; + + var response = await client.get(_uri); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('handler 2'))); + }); + + test('[statusCodes] controls which statuses cause cascading', () async { + var client = new Cascade(statusCodes: [302, 403]) + .add(_handlerClient(302, '/')) + .add(_handlerClient(403, 'handler 2')) + .add(_handlerClient(404, 'handler 3')) + .add(_handlerClient(200, 'handler 4')) + .client; + + var response = await client.get(_uri); + expect(response.statusCode, equals(404)); + expect(response.readAsString(), completion(equals('handler 3'))); + }); + + test('[shouldCascade] controls which responses cause cascading', () async { + var client = + new Cascade(shouldCascade: (response) => response.statusCode % 2 == 1) + .add(_handlerClient(301, '/')) + .add(_handlerClient(403, 'handler 2')) + .add(_handlerClient(404, 'handler 3')) + .add(_handlerClient(200, 'handler 4')) + .client; + + var response = await client.get(_uri); + expect(response.statusCode, equals(404)); + expect(response.readAsString(), completion(equals('handler 3'))); + }); + + test('Cascade calls close on all clients', () { + int accessLocation = 0; + + var client = new Cascade() + .add(new Client.handler((_) async => null, onClose: () { + expect(accessLocation, 2); + accessLocation = 3; + })) + .add(new Client.handler((_) async => null, onClose: () { + expect(accessLocation, 1); + accessLocation = 2; + })) + .add(new Client.handler((_) async => null, onClose: () { + expect(accessLocation, 0); + accessLocation = 1; + })) + .client; + + client.close(); + expect(accessLocation, 3); + }); + + group('errors', () { + test('getting the handler for an empty cascade fails', () { + expect(() => new Cascade().client, throwsStateError); + }); + + test('passing [statusCodes] and [shouldCascade] at the same time fails', + () { + expect( + () => + new Cascade(statusCodes: [404, 405], shouldCascade: (_) => false), + throwsArgumentError); + }); + }); +}