Skip to content

Add Cascade implementation #88

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions lib/src/cascade.dart
Original file line number Diff line number Diff line change
@@ -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<int> 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<Response>.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<int> 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);
}
157 changes: 157 additions & 0 deletions test/cascade_test.dart
Original file line number Diff line number Diff line change
@@ -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);
});
});
}