Add sinks to provide synchronous access to events.

These are useful when doing chunked conversion in a synchronous context.

Closes #2

R=lrn@google.com

Review URL: https://codereview.chromium.org//1911053002 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c56ab1..88ee3b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 1.1.0
+
+* Add `AccumulatorSink`, `ByteAccumulatorSink`, and `StringAccumulatorSink`
+  classes for providing synchronous access to the output of chunked converters.
+
 ## 1.0.1
 
 * Small improvement in percent decoder efficiency.
diff --git a/lib/convert.dart b/lib/convert.dart
index 50b0296..ad543e9 100644
--- a/lib/convert.dart
+++ b/lib/convert.dart
@@ -4,5 +4,8 @@
 
 library convert;
 
+export 'src/accumulator_sink.dart';
+export 'src/byte_accumulator_sink.dart';
 export 'src/hex.dart';
 export 'src/percent.dart';
+export 'src/string_accumulator_sink.dart';
diff --git a/lib/src/accumulator_sink.dart b/lib/src/accumulator_sink.dart
new file mode 100644
index 0000000..ca35f7a
--- /dev/null
+++ b/lib/src/accumulator_sink.dart
@@ -0,0 +1,37 @@
+// 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:collection';
+
+/// A sink that provides access to all the [events] that have been passed to it.
+///
+/// See also [ChunkedConversionSink.withCallback].
+class AccumulatorSink<T> implements Sink<T> {
+  /// An unmodifiable list of events passed to this sink so far.
+  List<T> get events => new UnmodifiableListView(_events);
+  final _events = <T>[];
+
+  /// Whether [close] has been called.
+  bool get isClosed => _isClosed;
+  var _isClosed = false;
+
+  /// Removes all events from [events].
+  ///
+  /// This can be used to avoid double-processing events.
+  void clear() {
+    _events.clear();
+  }
+
+  void add(T event) {
+    if (_isClosed) {
+      throw new StateError("Can't add to a closed sink.");
+    }
+
+    _events.add(event);
+  }
+
+  void close() {
+    _isClosed = true;
+  }
+}
diff --git a/lib/src/byte_accumulator_sink.dart b/lib/src/byte_accumulator_sink.dart
new file mode 100644
index 0000000..10bd8ee
--- /dev/null
+++ b/lib/src/byte_accumulator_sink.dart
@@ -0,0 +1,53 @@
+// 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 'dart:typed_data';
+
+import 'package:typed_data/typed_data.dart';
+
+/// A sink that provides access to the concatenated bytes passed to it.
+///
+/// See also [ByteConversionSink.withCallback].
+class ByteAccumulatorSink extends ByteConversionSinkBase {
+  /// The bytes accumulated so far.
+  ///
+  /// The returned [Uint8List] is viewing a shared buffer, so it should not be
+  /// changed and any bytes outside the view should not be accessed.
+  Uint8List get bytes => new Uint8List.view(_buffer.buffer, 0, _buffer.length);
+
+  final _buffer = new Uint8Buffer();
+
+  /// Whether [close] has been called.
+  bool get isClosed => _isClosed;
+  var _isClosed = false;
+
+  /// Removes all bytes from [bytes].
+  ///
+  /// This can be used to avoid double-processing data.
+  void clear() {
+    _buffer.clear();
+  }
+
+  void add(List<int> bytes) {
+    if (_isClosed) {
+      throw new StateError("Can't add to a closed sink.");
+    }
+
+    _buffer.addAll(bytes);
+  }
+
+  void addSlice(List<int> chunk, int start, int end, bool isLast) {
+    if (_isClosed) {
+      throw new StateError("Can't add to a closed sink.");
+    }
+
+    _buffer.addAll(chunk, start, end);
+    if (isLast) _isClosed = true;
+  }
+
+  void close() {
+    _isClosed = true;
+  }
+}
diff --git a/lib/src/string_accumulator_sink.dart b/lib/src/string_accumulator_sink.dart
new file mode 100644
index 0000000..95e2248
--- /dev/null
+++ b/lib/src/string_accumulator_sink.dart
@@ -0,0 +1,46 @@
+// 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';
+
+/// A sink that provides access to the concatenated strings passed to it.
+///
+/// See also [StringConversionSink.withCallback].
+class StringAccumulatorSink extends StringConversionSinkBase {
+  /// The string accumulated so far.
+  String get string => _buffer.toString();
+  final _buffer = new StringBuffer();
+
+  /// Whether [close] has been called.
+  bool get isClosed => _isClosed;
+  var _isClosed = false;
+
+  /// Empties [string].
+  ///
+  /// This can be used to avoid double-processing data.
+  void clear() {
+    _buffer.clear();
+  }
+
+  void add(String chunk) {
+    if (_isClosed) {
+      throw new StateError("Can't add to a closed sink.");
+    }
+
+    _buffer.write(chunk);
+  }
+
+  void addSlice(String chunk, int start, int end, bool isLast) {
+    if (_isClosed) {
+      throw new StateError("Can't add to a closed sink.");
+    }
+
+    _buffer.write(chunk.substring(start, end));
+    if (isLast) _isClosed = true;
+  }
+
+  void close() {
+    _isClosed = true;
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 0dc393b..c70ea39 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: convert
-version: 1.0.1
+version: 1.1.0
 description: Utilities for converting between data representations.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/convert
diff --git a/test/accumulator_sink_test.dart b/test/accumulator_sink_test.dart
new file mode 100644
index 0000000..b0312f1
--- /dev/null
+++ b/test/accumulator_sink_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 'package:convert/convert.dart';
+import 'package:test/test.dart';
+
+void main() {
+  var sink;
+  setUp(() {
+    sink = new AccumulatorSink<int>();
+  });
+
+  test("provides access to events as they're added", () {
+    expect(sink.events, isEmpty);
+
+    sink.add(1);
+    expect(sink.events, equals([1]));
+
+    sink.add(2);
+    expect(sink.events, equals([1, 2]));
+
+    sink.add(3);
+    expect(sink.events, equals([1, 2, 3]));
+  });
+
+  test("clear() clears the events", () {
+    sink..add(1)..add(2)..add(3);
+    expect(sink.events, equals([1, 2, 3]));
+
+    sink.clear();
+    expect(sink.events, isEmpty);
+
+    sink..add(4)..add(5)..add(6);
+    expect(sink.events, equals([4, 5, 6]));
+  });
+
+  test("indicates whether the sink is closed", () {
+    expect(sink.isClosed, isFalse);
+    sink.close();
+    expect(sink.isClosed, isTrue);
+  });
+
+  test("doesn't allow add() to be called after close()", () {
+    sink.close();
+    expect(() => sink.add(1), throwsStateError);
+  });
+}
diff --git a/test/byte_accumulator_sink_test.dart b/test/byte_accumulator_sink_test.dart
new file mode 100644
index 0000000..27c043e
--- /dev/null
+++ b/test/byte_accumulator_sink_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 'package:convert/convert.dart';
+import 'package:test/test.dart';
+
+void main() {
+  var sink;
+  setUp(() {
+    sink = new ByteAccumulatorSink();
+  });
+
+  test("provides access to the concatenated bytes", () {
+    expect(sink.bytes, isEmpty);
+
+    sink.add([1, 2, 3]);
+    expect(sink.bytes, equals([1, 2, 3]));
+
+    sink.addSlice([4, 5, 6, 7, 8], 1, 4, false);
+    expect(sink.bytes, equals([1, 2, 3, 5, 6, 7]));
+  });
+
+  test("clear() clears the bytes", () {
+    sink.add([1, 2, 3]);
+    expect(sink.bytes, equals([1, 2, 3]));
+
+    sink.clear();
+    expect(sink.bytes, isEmpty);
+
+    sink.add([4, 5, 6]);
+    expect(sink.bytes, equals([4, 5, 6]));
+  });
+
+  test("indicates whether the sink is closed", () {
+    expect(sink.isClosed, isFalse);
+    sink.close();
+    expect(sink.isClosed, isTrue);
+  });
+
+  test("indicates whether the sink is closed via addSlice", () {
+    expect(sink.isClosed, isFalse);
+    sink.addSlice([], 0, 0, true);
+    expect(sink.isClosed, isTrue);
+  });
+
+  test("doesn't allow add() to be called after close()", () {
+    sink.close();
+    expect(() => sink.add([1]), throwsStateError);
+  });
+
+  test("doesn't allow addSlice() to be called after close()", () {
+    sink.close();
+    expect(() => sink.addSlice([], 0, 0, false), throwsStateError);
+  });
+}
diff --git a/test/string_accumulator_sink_test.dart b/test/string_accumulator_sink_test.dart
new file mode 100644
index 0000000..e6e30e6
--- /dev/null
+++ b/test/string_accumulator_sink_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 'package:convert/convert.dart';
+import 'package:test/test.dart';
+
+void main() {
+  var sink;
+  setUp(() {
+    sink = new StringAccumulatorSink();
+  });
+
+  test("provides access to the concatenated string", () {
+    expect(sink.string, isEmpty);
+
+    sink.add("foo");
+    expect(sink.string, equals("foo"));
+
+    sink.addSlice(" bar baz", 1, 4, false);
+    expect(sink.string, equals("foobar"));
+  });
+
+  test("clear() clears the string", () {
+    sink.add("foo");
+    expect(sink.string, equals("foo"));
+
+    sink.clear();
+    expect(sink.string, isEmpty);
+
+    sink.add("bar");
+    expect(sink.string, equals("bar"));
+  });
+
+  test("indicates whether the sink is closed", () {
+    expect(sink.isClosed, isFalse);
+    sink.close();
+    expect(sink.isClosed, isTrue);
+  });
+
+  test("indicates whether the sink is closed via addSlice", () {
+    expect(sink.isClosed, isFalse);
+    sink.addSlice("", 0, 0, true);
+    expect(sink.isClosed, isTrue);
+  });
+
+  test("doesn't allow add() to be called after close()", () {
+    sink.close();
+    expect(() => sink.add("x"), throwsStateError);
+  });
+
+  test("doesn't allow addSlice() to be called after close()", () {
+    sink.close();
+    expect(() => sink.addSlice("", 0, 0, false), throwsStateError);
+  });
+}