nweiz@google.com 2014-04-16 01:23:01 +00:00
parent da7e681b60
commit f11614bb45
7 changed files with 88 additions and 30 deletions

View file

@ -28,4 +28,5 @@ Nikolaus Graf <nik@blossom.io>
Alexandre Ardhuin <alexandre.ardhuin@gmail.com>
Victor Berchet <victor.berchet@gmail.com>
Roel Spilker <r.spilker@gmail.com>
Martin Charles <martincharles07@gmail.com>
Martin Charles <martincharles07@gmail.com>
Anders Holmgren <andersmholmgren@gmail.com>

View file

@ -1,3 +1,8 @@
## 0.5.1
* Add a `context` map to `Request` and `Response` for passing data among
handlers and middleware.
## 0.5.0+1
* Allow `scheduled_test` development dependency up to v0.12.0

View file

@ -20,6 +20,19 @@ abstract class Message {
/// The value is immutable.
final Map<String, String> headers;
/// Extra context that can be used by for middleware and handlers.
///
/// For requests, this is used to pass data to inner middleware and handlers;
/// for responses, it's used to pass data to outer middleware and handlers.
///
/// Context properties that are used by a particular package should begin with
/// that package's name followed by a period. For example, if [logRequests]
/// wanted to take a prefix, its property name would be `"shelf.prefix"`,
/// since it's in the `shelf` package.
///
/// The value is immutable.
final Map<String, Object> context;
/// The streaming body of the message.
///
/// This can be read via [read] or [readAsString].
@ -28,8 +41,11 @@ abstract class Message {
/// Creates a new [Message].
///
/// If [headers] is `null`, it is treated as empty.
Message(this._body, {Map<String, String> headers})
: this.headers = _getIgnoreCaseMapView(headers);
Message(this._body, {Map<String, String> headers,
Map<String, Object> context})
: this.headers = _getIgnoreCaseMapView(headers),
this.context = new pc.UnmodifiableMapView(
context == null ? const {} : new Map.from(context));
/// The contents of the content-length field in [headers].
///

View file

@ -71,14 +71,14 @@ class Request extends Message {
// TODO(kevmoo) finish documenting the rest of the arguments.
Request(this.method, Uri requestedUri, {String protocolVersion,
Map<String, String> headers, Uri url, String scriptName,
Stream<List<int>> body})
Stream<List<int>> body, Map<String, Object> context})
: this.requestedUri = requestedUri,
this.protocolVersion = protocolVersion == null ?
'1.1' : protocolVersion,
this.url = _computeUrl(requestedUri, url, scriptName),
this.scriptName = _computeScriptName(requestedUri, url, scriptName),
super(body == null ? new Stream.fromIterable([]) : body,
headers: headers) {
headers: headers, context: context) {
if (method.isEmpty) throw new ArgumentError('method cannot be empty.');
// TODO(kevmoo) use isAbsolute property on Uri once Issue 18053 is fixed

View file

@ -53,8 +53,10 @@ class Response extends Message {
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.ok(body, {Map<String, String> headers, Encoding encoding})
: this(200, body: body, headers: headers, encoding: encoding);
Response.ok(body, {Map<String, String> headers, Encoding encoding,
Map<String, Object> context})
: this(200, body: body, headers: headers, encoding: encoding,
context: context);
/// Constructs a 301 Moved Permanently response.
///
@ -71,8 +73,9 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.movedPermanently(location, {body, Map<String, String> headers,
Encoding encoding})
: this._redirect(301, location, body, headers, encoding);
Encoding encoding, Map<String, Object> context})
: this._redirect(301, location, body, headers, encoding,
context: context);
/// Constructs a 302 Found response.
///
@ -89,8 +92,9 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.found(location, {body, Map<String, String> headers,
Encoding encoding})
: this._redirect(302, location, body, headers, encoding);
Encoding encoding, Map<String, Object> context})
: this._redirect(302, location, body, headers, encoding,
context: context);
/// Constructs a 303 See Other response.
///
@ -108,17 +112,20 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.seeOther(location, {body, Map<String, String> headers,
Encoding encoding})
: this._redirect(303, location, body, headers, encoding);
Encoding encoding, Map<String, Object> context})
: this._redirect(303, location, body, headers, encoding,
context: context);
/// Constructs a helper constructor for redirect responses.
Response._redirect(int statusCode, location, body,
Map<String, String> headers, Encoding encoding)
Map<String, String> headers, Encoding encoding,
{ Map<String, Object> context })
: this(statusCode,
body: body,
encoding: encoding,
headers: _addHeader(
headers, 'location', _locationToString(location)));
headers, 'location', _locationToString(location)),
context: context);
/// Constructs a 304 Not Modified response.
///
@ -126,9 +133,11 @@ class Response extends Message {
/// information used to determine whether the requested resource has changed
/// since the last request. It indicates that the resource has not changed and
/// the old value should be used.
Response.notModified({Map<String, String> headers})
Response.notModified({Map<String, String> headers,
Map<String, Object> context})
: this(304, headers: _addHeader(
headers, 'date', formatHttpDate(new DateTime.now())));
headers, 'date', formatHttpDate(new DateTime.now())),
context: context);
/// Constructs a 403 Forbidden response.
///
@ -143,8 +152,9 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.forbidden(body, {Map<String, String> headers,
Encoding encoding})
: this(403, body: body, headers: headers);
Encoding encoding, Map<String, Object> context})
: this(403, body: body, headers: headers,
context: context);
/// Constructs a 404 Not Found response.
///
@ -159,8 +169,10 @@ class Response extends Message {
/// If [encoding] is passed, the "encoding" field of the Content-Type header
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.notFound(body, {Map<String, String> headers, Encoding encoding})
: this(404, body: body, headers: headers);
Response.notFound(body, {Map<String, String> headers, Encoding encoding,
Map<String, Object> context})
: this(404, body: body, headers: headers,
context: context);
/// Constructs a 500 Internal Server Error response.
///
@ -176,10 +188,11 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response.internalServerError({body, Map<String, String> headers,
Encoding encoding})
Encoding encoding, Map<String, Object> context})
: this(500,
headers: body == null ? _adjust500Headers(headers) : headers,
body: body == null ? 'Internal Server Error' : body);
body: body == null ? 'Internal Server Error' : body,
context: context);
/// Constructs an HTTP response with the given [statusCode].
///
@ -194,9 +207,10 @@ class Response extends Message {
/// in [headers] will be set appropriately. If there is no existing
/// Content-Type header, it will be set to "application/octet-stream".
Response(this.statusCode, {body, Map<String, String> headers,
Encoding encoding})
Encoding encoding, Map<String, Object> context})
: super(_bodyToStream(body, encoding),
headers: _adjustHeaders(headers, encoding)) {
headers: _adjustHeaders(headers, encoding),
context: context) {
if (statusCode < 100) {
throw new ArgumentError("Invalid status code: $statusCode.");
}

View file

@ -1,5 +1,5 @@
name: shelf
version: 0.5.0+1
version: 0.5.1-dev
author: Dart Team <misc@dartlang.org>
description: Web Server Middleware for Dart
homepage: http://www.dartlang.org

View file

@ -11,13 +11,15 @@ import 'package:shelf/src/message.dart';
import 'package:unittest/unittest.dart';
class _TestMessage extends Message {
_TestMessage(Map<String, String> headers, Stream<List<int>> body)
: super(body, headers: headers);
_TestMessage(Map<String, String> headers, Map<String, Object> context,
Stream<List<int>> body)
: super(body, headers: headers, context: context);
}
Message _createMessage({Map<String, String> headers, Stream<List<int>> body}) {
Message _createMessage({Map<String, String> headers,
Map<String, Object> context, Stream<List<int>> body}) {
if (body == null) body = new Stream.fromIterable([]);
return new _TestMessage(headers, body);
return new _TestMessage(headers, context, body);
}
void main() {
@ -43,6 +45,26 @@ void main() {
expect(() => message.headers['h2'] = 'value2', throwsUnsupportedError);
});
});
group('context', () {
test('is accessible', () {
var message = _createMessage(context: {'foo': 'bar'});
expect(message.context, containsPair('foo', 'bar'));
});
test('null context value becomes empty and immutable', () {
var message = _createMessage();
expect(message.context, isEmpty);
expect(() => message.context['key'] = 'value', throwsUnsupportedError);
});
test('is immutable', () {
var message = _createMessage(context: {'key': 'value'});
expect(() => message.context['key'] = 'value', throwsUnsupportedError);
expect(() => message.context['key2'] = 'value', throwsUnsupportedError);
});
});
group("readAsString", () {
test("supports a null body", () {
var request = _createMessage();