Bring the YAML package's style up to modern standards.

R=jmesserly@google.com, rnystrom@google.com

Review URL: https://codereview.chromium.org//274953002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/yaml@36386 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..695e069
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## 0.9.0+1
+
+* The `YamlMap` class is deprecated. In a future version, maps returned by
+  `loadYaml` and `loadYamlStream` will be Dart `HashMap`s with a custom equality
+  operation.
diff --git a/README.md b/README.md
index 47a5419..531162f 100644
--- a/README.md
+++ b/README.md
@@ -3,21 +3,27 @@
 Use `loadYaml` to load a single document, or `loadYamlStream` to load a
 stream of documents. For example:
 
-    import 'package:yaml/yaml.dart';
-    main() {
-      var doc = loadYaml("YAML: YAML Ain't Markup Language");
-      print(doc['YAML']);
-    }
+```dart
+import 'package:yaml/yaml.dart';
+
+main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language");
+  print(doc['YAML']);
+}
+```
 
 This library currently doesn't support dumping to YAML. You should use
 `JSON.encode` from `dart:convert` instead:
 
-    import 'dart:convert';
-    import 'package:yaml/yaml.dart';
-    main() {
-      var doc = loadYaml("YAML: YAML Ain't Markup Language");
-      print(JSON.encode(doc));
-    }
+```dart
+import 'dart:convert';
+import 'package:yaml/yaml.dart';
+
+main() {
+  var doc = loadYaml("YAML: YAML Ain't Markup Language");
+  print(JSON.encode(doc));
+}
+```
 
 The source code for this package is at <http://code.google.com/p/dart>.
 Please file issues at <http://dartbug.com>. Other questions or comments can be
diff --git a/lib/src/composer.dart b/lib/src/composer.dart
index 40b7667..3178096 100644
--- a/lib/src/composer.dart
+++ b/lib/src/composer.dart
@@ -2,7 +2,7 @@
 // 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.
 
-library composer;
+library yaml.composer;
 
 import 'model.dart';
 import 'visitor.dart';
@@ -19,10 +19,11 @@
   /// that anchor.
   final _anchors = <String, Node>{};
 
-  /// The next id to use for the represenation graph's anchors. The spec doesn't
-  /// use anchors in the representation graph, but we do so that the constructor
-  /// can ensure that the same node in the representation graph produces the
-  /// same native object.
+  /// The next id to use for the represenation graph's anchors.
+  ///
+  /// The spec doesn't use anchors in the representation graph, but we do so
+  /// that the constructor can ensure that the same node in the representation
+  /// graph produces the same native object.
   var _idCounter = 0;
 
   Composer(this._root);
@@ -33,13 +34,15 @@
   /// Returns the anchor to which an alias node refers.
   Node visitAlias(AliasNode alias) {
     if (!_anchors.containsKey(alias.anchor)) {
-      throw new YamlException("no anchor for alias ${alias.anchor}");
+      throw new YamlException("No anchor for alias ${alias.anchor}.");
     }
     return _anchors[alias.anchor];
   }
 
   /// Parses a scalar node according to its tag, or auto-detects the type if no
-  /// tag exists. Currently this only supports the YAML core type schema.
+  /// tag exists.
+  ///
+  /// Currently this only supports the YAML core type schema.
   Node visitScalar(ScalarNode scalar) {
     if (scalar.tag.name == "!") {
       return setAnchor(scalar, parseString(scalar.content));
@@ -51,30 +54,31 @@
       return setAnchor(scalar, parseString(scalar.content));
     }
 
-    // TODO(nweiz): support the full YAML type repository
-    var tagParsers = {
-      'null': parseNull, 'bool': parseBool, 'int': parseInt,
-      'float': parseFloat, 'str': parseString
-    };
+    var result = _parseByTag(scalar);
+    if (result != null) return setAnchor(scalar, result);
+    throw new YamlException('Invalid literal for ${scalar.tag}: '
+        '"${scalar.content}".');
+  }
 
-    for (var key in tagParsers.keys) {
-      if (scalar.tag.name != Tag.yaml(key)) continue;
-      var result = tagParsers[key](scalar.content);
-      if (result != null) return setAnchor(scalar, result);
-      throw new YamlException('invalid literal for $key: "${scalar.content}"');
+  ScalarNode _parseByTag(ScalarNode scalar) {
+    switch (scalar.tag.name) {
+      case "null": return parseNull(scalar.content);
+      case "bool": return parseBool(scalar.content);
+      case "int": return parseInt(scalar.content);
+      case "float": return parseFloat(scalar.content);
+      case "str": return parseString(scalar.content);
     }
-
-    throw new YamlException('undefined tag: "${scalar.tag.name}"');
+    throw new YamlException('Undefined tag: ${scalar.tag}.');
   }
 
   /// Assigns a tag to the sequence and recursively composes its contents.
   Node visitSequence(SequenceNode seq) {
     var tagName = seq.tag.name;
     if (tagName != "!" && tagName != "?" && tagName != Tag.yaml("seq")) {
-      throw new YamlException("invalid tag for sequence: ${tagName}");
+      throw new YamlException("Invalid tag for sequence: ${seq.tag}.");
     }
 
-    var result = setAnchor(seq, new SequenceNode(Tag.yaml("seq"), null));
+    var result = setAnchor(seq, new SequenceNode(Tag.yaml('seq'), null));
     result.content = super.visitSequence(seq);
     return result;
   }
@@ -83,10 +87,10 @@
   Node visitMapping(MappingNode map) {
     var tagName = map.tag.name;
     if (tagName != "!" && tagName != "?" && tagName != Tag.yaml("map")) {
-      throw new YamlException("invalid tag for mapping: ${tagName}");
+      throw new YamlException("Invalid tag for mapping: ${map.tag}.");
     }
 
-    var result = setAnchor(map, new MappingNode(Tag.yaml("map"), null));
+    var result = setAnchor(map, new MappingNode(Tag.yaml('map'), null));
     result.content = super.visitMapping(map);
     return result;
   }
diff --git a/lib/src/constructor.dart b/lib/src/constructor.dart
index 428c476..116809d 100644
--- a/lib/src/constructor.dart
+++ b/lib/src/constructor.dart
@@ -2,7 +2,7 @@
 // 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.
 
-library constructor;
+library yaml.constructor;
 
 import 'model.dart';
 import 'visitor.dart';
diff --git a/lib/src/deep_equals.dart b/lib/src/deep_equals.dart
index 1e1f7ed..68fb236 100644
--- a/lib/src/deep_equals.dart
+++ b/lib/src/deep_equals.dart
@@ -2,75 +2,80 @@
 // 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.
 
-library deep_equals;
+library yaml.deep_equals;
 
 /// Returns whether two objects are structurally equivalent.
 ///
 /// This considers `NaN` values to be equivalent. It also handles
 /// self-referential structures.
-bool deepEquals(obj1, obj2, [List parents1, List parents2]) {
-  if (identical(obj1, obj2)) return true;
-  if (parents1 == null) {
-    parents1 = [];
-    parents2 = [];
-  }
+bool deepEquals(obj1, obj2) => new _DeepEquals().equals(obj1, obj2);
 
-  // parents1 and parents2 are guaranteed to be the same size.
-  for (var i = 0; i < parents1.length; i++) {
-    var loop1 = identical(obj1, parents1[i]);
-    var loop2 = identical(obj2, parents2[i]);
-    // If both structures loop in the same place, they're equal at that point in
-    // the structure. If one loops and the other doesn't, they're not equal.
-    if (loop1 && loop2) return true;
-    if (loop1 || loop2) return false;
-  }
+/// A class that provides access to the list of parent objects used for loop
+/// detection.
+class _DeepEquals {
+  final _parents1 = [];
+  final _parents2 = [];
 
-  parents1.add(obj1);
-  parents2.add(obj2);
-  try {
-    if (obj1 is List && obj2 is List) {
-      return _listEquals(obj1, obj2, parents1, parents2);
-    } else if (obj1 is Map && obj2 is Map) {
-      return _mapEquals(obj1, obj2, parents1, parents2);
-    } else if (obj1 is num && obj2 is num) {
-      return _numEquals(obj1, obj2);
-    } else {
-      return obj1 == obj2;
+  /// Returns whether [obj1] and [obj2] are structurally equivalent.
+  bool equals(obj1, obj2) {
+    // _parents1 and _parents2 are guaranteed to be the same size.
+    for (var i = 0; i < _parents1.length; i++) {
+      var loop1 = identical(obj1, _parents1[i]);
+      var loop2 = identical(obj2, _parents2[i]);
+      // If both structures loop in the same place, they're equal at that point
+      // in the structure. If one loops and the other doesn't, they're not
+      // equal.
+      if (loop1 && loop2) return true;
+      if (loop1 || loop2) return false;
     }
-  } finally {
-    parents1.removeLast();
-    parents2.removeLast();
-  }
-}
 
-/// Returns whether [list1] and [list2] are structurally equal. 
-bool _listEquals(List list1, List list2, List parents1, List parents2) {
-  if (list1.length != list2.length) return false;
-
-  for (var i = 0; i < list1.length; i++) {
-    if (!deepEquals(list1[i], list2[i], parents1, parents2)) return false;
+    _parents1.add(obj1);
+    _parents2.add(obj2);
+    try {
+      if (obj1 is List && obj2 is List) {
+        return _listEquals(obj1, obj2);
+      } else if (obj1 is Map && obj2 is Map) {
+        return _mapEquals(obj1, obj2);
+      } else if (obj1 is num && obj2 is num) {
+        return _numEquals(obj1, obj2);
+      } else {
+        return obj1 == obj2;
+      }
+    } finally {
+      _parents1.removeLast();
+      _parents2.removeLast();
+    }
   }
 
-  return true;
-}
+  /// Returns whether [list1] and [list2] are structurally equal. 
+  bool _listEquals(List list1, List list2) {
+    if (list1.length != list2.length) return false;
 
-/// Returns whether [map1] and [map2] are structurally equal. 
-bool _mapEquals(Map map1, Map map2, List parents1, List parents2) {
-  if (map1.length != map2.length) return false;
+    for (var i = 0; i < list1.length; i++) {
+      if (!equals(list1[i], list2[i])) return false;
+    }
 
-  for (var key in map1.keys) {
-    if (!map2.containsKey(key)) return false;
-    if (!deepEquals(map1[key], map2[key], parents1, parents2)) return false;
+    return true;
   }
 
-  return true;
-}
+  /// Returns whether [map1] and [map2] are structurally equal. 
+  bool _mapEquals(Map map1, Map map2) {
+    if (map1.length != map2.length) return false;
 
-/// Returns whether two numbers are equivalent.
-///
-/// This differs from `n1 == n2` in that it considers `NaN` to be equal to
-/// itself.
-bool _numEquals(num n1, num n2) {
-  if (n1.isNaN && n2.isNaN) return true;
-  return n1 == n2;
+    for (var key in map1.keys) {
+      if (!map2.containsKey(key)) return false;
+      if (!equals(map1[key], map2[key])) return false;
+    }
+
+    return true;
+  }
+
+  /// Returns whether two numbers are equivalent.
+  ///
+  /// This differs from `n1 == n2` in that it considers `NaN` to be equal to
+  /// itself.
+  bool _numEquals(num n1, num n2) {
+    if (n1.isNaN && n2.isNaN) return true;
+    return n1 == n2;
+  }
 }
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 37247f0..564cac6 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -5,39 +5,38 @@
 /// This file contains the node classes for the internal representations of YAML
 /// documents. These nodes are used for both the serialization tree and the
 /// representation graph.
-library model;
+library yaml.model;
 
 import 'parser.dart';
 import 'utils.dart';
 import 'visitor.dart';
 import 'yaml_exception.dart';
 
+/// The prefix for tag types defined by the YAML spec.
+const _YAML_URI_PREFIX = "tag:yaml.org,2002:";
+
 /// A tag that indicates the type of a YAML node.
 class Tag {
-  // TODO(nweiz): it would better match the semantics of the spec if there were
-  // a singleton instance of this class for each tag.
-
-  static const SCALAR_KIND = 0;
-  static const SEQUENCE_KIND = 1;
-  static const MAPPING_KIND = 2;
-
-  static const String YAML_URI_PREFIX = 'tag:yaml.org,2002:';
-
   /// The name of the tag, either a URI or a local tag beginning with "!".
   final String name;
 
-  /// The kind of the tag: SCALAR_KIND, SEQUENCE_KIND, or MAPPING_KIND.
-  final int kind;
-
-  Tag(this.name, this.kind);
-
-  Tag.scalar(String name) : this(name, SCALAR_KIND);
-  Tag.sequence(String name) : this(name, SEQUENCE_KIND);
-  Tag.mapping(String name) : this(name, MAPPING_KIND);
+  /// The kind of the tag.
+  final TagKind kind;
 
   /// Returns the standard YAML tag URI for [type].
   static String yaml(String type) => "tag:yaml.org,2002:$type";
 
+  const Tag(this.name, this.kind);
+
+  const Tag.scalar(String name)
+      : this(name, TagKind.SCALAR);
+
+  const Tag.sequence(String name)
+      : this(name, TagKind.SEQUENCE);
+
+  const Tag.mapping(String name)
+      : this(name, TagKind.MAPPING);
+
   /// Two tags are equal if their URIs are equal.
   operator ==(other) {
     if (other is! Tag) return false;
@@ -45,8 +44,8 @@
   }
 
   String toString() {
-    if (name.startsWith(YAML_URI_PREFIX)) {
-      return '!!${name.substring(YAML_URI_PREFIX.length)}';
+    if (name.startsWith(_YAML_URI_PREFIX)) {
+      return '!!${name.substring(_YAML_URI_PREFIX.length)}';
     } else {
       return '!<$name>';
     }
@@ -55,6 +54,24 @@
   int get hashCode => name.hashCode;
 }
 
+/// An enum for kinds of tags.
+class TagKind {
+  /// A tag indicating that the value is a scalar.
+  static const SCALAR = const TagKind._("scalar");
+
+  /// A tag indicating that the value is a sequence.
+  static const SEQUENCE = const TagKind._("sequence");
+
+  /// A tag indicating that the value is a mapping.
+  static const MAPPING = const TagKind._("mapping");
+
+  final String name;
+
+  const TagKind._(this.name);
+
+  String toString() => name;
+}
+
 /// The abstract class for YAML nodes.
 abstract class Node {
   /// Every YAML node has a tag that describes its type.
@@ -185,7 +202,7 @@
       return '"${escapedValue.join()}"';
     }
 
-    throw new YamlException("unknown scalar value: $value");
+    throw new YamlException('Unknown scalar value: "$value".');
   }
 
   String toString() => '$tag "$content"';
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index 162fef4..d1e20ff 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -2,7 +2,7 @@
 // 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.
 
-library parser;
+library yaml.parser;
 
 import 'dart:collection';
 
@@ -407,23 +407,24 @@
   error(String message) {
     // Line and column should be one-based.
     throw new SyntaxError(_line + 1, _column + 1,
-        "$message (in $_farthestContext)");
+        "$message (in $_farthestContext).");
   }
 
   /// If [result] is falsey, throws an error saying that [expected] was
   /// expected.
   expect(result, String expected) {
     if (truth(result)) return result;
-    error("expected $expected");
+    error("Expected $expected");
   }
 
   /// Throws an error saying that the parse failed. Uses [_farthestLine],
-  /// [_farthestColumn], and [_farthestContext] to provide additional information.
+  /// [_farthestColumn], and [_farthestContext] to provide additional
+  /// information.
   parseFailed() {
-    var message = "invalid YAML in $_farthestContext";
+    var message = "Invalid YAML in $_farthestContext";
     var extraError = _errorAnnotations[_farthestPos];
     if (extraError != null) message = "$message ($extraError)";
-    throw new SyntaxError(_farthestLine + 1, _farthestColumn + 1, message);
+    throw new SyntaxError(_farthestLine + 1, _farthestColumn + 1, "$message.");
   }
 
   /// Returns the number of spaces after the current position.
@@ -788,7 +789,7 @@
     case BLOCK_KEY:
     case FLOW_KEY:
       return s_separateInLine();
-    default: throw 'invalid context "$ctx"';
+    default: throw 'Invalid context "$ctx".';
     }
   }
 
@@ -1014,7 +1015,7 @@
     var char = peek();
     var indicator = indicatorType(char);
     if (indicator == C_RESERVED) {
-      error("reserved indicators can't start a plain scalar");
+      error("Reserved indicators can't start a plain scalar");
     }
     var match = (isNonSpace(char) && indicator == null) ||
       ((indicator == C_MAPPING_KEY ||
@@ -1037,7 +1038,7 @@
     case FLOW_KEY:
       // 129
       return isNonSpace(char) && !isFlowIndicator(char);
-    default: throw 'invalid context "$ctx"';
+    default: throw 'Invalid context "$ctx".';
     }
   }
 
@@ -1066,7 +1067,7 @@
       case BLOCK_KEY:
       case FLOW_KEY:
         return ns_plainOneLine(ctx);
-      default: throw 'invalid context "$ctx"';
+      default: throw 'Invalid context "$ctx".';
       }
     });
   });
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index a87555d..64cad47 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -2,38 +2,33 @@
 // 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.
 
-library utils;
+library yaml.utils;
 
-/// Returns the hash code for [obj]. This includes null, true, false, maps, and
-/// lists. Also handles self-referential structures.
-int hashCodeFor(obj, [List parents]) {
-  if (parents == null) {
-    parents = [];
-  } else if (parents.any((p) => identical(p, obj))) {
-    return -1;
-  }
+import 'package:collection/collection.dart';
 
-  parents.add(obj);
-  try {
-    if (obj == null) return 0;
-    if (obj == true) return 1;
-    if (obj == false) return 2;
-    if (obj is Map) {
-      return hashCodeFor(obj.keys, parents) ^
-        hashCodeFor(obj.values, parents);
-    }
-    if (obj is Iterable) {
-      // This is probably a really bad hash function, but presumably we'll get
-      // this in the standard library before it actually matters.
-      int hash = 0;
-      for (var e in obj) {
-        hash ^= hashCodeFor(e, parents);
+/// Returns a hash code for [obj] such that structurally equivalent objects
+/// will have the same hash code.
+///
+/// This supports deep equality for maps and lists, including those with
+/// self-referential structures.
+int hashCodeFor(obj) {
+  var parents = [];
+
+  _hashCodeFor(value) {
+    if (parents.any((parent) => identical(parent, value))) return -1;
+
+    parents.add(value);
+    try {
+      if (value is Map) {
+        return _hashCodeFor(value.keys) ^ _hashCodeFor(value.values);
+      } else if (value is Iterable) {
+        return const IterableEquality().hash(value.map(hashCodeFor));
       }
-      return hash;
+      return value.hashCode;
+    } finally {
+      parents.removeLast();
     }
-    return obj.hashCode;
-  } finally {
-    parents.removeLast();
   }
-}
 
+  return _hashCodeFor(obj);
+}
diff --git a/lib/src/visitor.dart b/lib/src/visitor.dart
index 4a9c54f..7095268 100644
--- a/lib/src/visitor.dart
+++ b/lib/src/visitor.dart
@@ -2,7 +2,7 @@
 // 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.
 
-library visitor;
+library yaml.visitor;
 
 import 'model.dart';
 import 'yaml_map.dart';
diff --git a/lib/src/yaml_exception.dart b/lib/src/yaml_exception.dart
index 66697a1..a863274 100644
--- a/lib/src/yaml_exception.dart
+++ b/lib/src/yaml_exception.dart
@@ -2,7 +2,7 @@
 // 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.
 
-library yaml_exception;
+library yaml.exception;
 
 /// An error thrown by the YAML processor.
 class YamlException implements Exception {
diff --git a/lib/src/yaml_map.dart b/lib/src/yaml_map.dart
index 5e3e20a..ff3da36 100644
--- a/lib/src/yaml_map.dart
+++ b/lib/src/yaml_map.dart
@@ -2,86 +2,39 @@
 // 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.
 
-library yaml_map;
+library yaml.map;
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
 
 import 'deep_equals.dart';
 import 'utils.dart';
 
-/// This class wraps behaves almost identically to the normal Dart Map
+/// This class behaves almost identically to the normal Dart [Map]
 /// implementation, with the following differences:
 ///
-///  *  It allows null, NaN, boolean, list, and map keys.
+///  *  It allows NaN, list, and map keys.
 ///  *  It defines `==` structurally. That is, `yamlMap1 == yamlMap2` if they
 ///     have the same contents.
 ///  *  It has a compatible [hashCode] method.
-class YamlMap implements Map {
-  final Map _map;
+///
+/// This class is deprecated. In future releases, this package will use
+/// a [HashMap] with a custom equality operation rather than a custom class.
+@Deprecated('1.0.0')
+class YamlMap extends DelegatingMap {
+  YamlMap()
+      : super(new HashMap(equals: deepEquals, hashCode: hashCodeFor));
 
-  YamlMap() : _map = new Map();
-
-  YamlMap.from(Map map) : _map = new Map.from(map);
-
-  YamlMap._wrap(this._map);
-
-  void addAll(Map other) {
-    other.forEach((key, value) {
-      this[key] = value;
-    });
+  YamlMap.from(Map map)
+      : super(new HashMap(equals: deepEquals, hashCode: hashCodeFor)) {
+    addAll(map);
   }
 
-  bool containsValue(value) => _map.containsValue(value);
-  bool containsKey(key) => _map.containsKey(_wrapKey(key));
-  operator [](key) => _map[_wrapKey(key)];
-  operator []=(key, value) { _map[_wrapKey(key)] = value; }
-  putIfAbsent(key, ifAbsent()) => _map.putIfAbsent(_wrapKey(key), ifAbsent);
-  remove(key) => _map.remove(_wrapKey(key));
-  void clear() => _map.clear();
-  void forEach(void f(key, value)) =>
-    _map.forEach((k, v) => f(_unwrapKey(k), v));
-  Iterable get keys => _map.keys.map(_unwrapKey);
-  Iterable get values => _map.values;
-  int get length => _map.length;
-  bool get isEmpty => _map.isEmpty;
-  bool get isNotEmpty => _map.isNotEmpty;
-  String toString() => _map.toString();
-
-  int get hashCode => hashCodeFor(_map);
+  int get hashCode => hashCodeFor(this);
 
   bool operator ==(other) {
     if (other is! YamlMap) return false;
     return deepEquals(this, other);
   }
-
-  /// Wraps an object for use as a key in the map.
-  _wrapKey(obj) {
-    if (obj != null && obj is! bool && obj is! List &&
-        (obj is! num || !obj.isNaN) &&
-        (obj is! Map || obj is YamlMap)) {
-      return obj;
-    } else if (obj is Map) {
-      return new YamlMap._wrap(obj);
-    }
-    return new _WrappedHashKey(obj);
-  }
-
-  /// Unwraps an object that was used as a key in the map.
-  _unwrapKey(obj) => obj is _WrappedHashKey ? obj.value : obj;
-}
-
-/// A class for wrapping normally-unhashable objects that are being used as keys
-/// in a YamlMap.
-class _WrappedHashKey {
-  final value;
-
-  _WrappedHashKey(this.value);
-
-  int get hashCode => hashCodeFor(value);
-
-  String toString() => value.toString();
-
-  /// This is defined as both values being structurally equal.
-  bool operator ==(other) {
-    if (other is! _WrappedHashKey) return false;
-    return deepEquals(this.value, other.value);
-  }
 }
diff --git a/lib/yaml.dart b/lib/yaml.dart
index fa52861..dc895e6 100644
--- a/lib/yaml.dart
+++ b/lib/yaml.dart
@@ -2,44 +2,6 @@
 // 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.
 
-/// A parser for [YAML](http://www.yaml.org/).
-///
-/// ## Installing ##
-///
-/// Use [pub][] to install this package. Add the following to your
-/// `pubspec.yaml` file.
-///
-///     dependencies:
-///       yaml: any
-///
-/// Then run `pub install`.
-///
-/// For more information, see the
-/// [yaml package on pub.dartlang.org][pkg].
-///
-/// ## Using ##
-///
-/// Use [loadYaml] to load a single document, or [loadYamlStream] to load a
-/// stream of documents. For example:
-///
-///     import 'package:yaml/yaml.dart';
-///     main() {
-///       var doc = loadYaml("YAML: YAML Ain't Markup Language");
-///       print(doc['YAML']);
-///     }
-///
-/// This library currently doesn't support dumping to YAML. You should use
-/// `JSON.encode` from `dart:convert` instead:
-///
-///     import 'dart:convert';
-///     import 'package:yaml/yaml.dart';
-///     main() {
-///       var doc = loadYaml("YAML: YAML Ain't Markup Language");
-///       print(JSON.encode(doc));
-///     }
-///
-/// [pub]: http://pub.dartlang.org
-/// [pkg]: http://pub.dartlang.org/packages/yaml
 library yaml;
 
 import 'src/composer.dart';
@@ -50,18 +12,23 @@
 export 'src/yaml_exception.dart';
 export 'src/yaml_map.dart';
 
-/// Loads a single document from a YAML string. If the string contains more than
-/// one document, this throws an error.
+/// Loads a single document from a YAML string.
+///
+/// If the string contains more than one document, this throws a
+/// [YamlException]. In future releases, this will become an [ArgumentError].
 ///
 /// The return value is mostly normal Dart objects. However, since YAML mappings
 /// support some key types that the default Dart map implementation doesn't
-/// (null, NaN, booleans, lists, and maps), all maps in the returned document
-/// are [YamlMap]s. These have a few small behavioral differences from the
-/// default Map implementation; for details, see the [YamlMap] class.
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// In future versions, maps will instead be [HashMap]s with a custom equality
+/// operation.
 loadYaml(String yaml) {
   var stream = loadYamlStream(yaml);
   if (stream.length != 1) {
-    throw new YamlException("Expected 1 document, were ${stream.length}");
+    throw new YamlException("Expected 1 document, were ${stream.length}.");
   }
   return stream[0];
 }
@@ -70,9 +37,12 @@
 ///
 /// The return value is mostly normal Dart objects. However, since YAML mappings
 /// support some key types that the default Dart map implementation doesn't
-/// (null, NaN, booleans, lists, and maps), all maps in the returned document
-/// are [YamlMap]s. These have a few small behavioral differences from the
-/// default Map implementation; for details, see the [YamlMap] class.
+/// (NaN, lists, and maps), all maps in the returned document are [YamlMap]s.
+/// These have a few small behavioral differences from the default Map
+/// implementation; for details, see the [YamlMap] class.
+///
+/// In future versions, maps will instead be [HashMap]s with a custom equality
+/// operation.
 List loadYamlStream(String yaml) {
   return new Parser(yaml).l_yamlStream()
       .map((doc) => new Constructor(new Composer(doc).compose()).construct())
diff --git a/pubspec.yaml b/pubspec.yaml
index 1fb4c8d..c3eacd7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,8 +1,10 @@
 name: yaml
-version: 0.9.0
+version: 0.9.0+1
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: A parser for YAML.
+dependencies:
+  collection: ">=0.9.2 <0.10.0"
 dev_dependencies:
   unittest: ">=0.9.0 <0.10.0"
 environment:
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..0bac9f9
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2014, 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.
+
+library yaml.test.utils;
+
+import 'package:unittest/unittest.dart';
+import 'package:yaml/src/deep_equals.dart' as de;
+import 'package:yaml/yaml.dart';
+
+/// A matcher that validates that a closure or Future throws a [YamlException].
+final Matcher throwsYamlException = throwsA(new isInstanceOf<YamlException>());
+
+/// Returns a matcher that asserts that the value equals [expected].
+///
+/// This handles recursive loops and considers `NaN` to equal itself.
+Matcher deepEquals(expected) =>
+    predicate((actual) => de.deepEquals(actual, expected), "equals $expected");
+
+/// Constructs a new yaml.YamlMap, optionally from a normal Map.
+Map yamlMap([Map from]) =>
+    from == null ? new YamlMap() : new YamlMap.from(from);
+
+/// Asserts that a string containing a single YAML document produces a given
+/// value when loaded.
+void expectYamlLoads(expected, String source) {
+  var actual = loadYaml(cleanUpLiteral(source));
+  expect(expected, deepEquals(actual));
+}
+
+/// Asserts that a string containing a stream of YAML documents produces a given
+/// list of values when loaded.
+void expectYamlStreamLoads(List expected, String source) {
+  var actual = loadYamlStream(cleanUpLiteral(source));
+  expect(expected, deepEquals(actual));
+}
+
+/// Asserts that a string containing a single YAML document throws a
+/// [YamlException].
+void expectYamlFails(String source) {
+  expect(() => loadYaml(cleanUpLiteral(source)), throwsYamlException);
+}
+
+/// Removes eight spaces of leading indentation from a multiline string.
+///
+/// Note that this is very sensitive to how the literals are styled. They should
+/// be:
+///     '''
+///     Text starts on own line. Lines up with subsequent lines.
+///     Lines are indented exactly 8 characters from the left margin.
+///     Close is on the same line.'''
+///
+/// This does nothing if text is only a single line.
+String cleanUpLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var j = 0; j < lines.length; j++) {
+    if (lines[j].length > 8) {
+      lines[j] = lines[j].substring(8, lines[j].length);
+    } else {
+      lines[j] = '';
+    }
+  }
+
+  return lines.join('\n');
+}
+
+/// Indents each line of [text] so that, when passed to [cleanUpLiteral], it
+/// will produce output identical to [text].
+///
+/// This is useful for literals that need to include newlines but can't be
+/// conveniently represented as multi-line strings.
+String indentLiteral(String text) {
+  var lines = text.split('\n');
+  if (lines.length <= 1) return text;
+
+  for (var i = 0; i < lines.length; i++) {
+    lines[i] = "        ${lines[i]}";
+  }
+
+  return lines.join("\n");
+}
diff --git a/test/yaml_test.dart b/test/yaml_test.dart
index 42cb613..fcfc12e 100644
--- a/test/yaml_test.dart
+++ b/test/yaml_test.dart
@@ -2,36 +2,13 @@
 // 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.
 
-library yaml_test;
+library yaml.test;
 
 // TODO(rnystrom): rewrite tests so that they don't need "Expect".
-import "package:expect/expect.dart";
 import 'package:unittest/unittest.dart';
 import 'package:yaml/yaml.dart';
-import 'package:yaml/src/deep_equals.dart';
-// TODO(jmesserly): we should not be reaching outside the YAML package
-// The http package has a similar problem.
-import '../../../tests/utils/test_utils.dart';
 
-/// Constructs a new yaml.YamlMap, optionally from a normal Map.
-Map yamlMap([Map from]) =>
-    from == null ? new YamlMap() : new YamlMap.from(from);
-
-/// Asserts that a string containing a single YAML document produces a given
-/// value when loaded.
-expectYamlLoads(expected, String source) {
-  var actual = loadYaml(cleanUpLiteral(source));
-  Expect.isTrue(deepEquals(expected, actual), 
-      'expectYamlLoads(expected: <$expected>, actual: <$actual>)');
-}
-
-/// Asserts that a string containing a stream of YAML documents produces a given
-/// list of values when loaded.
-expectYamlStreamLoads(List expected, String source) {
-  var actual = loadYamlStream(cleanUpLiteral(source));
-  Expect.isTrue(deepEquals(expected, actual), 
-      'expectYamlStreamLoads(expected: <$expected>, actual: <$actual>)');
-}
+import 'utils.dart';
 
 main() {
   var infinity = double.parse("Infinity");
@@ -500,7 +477,7 @@
 
     expectDisallowsCharacter(int charCode) {
       var char = new String.fromCharCodes([charCode]);
-      Expect.throws(() => loadYaml('The character "$char" is disallowed'));
+      expectYamlFails('The character "$char" is disallowed');
     }
 
     test("doesn't include C0 control characters", () {
@@ -598,8 +575,8 @@
     // });
 
     test('[Example 5.10]', () {
-      Expect.throws(() => loadYaml("commercial-at: @text"));
-      Expect.throws(() => loadYaml("commercial-at: `text"));
+      expectYamlFails("commercial-at: @text");
+      expectYamlFails("commercial-at: `text");
     });
   });
 
@@ -610,7 +587,7 @@
     });
 
     group('do not include', () {
-      test('form feed', () => Expect.throws(() => loadYaml("- 1\x0C- 2")));
+      test('form feed', () => expectYamlFails("- 1\x0C- 2"));
       test('NEL', () => expectYamlLoads(["1\x85- 2"], "- 1\x85- 2"));
       test('0x2028', () => expectYamlLoads(["1\u2028- 2"], "- 1\u2028- 2"));
       test('0x2029', () => expectYamlLoads(["1\u2029- 2"], "- 1\u2029- 2"));
@@ -669,27 +646,27 @@
     });
 
     test('[Example 5.14]', () {
-      Expect.throws(() => loadYaml('Bad escape: "\\c"'));
-      Expect.throws(() => loadYaml('Bad escape: "\\xq-"'));
+      expectYamlFails('Bad escape: "\\c"');
+      expectYamlFails('Bad escape: "\\xq-"');
     });
   });
 
   // Chapter 6: Basic Structures
   group('6.1: Indentation Spaces', () {
     test('may not include TAB characters', () {
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         -
         \t- foo
-        \t- bar""")));
+        \t- bar""");
     });
 
     test('must be the same for all sibling nodes', () {
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         -
           - foo
-         - bar""")));
+         - bar""");
     });
 
     test('may be different for the children of sibling nodes', () {
@@ -916,10 +893,10 @@
 
   group('6.7: Separation Lines', () {
     test('may not be used within implicit keys', () {
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         [1,
-         2]: 3""")));
+         2]: 3""");
     });
 
     test('[Example 6.12]', () {
@@ -960,11 +937,11 @@
     // });
 
     // test('[Example 6.15]', () {
-    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //   expectYamlFails(
     //     """
     //     %YAML 1.2
     //     %YAML 1.1
-    //     foo""")));
+    //     foo""");
     // });
 
     // test('[Example 6.16]', () {
@@ -976,11 +953,11 @@
     // });
 
     // test('[Example 6.17]', () {
-    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //   ExpectYamlFails(
     //     """
     //     %TAG ! !foo
     //     %TAG ! !foo
-    //     bar""")));
+    //     bar""");
     // });
 
     // Examples 6.18 through 6.22 test custom tag URIs, which this
@@ -1010,8 +987,8 @@
     // // doesn't plan to support.
 
     // test('[Example 6.25]', () {
-    //   Expect.throws(() => loadYaml("- !<!> foo"));
-    //   Expect.throws(() => loadYaml("- !<\$:?> foo"));
+    //   expectYamlFails("- !<!> foo");
+    //   expectYamlFails("- !<\$:?> foo");
     // });
 
     // // Examples 6.26 and 6.27 test custom tag URIs, which this implementation
@@ -1040,10 +1017,10 @@
   // Chapter 7: Flow Styles
   group('7.1: Alias Nodes', () {
     // test("must not use an anchor that doesn't previously occur", () {
-    //   Expect.throws(() => loadYaml(cleanUpLiteral(
+    //   expectYamlFails(
     //     """
     //     - *anchor
-    //     - &anchor foo"""));
+    //     - &anchor foo""");
     // });
 
     // test("don't have to exist for a given anchor node", () {
@@ -1051,18 +1028,17 @@
     // });
 
     // group('must not specify', () {
-    //   test('tag properties', () => Expect.throws(() => loadYaml(cleanUpLiteral(
+    //   test('tag properties', () => expectYamlFails(
     //     """
     //     - &anchor foo
-    //     - !str *anchor""")));
+    //     - !str *anchor""");
 
-    //   test('anchor properties', () => Expect.throws(
-    //           () => loadYaml(cleanUpLiteral(
+    //   test('anchor properties', () => expectYamlFails(
     //     """
     //     - &anchor foo
-    //     - &anchor2 *anchor""")));
+    //     - &anchor2 *anchor""");
 
-    //   test('content', () => Expect.throws(() => loadYaml(cleanUpLiteral(
+    //   test('content', () => expectYamlFails(
     //     """
     //     - &anchor foo
     //     - *anchor bar""")));
@@ -1075,9 +1051,7 @@
     //     alias: *anchor""");
     //   var anchorList = doc['anchor'];
     //   var aliasList = doc['alias'];
-    //   Expect.isTrue(anchorList === aliasList);
-    //   anchorList.add('d');
-    //   Expect.listEquals(['a', 'b', 'c', 'd'], aliasList);
+    //   expect(anchorList, same(aliasList));
 
     //   doc = loadYaml(cleanUpLiteral(
     //     """
@@ -1086,9 +1060,7 @@
     //       : bar""");
     //   anchorList = doc.keys[0];
     //   aliasList = doc[['a', 'b', 'c']].keys[0];
-    //   Expect.isTrue(anchorList === aliasList);
-    //   anchorList.add('d');
-    //   Expect.listEquals(['a', 'b', 'c', 'd'], aliasList);
+    //   expect(anchorList, same(aliasList));
     // });
 
     // test('[Example 7.1]', () {
@@ -1344,15 +1316,15 @@
     });
 
     test('[Example 7.22]', () {
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         [ foo
-         bar: invalid ]""")));
+         bar: invalid ]""");
 
       // TODO(nweiz): enable this when we throw an error for long keys
       // var dotList = new List.filled(1024, ' ');
       // var dots = dotList.join();
-      // Expect.throws(() => loadYaml('[ "foo...$dots...bar": invalid ]'));
+      // expectYamlFails('[ "foo...$dots...bar": invalid ]');
     });
   });
 
@@ -1421,22 +1393,22 @@
     });
 
     test('[Example 8.3]', () {
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         - |
           
-         text""")));
+         text""");
 
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         - >
           text
-         text""")));
+         text""");
 
-      Expect.throws(() => loadYaml(cleanUpLiteral(
+      expectYamlFails(
         """
         - |2
-         text""")));
+         text""");
     });
 
     test('[Example 8.4]', () {