Add 'body' named argument to Request/Response change method

R=nweiz@google.com

Review URL: https://codereview.chromium.org//1191033003.
diff --git a/.gitignore b/.gitignore
index 388eff0..7dbf035 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 .pub/
 build/
 packages
+.packages
 
 # Or the files created by dart2js.
 *.dart.js
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9754a56..76b7643 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.6.2
+
+* Added a `body` named argument to `change` method on `Request` and `Response`.
+
 ## 0.6.1+3
 
 * Updated minimum SDK to `1.9.0`.
diff --git a/lib/src/message.dart b/lib/src/message.dart
index c1347cb..93a0730 100644
--- a/lib/src/message.dart
+++ b/lib/src/message.dart
@@ -138,7 +138,8 @@
 
   /// Creates a new [Message] by copying existing values and applying specified
   /// changes.
-  Message change({Map<String, String> headers, Map<String, Object> context});
+  Message change({Map<String, String> headers, Map<String, Object> context,
+      body});
 }
 
 /// Converts [body] to a byte stream.
diff --git a/lib/src/request.dart b/lib/src/request.dart
index 7e4f869..b7882fe 100644
--- a/lib/src/request.dart
+++ b/lib/src/request.dart
@@ -190,6 +190,9 @@
   /// [Request]. All other context and header values from the [Request] will be
   /// included in the copied [Request] unchanged.
   ///
+  /// [body] is the request body. It may be either a [String] or a
+  /// [Stream<List<int>>].
+  ///
   /// [path] is used to update both [handlerPath] and [url]. It's designed for
   /// routing middleware, and represents the path from the current handler to
   /// the next handler. It must be a prefix of [url]; [handlerPath] becomes
@@ -203,10 +206,12 @@
   ///     print(request.handlerPath); // => /static/dir/
   ///     print(request.url);        // => file.html
   Request change({Map<String, String> headers, Map<String, Object> context,
-      String path}) {
+      String path, body}) {
     headers = updateMap(this.headers, headers);
     context = updateMap(this.context, context);
 
+    if (body == null) body = this.read();
+
     var handlerPath = this.handlerPath;
     if (path != null) handlerPath += path;
 
@@ -214,7 +219,7 @@
         protocolVersion: this.protocolVersion,
         headers: headers,
         handlerPath: handlerPath,
-        body: this.read(),
+        body: body,
         context: context,
         onHijack: _onHijack);
   }
diff --git a/lib/src/response.dart b/lib/src/response.dart
index 16bd1da..55863cb 100644
--- a/lib/src/response.dart
+++ b/lib/src/response.dart
@@ -230,11 +230,17 @@
   ///
   /// All other context and header values from the [Response] will be included
   /// in the copied [Response] unchanged.
-  Response change({Map<String, String> headers, Map<String, Object> context}) {
+  ///
+  /// [body] is the request body. It may be either a [String] or a
+  /// [Stream<List<int>>].
+  Response change(
+      {Map<String, String> headers, Map<String, Object> context, body}) {
     headers = updateMap(this.headers, headers);
     context = updateMap(this.context, context);
 
-    return new Response(this.statusCode, body: this.read(), headers: headers,
+    if (body == null) body = this.read();
+
+    return new Response(this.statusCode, body: body, headers: headers,
         context: context);
   }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index 26515fa..f6e2e6e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: shelf
-version: 0.6.1+3
+version: 0.6.2
 author: Dart Team <misc@dartlang.org>
 description: Web Server Middleware for Dart
 homepage: https://github.com/dart-lang/shelf
diff --git a/test/message_change_test.dart b/test/message_change_test.dart
index 617d26a..ed41de2 100644
--- a/test/message_change_test.dart
+++ b/test/message_change_test.dart
@@ -4,6 +4,9 @@
 
 library shelf.message_change_test;
 
+import 'dart:async';
+import 'dart:convert';
+
 import 'package:test/test.dart';
 
 import 'package:shelf/shelf.dart';
@@ -13,15 +16,15 @@
 
 void main() {
   group('Request', () {
-    _testChange(({headers, context}) {
-      return new Request('GET', LOCALHOST_URI,
+    _testChange(({body, headers, context}) {
+      return new Request('GET', LOCALHOST_URI, body: body,
           headers: headers, context: context);
     });
   });
 
   group('Response', () {
-    _testChange(({headers, context}) {
-      return new Response.ok(null, headers: headers, context: context);
+    _testChange(({body, headers, context}) {
+      return new Response.ok(body, headers: headers, context: context);
     });
   });
 }
@@ -29,7 +32,29 @@
 /// Shared test method used by [Request] and [Response] tests to validate
 /// the behavior of `change` with different `headers` and `context` values.
 void _testChange(Message factory(
-    {Map<String, String> headers, Map<String, Object> context})) {
+    {body, Map<String, String> headers, Map<String, Object> context})) {
+  group('body', () {
+    test('with String', () async {
+      var request = factory(body: 'Hello, world');
+      var copy = request.change(body: 'Goodbye, world');
+
+      var newBody = await copy.readAsString();
+
+      expect(newBody, equals('Goodbye, world'));
+    });
+
+    test('with Stream', () async {
+      var request = factory(body: 'Hello, world');
+      var copy = request.change(
+          body: new Stream.fromIterable(['Goodbye, world'])
+            .transform(UTF8.encoder));
+
+      var newBody = await copy.readAsString();
+
+      expect(newBody, equals('Goodbye, world'));
+    });
+  });
+
   test('with empty headers returns indentical instance', () {
     var request = factory(headers: {'header1': 'header value 1'});
     var copy = request.change(headers: {});
diff --git a/test/message_test.dart b/test/message_test.dart
index c03ec98..8a5abed 100644
--- a/test/message_test.dart
+++ b/test/message_test.dart
@@ -17,7 +17,8 @@
       Encoding encoding)
       : super(body, headers: headers, context: context, encoding: encoding);
 
-  Message change({Map<String, String> headers, Map<String, Object> context}) {
+  Message change({Map<String, String> headers, Map<String, Object> context,
+      body}) {
     throw new UnimplementedError();
   }
 }