Request now has the same body model as Response

BUG= https://code.google.com/p/dart/issues/detail?id=21041
R=nweiz@google.com

Review URL: https://codereview.chromium.org//851423003
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7b74b6..8c95a75 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.5.7
+
+* Updated `Request` to support the `body` model from `Response`.  
+
 ## 0.5.6
 
 * Fixed `createMiddleware` to only catch errors if `errorHandler` is provided.
diff --git a/lib/src/message.dart b/lib/src/message.dart
index ba5c5fa..db420d6 100644
--- a/lib/src/message.dart
+++ b/lib/src/message.dart
@@ -11,6 +11,7 @@
 import 'package:stack_trace/stack_trace.dart';
 
 import 'shelf_unmodifiable_map.dart';
+import 'util.dart';
 
 /// Represents logic shared between [Request] and [Response].
 abstract class Message {
@@ -45,11 +46,21 @@
 
   /// Creates a new [Message].
   ///
+  /// [body] is the response body. It may be either a [String], a
+  /// [Stream<List<int>>], or `null` to indicate no body. If it's a [String],
+  /// [encoding] is used to encode it to a [Stream<List<int>>]. It defaults to
+  /// UTF-8.
+  ///
   /// If [headers] is `null`, it is treated as empty.
-  Message(this._body,
-      {Map<String, String> headers, Map<String, Object> context})
-      : this.headers = new ShelfUnmodifiableMap<String>(headers,
-          ignoreKeyCase: true),
+  ///
+  /// 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".
+  Message(body, {Encoding encoding, Map<String, String> headers,
+      Map<String, Object> context})
+      : this._body = _bodyToStream(body, encoding),
+        this.headers = new ShelfUnmodifiableMap<String>(
+            _adjustHeaders(headers, encoding), ignoreKeyCase: true),
         this.context = new ShelfUnmodifiableMap<Object>(context,
             ignoreKeyCase: false);
 
@@ -130,3 +141,34 @@
   /// changes.
   Message change({Map<String, String> headers, Map<String, Object> context});
 }
+
+/// Converts [body] to a byte stream.
+///
+/// [body] may be either a [String], a [Stream<List<int>>], or `null`. If it's a
+/// [String], [encoding] will be used to convert it to a [Stream<List<int>>].
+Stream<List<int>> _bodyToStream(body, Encoding encoding) {
+  if (encoding == null) encoding = UTF8;
+  if (body == null) return new Stream.fromIterable([]);
+  if (body is String) return new Stream.fromIterable([encoding.encode(body)]);
+  if (body is Stream) return body;
+
+  throw new ArgumentError('Response body "$body" must be a String or a '
+      'Stream.');
+}
+
+/// Adds information about [encoding] to [headers].
+///
+/// Returns a new map without modifying [headers].
+Map<String, String> _adjustHeaders(
+    Map<String, String> headers, Encoding encoding) {
+  if (headers == null) headers = const {};
+  if (encoding == null) return headers;
+  if (headers['content-type'] == null) {
+    return addHeader(headers, 'content-type',
+        'application/octet-stream; charset=${encoding.name}');
+  }
+
+  var contentType = new MediaType.parse(headers['content-type']).change(
+      parameters: {'charset': encoding.name});
+  return addHeader(headers, 'content-type', contentType.toString());
+}
diff --git a/lib/src/request.dart b/lib/src/request.dart
index 5f6e960..253a516 100644
--- a/lib/src/request.dart
+++ b/lib/src/request.dart
@@ -5,6 +5,7 @@
 library shelf.request;
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:http_parser/http_parser.dart';
 
@@ -88,6 +89,15 @@
   /// Setting one of [url] or [scriptName] and not the other will throw an
   /// [ArgumentError].
   ///
+  /// [body] is the request body. It may be either a [String], a
+  /// [Stream<List<int>>], or `null` to indicate no body.
+  /// If it's a [String], [encoding] is used to encode it to a
+  /// [Stream<List<int>>]. The default encoding is UTF-8.
+  ///
+  /// 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".
+  ///
   /// The default value for [protocolVersion] is '1.1'.
   ///
   /// ## `onHijack`
@@ -117,8 +127,8 @@
   /// See also [hijack].
   // TODO(kevmoo) finish documenting the rest of the arguments.
   Request(String method, Uri requestedUri, {String protocolVersion,
-      Map<String, String> headers, Uri url, String scriptName,
-      Stream<List<int>> body, Map<String, Object> context,
+      Map<String, String> headers, Uri url, String scriptName, body,
+      Encoding encoding, Map<String, Object> context,
       OnHijackCallback onHijack})
       : this._(method, requestedUri,
           protocolVersion: protocolVersion,
@@ -126,6 +136,7 @@
           url: url,
           scriptName: scriptName,
           body: body,
+          encoding: encoding,
           context: context,
           onHijack: onHijack == null ? null : new _OnHijack(onHijack));
 
@@ -136,8 +147,8 @@
   /// source [Request] to ensure that [hijack] can only be called once, even
   /// from a changed [Request].
   Request._(this.method, Uri requestedUri, {String protocolVersion,
-      Map<String, String> headers, Uri url, String scriptName,
-      Stream<List<int>> body, Map<String, Object> context, _OnHijack onHijack})
+      Map<String, String> headers, Uri url, String scriptName, body,
+      Encoding encoding, Map<String, Object> context, _OnHijack onHijack})
       : this.requestedUri = requestedUri,
         this.protocolVersion = protocolVersion == null
             ? '1.1'
@@ -145,8 +156,7 @@
         this.url = _computeUrl(requestedUri, url, scriptName),
         this.scriptName = _computeScriptName(requestedUri, url, scriptName),
         this._onHijack = onHijack,
-        super(body == null ? new Stream.fromIterable([]) : body,
-            headers: headers, context: context) {
+        super(body, encoding: encoding, headers: headers, context: context) {
     if (method.isEmpty) throw new ArgumentError('method cannot be empty.');
 
     if (!requestedUri.isAbsolute) {
diff --git a/lib/src/response.dart b/lib/src/response.dart
index 0298813..16bd1da 100644
--- a/lib/src/response.dart
+++ b/lib/src/response.dart
@@ -4,7 +4,6 @@
 
 library shelf.response;
 
-import 'dart:async';
 import 'dart:convert';
 
 import 'package:http_parser/http_parser.dart';
@@ -124,7 +123,7 @@
       : this(statusCode,
             body: body,
             encoding: encoding,
-            headers: _addHeader(
+            headers: addHeader(
                 headers, 'location', _locationToString(location)),
             context: context);
 
@@ -136,7 +135,7 @@
   /// the old value should be used.
   Response.notModified({Map<String, String> headers,
     Map<String, Object> context})
-      : this(304, headers: _addHeader(
+      : this(304, headers: addHeader(
             headers, 'date', formatHttpDate(new DateTime.now())),
             context: context);
 
@@ -213,9 +212,7 @@
   /// Content-Type header, it will be set to "application/octet-stream".
   Response(this.statusCode, {body, Map<String, String> headers,
       Encoding encoding, Map<String, Object> context})
-      : super(_bodyToStream(body, encoding),
-          headers: _adjustHeaders(headers, encoding),
-          context: context) {
+      : super(body, encoding: encoding, headers: headers, context: context) {
     if (statusCode < 100) {
       throw new ArgumentError("Invalid status code: $statusCode.");
     }
@@ -242,59 +239,18 @@
   }
 }
 
-/// Converts [body] to a byte stream.
-///
-/// [body] may be either a [String], a [Stream<List<int>>], or `null`. If it's a
-/// [String], [encoding] will be used to convert it to a [Stream<List<int>>].
-Stream<List<int>> _bodyToStream(body, Encoding encoding) {
-  if (encoding == null) encoding = UTF8;
-  if (body == null) return new Stream.fromIterable([]);
-  if (body is String) return new Stream.fromIterable([encoding.encode(body)]);
-  if (body is Stream) return body;
-
-  throw new ArgumentError('Response body "$body" must be a String or a '
-      'Stream.');
-}
-
-/// Adds information about [encoding] to [headers].
-///
-/// Returns a new map without modifying [headers].
-Map<String, String> _adjustHeaders(
-    Map<String, String> headers, Encoding encoding) {
-  if (headers == null) headers = const {};
-  if (encoding == null) return headers;
-  if (headers['content-type'] == null) {
-    return _addHeader(headers, 'content-type',
-        'application/octet-stream; charset=${encoding.name}');
-  }
-
-  var contentType = new MediaType.parse(headers['content-type'])
-      .change(parameters: {'charset': encoding.name});
-  return _addHeader(headers, 'content-type', contentType.toString());
-}
-
-/// Adds a header with [name] and [value] to [headers], which may be null.
-///
-/// Returns a new map without modifying [headers].
-Map<String, String> _addHeader(
-    Map<String, String> headers, String name, String value) {
-  headers = headers == null ? {} : new Map.from(headers);
-  headers[name] = value;
-  return headers;
-}
-
 /// Adds content-type information to [headers].
 ///
 /// Returns a new map without modifying [headers]. This is used to add
 /// content-type information when creating a 500 response with a default body.
 Map<String, String> _adjustErrorHeaders(Map<String, String> headers) {
   if (headers == null || headers['content-type'] == null) {
-    return _addHeader(headers, 'content-type', 'text/plain');
+    return addHeader(headers, 'content-type', 'text/plain');
   }
 
   var contentType = new MediaType.parse(headers['content-type'])
       .change(mimeType: 'text/plain');
-  return _addHeader(headers, 'content-type', contentType.toString());
+  return addHeader(headers, 'content-type', contentType.toString());
 }
 
 /// Converts [location], which may be a [String] or a [Uri], to a [String].
diff --git a/lib/src/util.dart b/lib/src/util.dart
index 270ea6b..4154e8e 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -40,3 +40,13 @@
 
   return new Map.from(original)..addAll(updates);
 }
+
+/// Adds a header with [name] and [value] to [headers], which may be null.
+///
+/// Returns a new map without modifying [headers].
+Map<String, String> addHeader(
+    Map<String, String> headers, String name, String value) {
+  headers = headers == null ? {} : new Map.from(headers);
+  headers[name] = value;
+  return headers;
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 57ec073..db43786 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: shelf
-version: 0.5.7-dev
+version: 0.5.7
 author: Dart Team <misc@dartlang.org>
 description: Web Server Middleware for Dart
 homepage: https://github.com/dart-lang/shelf
diff --git a/test/message_test.dart b/test/message_test.dart
index 669d9ff..2ce5575 100644
--- a/test/message_test.dart
+++ b/test/message_test.dart
@@ -13,8 +13,9 @@
 import 'test_util.dart';
 
 class _TestMessage extends Message {
-  _TestMessage(Map<String, String> headers, Map<String, Object> context,
-      Stream<List<int>> body) : super(body, headers: headers, context: context);
+  _TestMessage(Map<String, String> headers, Map<String, Object> context, body,
+      Encoding encoding)
+      : super(body, headers: headers, context: context, encoding: encoding);
 
   Message change({Map<String, String> headers, Map<String, Object> context}) {
     throw new UnimplementedError();
@@ -22,9 +23,8 @@
 }
 
 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, context, body);
+    Map<String, Object> context, body, Encoding encoding}) {
+  return new _TestMessage(headers, context, body, encoding);
 }
 
 void main() {
@@ -202,5 +202,38 @@
         'content-type': 'text/plain; charset=iso-8859-1'
       }).encoding, equals(LATIN1));
     });
+
+    test("defaults to encoding a String as UTF-8", () {
+      expect(_createMessage(body: "è").read().toList(),
+          completion(equals([[195, 168]])));
+    });
+
+    test("uses the explicit encoding if available", () {
+      expect(_createMessage(body: "è", encoding: LATIN1).read().toList(),
+          completion(equals([[232]])));
+    });
+
+    test("adds an explicit encoding to the content-type", () {
+      var request = _createMessage(
+          body: "è", encoding: LATIN1, headers: {'content-type': 'text/plain'});
+      expect(request.headers,
+          containsPair('content-type', 'text/plain; charset=iso-8859-1'));
+    });
+
+    test("sets an absent content-type to application/octet-stream in order to "
+        "set the charset", () {
+      var request = _createMessage(body: "è", encoding: LATIN1);
+      expect(request.headers, containsPair(
+          'content-type', 'application/octet-stream; charset=iso-8859-1'));
+    });
+
+    test("overwrites an existing charset if given an explicit encoding", () {
+      var request = _createMessage(
+          body: "è",
+          encoding: LATIN1,
+          headers: {'content-type': 'text/plain; charset=whatever'});
+      expect(request.headers,
+          containsPair('content-type', 'text/plain; charset=iso-8859-1'));
+    });
   });
 }
diff --git a/test/request_test.dart b/test/request_test.dart
index 47b1b7f..fa75bd0 100644
--- a/test/request_test.dart
+++ b/test/request_test.dart
@@ -5,14 +5,16 @@
 library shelf.request_test;
 
 import 'dart:async';
+import 'dart:convert';
 
 import 'package:shelf/shelf.dart';
 import 'package:unittest/unittest.dart';
 
 import 'test_util.dart';
 
-Request _request([Map<String, String> headers, Stream<List<int>> body]) {
-  return new Request("GET", LOCALHOST_URI, headers: headers, body: body);
+Request _request({Map<String, String> headers, body, Encoding encoding}) {
+  return new Request("GET", LOCALHOST_URI,
+      headers: headers, body: body, encoding: encoding);
 }
 
 void main() {
@@ -125,8 +127,8 @@
     });
 
     test("comes from the Last-Modified header", () {
-      var request =
-          _request({'if-modified-since': 'Sun, 06 Nov 1994 08:49:37 GMT'});
+      var request = _request(
+          headers: {'if-modified-since': 'Sun, 06 Nov 1994 08:49:37 GMT'});
       expect(request.ifModifiedSince,
           equals(DateTime.parse("1994-11-06 08:49:37z")));
     });
diff --git a/test/response_test.dart b/test/response_test.dart
index 71ff58f..7c399de 100644
--- a/test/response_test.dart
+++ b/test/response_test.dart
@@ -27,40 +27,6 @@
     });
   });
 
-  group("new Response", () {
-    test("defaults to encoding a String as UTF-8", () {
-      expect(new Response.ok("è").read().toList(),
-          completion(equals([[195, 168]])));
-    });
-
-    test("uses the explicit encoding if available", () {
-      expect(new Response.ok("è", encoding: LATIN1).read().toList(),
-          completion(equals([[232]])));
-    });
-
-    test("adds an explicit encoding to the content-type", () {
-      var response = new Response.ok("è",
-          encoding: LATIN1, headers: {'content-type': 'text/plain'});
-      expect(response.headers,
-          containsPair('content-type', 'text/plain; charset=iso-8859-1'));
-    });
-
-    test("sets an absent content-type to application/octet-stream in order to "
-        "set the charset", () {
-      var response = new Response.ok("è", encoding: LATIN1);
-      expect(response.headers, containsPair(
-          'content-type', 'application/octet-stream; charset=iso-8859-1'));
-    });
-
-    test("overwrites an existing charset if given an explicit encoding", () {
-      var response = new Response.ok("è",
-          encoding: LATIN1,
-          headers: {'content-type': 'text/plain; charset=whatever'});
-      expect(response.headers,
-          containsPair('content-type', 'text/plain; charset=iso-8859-1'));
-    });
-  });
-
   group("new Response.internalServerError without a body", () {
     test('sets the body to "Internal Server Error"', () {
       var response = new Response.internalServerError();