Add StreamChannel.transform().

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1639643002 .
diff --git a/pkgs/stream_channel/lib/stream_channel.dart b/pkgs/stream_channel/lib/stream_channel.dart
index ff36ec7..2885e2d 100644
--- a/pkgs/stream_channel/lib/stream_channel.dart
+++ b/pkgs/stream_channel/lib/stream_channel.dart
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
 
 export 'src/delegating_stream_channel.dart';
 export 'src/isolate_channel.dart';
@@ -66,6 +69,13 @@
   /// Connects [this] to [other], so that any values emitted by either are sent
   /// directly to the other.
   void pipe(StreamChannel<T> other);
+
+  /// Transforms [this] using [codec].
+  ///
+  /// This returns a stream channel that encodes all input using [Codec.encoder]
+  /// before passing it to this channel's [sink], and decodes all output from
+  /// this channel's [stream] using [Codec.decoder].
+  StreamChannel transform(Codec<dynamic, T> codec);
 }
 
 /// An implementation of [StreamChannel] that simply takes a stream and a sink
@@ -87,4 +97,12 @@
     stream.pipe(other.sink);
     other.stream.pipe(sink);
   }
+
+  StreamChannel transform(Codec<dynamic, T> codec) {
+    var sinkTransformer =
+        new StreamSinkTransformer.fromStreamTransformer(codec.encoder);
+    return new _StreamChannel(
+        stream.transform(codec.decoder),
+        sinkTransformer.bind(sink));
+  }
 }
diff --git a/pkgs/stream_channel/test/stream_channel_test.dart b/pkgs/stream_channel/test/stream_channel_test.dart
new file mode 100644
index 0000000..30a7db4
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_test.dart
@@ -0,0 +1,58 @@
+// Copyright (c) 2016, 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 'dart:convert';
+import 'dart:isolate';
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+  var streamController;
+  var sinkController;
+  var channel;
+  setUp(() {
+    streamController = new StreamController();
+    sinkController = new StreamController();
+    channel = new StreamChannel(
+        streamController.stream, sinkController.sink);
+  });
+
+  test("pipe() pipes data from each channel's stream into the other's sink",
+      () {
+    var otherStreamController = new StreamController();
+    var otherSinkController = new StreamController();
+    var otherChannel = new StreamChannel(
+        otherStreamController.stream, otherSinkController.sink);
+    channel.pipe(otherChannel);
+
+    streamController.add(1);
+    streamController.add(2);
+    streamController.add(3);
+    streamController.close();
+    expect(otherSinkController.stream.toList(), completion(equals([1, 2, 3])));
+
+    otherStreamController.add(4);
+    otherStreamController.add(5);
+    otherStreamController.add(6);
+    otherStreamController.close();
+    expect(sinkController.stream.toList(), completion(equals([4, 5, 6])));
+  });
+
+  test("transform() transforms the channel", () {
+    var transformed = channel.transform(UTF8);
+
+    streamController.add([102, 111, 111, 98, 97, 114]);
+    streamController.close();
+    expect(transformed.stream.toList(), completion(equals(["foobar"])));
+
+    transformed.sink.add("fblthp");
+    transformed.sink.close();
+    expect(sinkController.stream.toList(),
+        completion(equals([[102, 98, 108, 116, 104, 112]])));
+  });
+}