Add a document-by-document JSON transformer.

This makes it easy to send JSON over WebSocket connections, for example.

R=rnystrom@google.com

Review URL: https://codereview.chromium.org//1643843002 .
diff --git a/lib/src/json_document_transformer.dart b/lib/src/json_document_transformer.dart
new file mode 100644
index 0000000..8d8dcce
--- /dev/null
+++ b/lib/src/json_document_transformer.dart
@@ -0,0 +1,41 @@
+// 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:convert';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+import 'stream_channel_transformer.dart';
+
+/// The canonical instance of [JsonDocumentTransformer].
+final jsonDocument = new JsonDocumentTransformer();
+
+/// A [StreamChannelTransformer] that transforms JSON documents—strings that
+/// contain individual objects encoded as JSON—into decoded Dart objects.
+///
+/// This decodes JSON that's emitted by the transformed channel's stream, and
+/// encodes objects so that JSON is passed to the transformed channel's sink.
+class JsonDocumentTransformer
+    implements StreamChannelTransformer<String, Object> {
+  /// The underlying codec that implements the encoding and decoding logic.
+  final JsonCodec _codec;
+
+  /// Creates a new transformer.
+  ///
+  /// The [reviver] and [toEncodable] arguments work the same way as the
+  /// corresponding arguments to [new JsonCodec].
+  JsonDocumentTransformer({reviver(key, value), toEncodable(object)})
+      : _codec = new JsonCodec(reviver: reviver, toEncodable: toEncodable);
+
+  JsonDocumentTransformer._(this._codec);
+
+  StreamChannel bind(StreamChannel<String> channel) {
+    var stream = channel.stream.map(_codec.decode);
+    var sink = new StreamSinkTransformer.fromHandlers(handleData: (data, sink) {
+      sink.add(_codec.encode(data));
+    }).bind(channel.sink);
+    return new StreamChannel(stream, sink);
+  }
+}
diff --git a/lib/stream_channel.dart b/lib/stream_channel.dart
index e7fb055..f914188 100644
--- a/lib/stream_channel.dart
+++ b/lib/stream_channel.dart
@@ -8,6 +8,7 @@
 
 export 'src/delegating_stream_channel.dart';
 export 'src/isolate_channel.dart';
+export 'src/json_document_transformer.dart';
 export 'src/multi_channel.dart';
 export 'src/stream_channel_completer.dart';
 export 'src/stream_channel_transformer.dart';
diff --git a/test/json_document_transformer_test.dart b/test/json_document_transformer_test.dart
new file mode 100644
index 0000000..fec3d2a
--- /dev/null
+++ b/test/json_document_transformer_test.dart
@@ -0,0 +1,50 @@
+// 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 '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("decodes JSON emitted by the channel", () {
+    var transformed = channel.transform(jsonDocument);
+    streamController.add('{"foo": "bar"}');
+    expect(transformed.stream.first, completion(equals({"foo": "bar"})));
+  });
+
+  test("encodes objects added to the channel", () {
+    var transformed = channel.transform(jsonDocument);
+    transformed.sink.add({"foo": "bar"});
+    expect(sinkController.stream.first,
+        completion(equals(JSON.encode({"foo": "bar"}))));
+  });
+
+  test("supports the reviver function", () {
+    var transformed = channel.transform(
+        new JsonDocumentTransformer(reviver: (key, value) => "decoded"));
+    streamController.add('{"foo": "bar"}');
+    expect(transformed.stream.first, completion(equals("decoded")));
+  });
+
+  test("supports the toEncodable function", () {
+    var transformed = channel.transform(
+        new JsonDocumentTransformer(toEncodable: (object) => "encoded"));
+    transformed.sink.add(new Object());
+    expect(sinkController.stream.first, completion(equals('"encoded"')));
+  });
+}