Skip to content

Commit a4604c2

Browse files
authored
feat(dart_frog): add cascade (#98)
1 parent 1dfb53f commit a4604c2

File tree

4 files changed

+250
-0
lines changed

4 files changed

+250
-0
lines changed

packages/dart_frog/lib/dart_frog.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ library dart_frog;
33

44
export 'src/_internal.dart'
55
show
6+
Cascade,
67
Pipeline,
78
Request,
89
RequestContext,

packages/dart_frog/lib/src/_internal.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:http_methods/http_methods.dart' show isHttpMethod;
66
import 'package:shelf/shelf.dart' as shelf;
77
import 'package:shelf/shelf_io.dart' as shelf_io;
88

9+
part 'cascade.dart';
910
part 'pipeline.dart';
1011
part 'request.dart';
1112
part 'context.dart';
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
part of '_internal.dart';
2+
3+
/// {@template cascade}
4+
/// A class that supports calling multiple handlers
5+
/// in sequence and returns the first acceptable response.
6+
///
7+
/// By default, a response is considered acceptable if it has a status other
8+
/// than 404 or 405; other statuses indicate that the handler understood the
9+
/// request.
10+
///
11+
/// If all handlers return unacceptable responses, the final response will be
12+
/// returned.
13+
///
14+
/// ```dart
15+
/// final handler = Cascade()
16+
/// .add(staticAssetHandler)
17+
/// .add(router)
18+
/// .handler;
19+
/// ```
20+
/// {@endtemplate}
21+
class Cascade {
22+
/// {@macro cascade}
23+
Cascade({
24+
Iterable<int>? statusCodes,
25+
bool Function(Response)? shouldCascade,
26+
}) : this._(
27+
shelf.Cascade(
28+
statusCodes: statusCodes,
29+
shouldCascade: shouldCascade != null
30+
? (response) => shouldCascade(Response._(response))
31+
: null,
32+
),
33+
);
34+
35+
Cascade._(this._cascade);
36+
37+
final shelf.Cascade _cascade;
38+
39+
/// Returns a new [Cascade] instance with the [handler] added to the end.
40+
///
41+
/// The provided [handler] will only be called if all previous
42+
/// handlers in the cascade return unacceptable responses.
43+
Cascade add(Handler handler) {
44+
return Cascade._(_cascade.add(toShelfHandler(handler)));
45+
}
46+
47+
/// Exposes this cascade as a single handler.
48+
///
49+
/// This handler will call each inner handler in the cascade until one returns
50+
/// an acceptable response, and return that. If no inner handlers return an
51+
/// acceptable response, this will return the final response.
52+
Handler get handler => fromShelfHandler(_cascade.handler);
53+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog/dart_frog.dart';
4+
import 'package:mocktail/mocktail.dart';
5+
import 'package:test/test.dart';
6+
7+
final _request = Request('GET', _localhostUri);
8+
final _localhostUri = Uri.parse('http://localhost/');
9+
10+
Future<Response> _makeSimpleRequest(Handler handler) {
11+
return Future.sync(() => handler(_SimpleRequestContext()));
12+
}
13+
14+
class _MockRequestContext extends Mock implements RequestContext {}
15+
16+
class _SimpleRequestContext extends Fake implements RequestContext {
17+
@override
18+
Request get request => _request;
19+
}
20+
21+
void main() {
22+
group('Cascade', () {
23+
group('a cascade with several handlers', () {
24+
late Handler handler;
25+
26+
setUp(() {
27+
Response handler1(RequestContext context) {
28+
if (context.request.headers['one'] == 'false') {
29+
return Response(statusCode: HttpStatus.notFound, body: 'handler 1');
30+
} else {
31+
return Response(body: 'handler 1');
32+
}
33+
}
34+
35+
Response handler2(RequestContext context) {
36+
if (context.request.headers['two'] == 'false') {
37+
return Response(statusCode: HttpStatus.notFound, body: 'handler 2');
38+
} else {
39+
return Response(body: 'handler 2');
40+
}
41+
}
42+
43+
Response handler3(RequestContext context) {
44+
if (context.request.headers['three'] == 'false') {
45+
return Response(statusCode: HttpStatus.notFound, body: 'handler 3');
46+
} else {
47+
return Response(body: 'handler 3');
48+
}
49+
}
50+
51+
handler = Cascade().add(handler1).add(handler2).add(handler3).handler;
52+
});
53+
54+
test('the first response should be returned if it matches', () async {
55+
final response = await _makeSimpleRequest(handler);
56+
57+
expect(response.statusCode, equals(200));
58+
expect(response.body(), completion(equals('handler 1')));
59+
});
60+
61+
test(
62+
'the second response should be returned if it matches and the first '
63+
"doesn't", () async {
64+
final context = _MockRequestContext();
65+
final request = Request(
66+
'GET',
67+
_localhostUri,
68+
headers: {'one': 'false'},
69+
);
70+
when(() => context.request).thenReturn(request);
71+
72+
final response = await handler(context);
73+
74+
expect(response.statusCode, equals(200));
75+
expect(response.body(), completion(equals('handler 2')));
76+
});
77+
78+
test(
79+
'the third response should be returned if it matches and the first '
80+
"two don't", () async {
81+
final context = _MockRequestContext();
82+
final request = Request(
83+
'GET',
84+
_localhostUri,
85+
headers: {'one': 'false', 'two': 'false'},
86+
);
87+
when(() => context.request).thenReturn(request);
88+
89+
final response = await handler(context);
90+
91+
expect(response.statusCode, equals(200));
92+
expect(response.body(), completion(equals('handler 3')));
93+
});
94+
95+
test('the third response should be returned if no response matches',
96+
() async {
97+
final context = _MockRequestContext();
98+
final request = Request(
99+
'GET',
100+
_localhostUri,
101+
headers: {'one': 'false', 'two': 'false', 'three': 'false'},
102+
);
103+
when(() => context.request).thenReturn(request);
104+
105+
final response = await handler(context);
106+
107+
expect(response.statusCode, equals(404));
108+
expect(response.body(), completion(equals('handler 3')));
109+
});
110+
});
111+
112+
test('a 404 response triggers a cascade by default', () async {
113+
final handler = Cascade()
114+
.add(
115+
(_) => Response(statusCode: HttpStatus.notFound, body: 'handler 1'),
116+
)
117+
.add((_) => Response(body: 'handler 2'))
118+
.handler;
119+
120+
final response = await _makeSimpleRequest(handler);
121+
122+
expect(response.statusCode, equals(200));
123+
expect(response.body(), completion(equals('handler 2')));
124+
});
125+
126+
test('a 405 response triggers a cascade by default', () async {
127+
final handler = Cascade()
128+
.add((_) => Response(statusCode: 405))
129+
.add((_) => Response(body: 'handler 2'))
130+
.handler;
131+
132+
final response = await _makeSimpleRequest(handler);
133+
134+
expect(response.statusCode, equals(200));
135+
expect(response.body(), completion(equals('handler 2')));
136+
});
137+
138+
test('[statusCodes] controls which statuses cause cascading', () async {
139+
final handler = Cascade(statusCodes: [302, 403])
140+
.add((_) => Response(statusCode: HttpStatus.found, body: '/'))
141+
.add(
142+
(_) =>
143+
Response(statusCode: HttpStatus.forbidden, body: 'handler 2'),
144+
)
145+
.add(
146+
(_) => Response(statusCode: HttpStatus.notFound, body: 'handler 3'),
147+
)
148+
.add((_) => Response(body: 'handler 4'))
149+
.handler;
150+
151+
final response = await _makeSimpleRequest(handler);
152+
153+
expect(response.statusCode, equals(404));
154+
expect(response.body(), completion(equals('handler 3')));
155+
});
156+
157+
test('[shouldCascade] controls which responses cause cascading', () async {
158+
bool shouldCascade(Response response) => response.statusCode.isOdd;
159+
160+
final handler = Cascade(shouldCascade: shouldCascade)
161+
.add(
162+
(_) => Response(statusCode: HttpStatus.movedPermanently, body: '/'),
163+
)
164+
.add(
165+
(_) =>
166+
Response(statusCode: HttpStatus.forbidden, body: 'handler 2'),
167+
)
168+
.add(
169+
(_) => Response(statusCode: HttpStatus.notFound, body: 'handler 3'),
170+
)
171+
.add((_) => Response(body: 'handler 4'))
172+
.handler;
173+
174+
final response = await _makeSimpleRequest(handler);
175+
176+
expect(response.statusCode, equals(404));
177+
expect(response.body(), completion(equals('handler 3')));
178+
});
179+
180+
group('errors', () {
181+
test('getting the handler for an empty cascade fails', () {
182+
expect(() => Cascade().handler, throwsStateError);
183+
});
184+
185+
test(
186+
'passing [statusCodes] and [shouldCascade] '
187+
'at the same time fails', () {
188+
expect(
189+
() => Cascade(statusCodes: [404, 405], shouldCascade: (_) => false),
190+
throwsArgumentError,
191+
);
192+
});
193+
});
194+
});
195+
}

0 commit comments

Comments
 (0)