Add new constructors to YamlNode subclasses.

These constructors make it easy to use the same code for extracting data from
both YAML and non-YAML objects.

R=rnystrom@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/yaml@37665 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/pkgs/yaml/CHANGELOG.md b/pkgs/yaml/CHANGELOG.md
index fdd9240..186cfab 100644
--- a/pkgs/yaml/CHANGELOG.md
+++ b/pkgs/yaml/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 1.1.0
 
+* Add new publicly-accessible constructors for `YamlNode` subclasses. These
+  constructors make it possible to use the same API to access non-YAML data as
+  YAML data.
+
 * Make `YamlException` inherit from source_map's [`SpanFormatException`][]. This
   improves the error formatting and allows callers access to source range
   information.
diff --git a/pkgs/yaml/lib/src/constructor.dart b/pkgs/yaml/lib/src/constructor.dart
index 5cf23c7..5931dae 100644
--- a/pkgs/yaml/lib/src/constructor.dart
+++ b/pkgs/yaml/lib/src/constructor.dart
@@ -26,14 +26,14 @@
 
   /// Returns the value of a scalar.
   YamlScalar visitScalar(ScalarNode scalar) =>
-      new YamlScalar(scalar.value, scalar.span);
+      new YamlScalar.internal(scalar.value, scalar.span);
 
   /// Converts a sequence into a List of Dart objects.
   YamlList visitSequence(SequenceNode seq) {
     var anchor = getAnchor(seq);
     if (anchor != null) return anchor;
     var nodes = [];
-    var dartSeq = setAnchor(seq, new YamlList(nodes, seq.span));
+    var dartSeq = setAnchor(seq, new YamlList.internal(nodes, seq.span));
     nodes.addAll(super.visitSequence(seq));
     return dartSeq;
   }
@@ -43,7 +43,7 @@
     var anchor = getAnchor(map);
     if (anchor != null) return anchor;
     var nodes = deepEqualsMap();
-    var dartMap = setAnchor(map, new YamlMap(nodes, map.span));
+    var dartMap = setAnchor(map, new YamlMap.internal(nodes, map.span));
     super.visitMapping(map).forEach((k, v) => nodes[k] = v);
     return dartMap;
   }
@@ -57,10 +57,14 @@
 
     // Re-wrap [value]'s contents so that it's associated with the span of the
     // anchor rather than its original definition.
-    if (value is YamlMap) return new YamlMap(value.nodes, anchored.span);
-    if (value is YamlList) return new YamlList(value.nodes, anchored.span);
-    assert(value is YamlScalar);
-    return new YamlScalar(value.value, anchored.span);
+    if (value is YamlMap) {
+      return new YamlMap.internal(value.nodes, anchored.span);
+    } else if (value is YamlList) {
+      return new YamlList.internal(value.nodes, anchored.span);
+    } else {
+      assert(value is YamlScalar);
+      return new YamlScalar.internal(value.value, anchored.span);
+    }
   }
 
   /// Records that [value] is the Dart object representing [anchored].
diff --git a/pkgs/yaml/lib/src/null_span.dart b/pkgs/yaml/lib/src/null_span.dart
new file mode 100644
index 0000000..b3e3254
--- /dev/null
+++ b/pkgs/yaml/lib/src/null_span.dart
@@ -0,0 +1,47 @@
+// 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.null_span;
+
+import 'package:path/path.dart' as p;
+import 'package:source_maps/source_maps.dart';
+
+/// A [Span] with no location information.
+///
+/// This is used with [YamlMap.wrap] and [YamlList.wrap] to provide means of
+/// accessing a non-YAML map that behaves transparently like a map parsed from
+/// YAML.
+class NullSpan extends Span {
+  Location get end => start;
+  final text = "";
+
+  NullSpan(String sourceUrl)
+      : this._(new NullLocation(sourceUrl));
+
+  NullSpan._(Location location)
+      : super(location, location, false);
+
+  String formatLocationMessage(String message, {bool useColors: false,
+      String color}) {
+    var locationMessage = sourceUrl == null ? "in an unknown location" :
+        "in ${p.prettyUri(sourceUrl)}";
+    return "$locationMessage: $message";
+  }
+}
+
+/// A [Location] with no location information.
+///
+/// This is used with [YamlMap.wrap] and [YamlList.wrap] to provide means of
+/// accessing a non-YAML map that behaves transparently like a map parsed from
+/// YAML.
+class NullLocation extends Location {
+  final String sourceUrl;
+  final line = 0;
+  final column = 0;
+
+  String get formatString => sourceUrl == null ? "unknown location" : sourceUrl;
+
+  NullLocation(this.sourceUrl)
+      : super(0);
+}
diff --git a/pkgs/yaml/lib/src/yaml_node.dart b/pkgs/yaml/lib/src/yaml_node.dart
index d0e0578..1aded2b 100644
--- a/pkgs/yaml/lib/src/yaml_node.dart
+++ b/pkgs/yaml/lib/src/yaml_node.dart
@@ -2,13 +2,16 @@
 // 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.yaml_node;
 
 import 'dart:collection' as collection;
 
 import 'package:collection/collection.dart';
 import 'package:source_maps/source_maps.dart';
 
+import 'null_span.dart';
+import 'yaml_node_wrapper.dart';
+
 /// An interface for parsed nodes from a YAML source tree.
 ///
 /// [YamlMap]s and [YamlList]s implement this interface in addition to the
@@ -37,14 +40,40 @@
 class YamlMap extends YamlNode with collection.MapMixin, UnmodifiableMapMixin  {
   final Span span;
 
+  /// A view of [this] where the keys and values are guaranteed to be
+  /// [YamlNode]s.
+  ///
+  /// The key type is `dynamic` to allow values to be accessed using
+  /// non-[YamlNode] keys, but [Map.keys] and [Map.forEach] will always expose
+  /// them as [YamlNode]s. For example, for `{"foo": [1, 2, 3]}` [nodes] will be
+  /// a map from a [YamlScalar] to a [YamlList], but since the key type is
+  /// `dynamic` `map.nodes["foo"]` will still work.
   final Map<dynamic, YamlNode> nodes;
 
   Map get value => this;
 
   Iterable get keys => nodes.keys.map((node) => node.value);
 
-  /// Users of the library should not construct [YamlMap]s manually.
-  YamlMap(Map<dynamic, YamlNode> nodes, this.span)
+  /// Creates an empty YamlMap.
+  ///
+  /// This map's [span] won't have useful location information. However, it will
+  /// have a reasonable implementation of [Span.getLocationMessage]. If
+  /// [sourceName] is passed, it's used as the [Span.sourceUrl].
+  factory YamlMap({String sourceName}) =>
+      new YamlMapWrapper(const {}, sourceName);
+
+  /// Wraps a Dart map so that it can be accessed (recursively) like a
+  /// [YamlMap].
+  ///
+  /// Any [Span]s returned by this map or its children will be dummies without
+  /// useful location information. However, they will have a reasonable
+  /// implementation of [Span.getLocationMessage]. If [sourceName] is passed,
+  /// it's used as the [Span.sourceUrl].
+  factory YamlMap.wrap(Map dartMap, {String sourceName}) =>
+      new YamlMapWrapper(dartMap, sourceName);
+
+  /// Users of the library should not use this constructor.
+  YamlMap.internal(Map<dynamic, YamlNode> nodes, this.span)
       : nodes = new UnmodifiableMapView<dynamic, YamlNode>(nodes);
 
   operator [](key) {
@@ -68,8 +97,26 @@
     throw new UnsupportedError("Cannot modify an unmodifiable List");
   }
 
-  /// Users of the library should not construct [YamlList]s manually.
-  YamlList(List<YamlNode> nodes, this.span)
+  /// Creates an empty YamlList.
+  ///
+  /// This list's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [Span.getLocationMessage]. If
+  /// [sourceName] is passed, it's used as the [Span.sourceUrl].
+  factory YamlList({String sourceName}) =>
+      new YamlListWrapper(const [], sourceName);
+
+  /// Wraps a Dart list so that it can be accessed (recursively) like a
+  /// [YamlList].
+  ///
+  /// Any [Span]s returned by this list or its children will be dummies without
+  /// useful location information. However, they will have a reasonable
+  /// implementation of [Span.getLocationMessage]. If [sourceName] is passed,
+  /// it's used as the [Span.sourceUrl].
+  factory YamlList.wrap(List dartList, {String sourceName}) =>
+      new YamlListWrapper(dartList, sourceName);
+
+  /// Users of the library should not use this constructor.
+  YamlList.internal(List<YamlNode> nodes, this.span)
       : nodes = new UnmodifiableListView<YamlNode>(nodes);
 
   operator [](int index) => nodes[index].value;
@@ -85,8 +132,16 @@
 
   final value;
 
-  /// Users of the library should not construct [YamlScalar]s manually.
-  YamlScalar(this.value, this.span);
+  /// Wraps a Dart value in a [YamlScalar].
+  ///
+  /// This scalar's [span] won't have useful location information. However, it
+  /// will have a reasonable implementation of [Span.getLocationMessage]. If
+  /// [sourceName] is passed, it's used as the [Span.sourceUrl].
+  YamlScalar.wrap(this.value, {String sourceName})
+      : span = new NullSpan(sourceName);
+
+  /// Users of the library should not use this constructor.
+  YamlScalar.internal(this.value, this.span);
 
   String toString() => value.toString();
 }
diff --git a/pkgs/yaml/lib/src/yaml_node_wrapper.dart b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
new file mode 100644
index 0000000..8ea084e
--- /dev/null
+++ b/pkgs/yaml/lib/src/yaml_node_wrapper.dart
@@ -0,0 +1,149 @@
+// 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.yaml_node_wrapper;
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart' as pkg_collection;
+import 'package:source_maps/source_maps.dart';
+
+import 'null_span.dart';
+import 'yaml_node.dart';
+
+/// A wrapper that makes a normal Dart map behave like a [YamlMap].
+class YamlMapWrapper extends MapBase
+    with pkg_collection.UnmodifiableMapMixin<dynamic, YamlNode>
+    implements YamlMap {
+  final Map _dartMap;
+
+  final Span span;
+
+  final Map<dynamic, YamlNode> nodes;
+
+  Map get value => this;
+
+  Iterable get keys => _dartMap.keys;
+
+  YamlMapWrapper(Map dartMap, String sourceName)
+      : this._(dartMap, new NullSpan(sourceName));
+
+  YamlMapWrapper._(Map dartMap, Span span)
+      : _dartMap = dartMap,
+        span = span,
+        nodes = new _YamlMapNodes(dartMap, span);
+
+  operator [](Object key) {
+    var value = _dartMap[key];
+    if (value is Map) return new YamlMapWrapper._(value, span);
+    if (value is List) return new YamlListWrapper._(value, span);
+    return value;
+  }
+
+  int get hashCode => _dartMap.hashCode;
+
+  operator ==(Object other) =>
+      other is YamlMapWrapper && other._dartMap == _dartMap;
+}
+
+/// The implementation of [YamlMapWrapper.nodes] as a wrapper around the Dart
+/// map.
+class _YamlMapNodes extends MapBase<dynamic, YamlNode>
+    with pkg_collection.UnmodifiableMapMixin<dynamic, YamlNode> {
+  final Map _dartMap;
+
+  final Span _span;
+
+  Iterable get keys =>
+      _dartMap.keys.map((key) => new YamlScalar.internal(key, _span));
+
+  _YamlMapNodes(this._dartMap, this._span);
+
+  YamlNode operator [](Object key) {
+    if (key is YamlScalar) key = key.value;
+    if (!_dartMap.containsKey(key)) return null;
+    return _nodeForValue(_dartMap[key], _span);
+  }
+
+  int get hashCode => _dartMap.hashCode;
+
+  operator ==(Object other) =>
+      other is _YamlMapNodes && other._dartMap == _dartMap;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// A wrapper that makes a normal Dart list behave like a [YamlList].
+class YamlListWrapper extends ListBase implements YamlList {
+  final List _dartList;
+
+  final Span span;
+
+  final List<YamlNode> nodes;
+
+  List get value => this;
+
+  int get length => _dartList.length;
+
+  set length(int index) {
+    throw new UnsupportedError("Cannot modify an unmodifiable List.");
+  }
+
+  YamlListWrapper(List dartList, String sourceName)
+      : this._(dartList, new NullSpan(sourceName));
+
+  YamlListWrapper._(List dartList, Span span)
+      : _dartList = dartList,
+        span = span,
+        nodes = new _YamlListNodes(dartList, span);
+
+  operator [](int index) {
+    var value = _dartList[index];
+    if (value is Map) return new YamlMapWrapper._(value, span);
+    if (value is List) return new YamlListWrapper._(value, span);
+    return value;
+  }
+
+  operator []=(int index, value) {
+    throw new UnsupportedError("Cannot modify an unmodifiable List.");
+  }
+
+  int get hashCode => _dartList.hashCode;
+
+  operator ==(Object other) =>
+      other is YamlListWrapper && other._dartList == _dartList;
+}
+
+// TODO(nweiz): Use UnmodifiableListMixin when issue 18970 is fixed.
+/// The implementation of [YamlListWrapper.nodes] as a wrapper around the Dart
+/// list.
+class _YamlListNodes extends ListBase<YamlNode> {
+  final List _dartList;
+
+  final Span _span;
+
+  int get length => _dartList.length;
+
+  set length(int index) {
+    throw new UnsupportedError("Cannot modify an unmodifiable List.");
+  }
+
+  _YamlListNodes(this._dartList, this._span);
+
+  YamlNode operator [](int index) => _nodeForValue(_dartList[index], _span);
+
+  operator []=(int index, value) {
+    throw new UnsupportedError("Cannot modify an unmodifiable List.");
+  }
+
+  int get hashCode => _dartList.hashCode;
+
+  operator ==(Object other) =>
+      other is _YamlListNodes && other._dartList == _dartList;
+}
+
+YamlNode _nodeForValue(value, Span span) {
+  if (value is Map) return new YamlMapWrapper._(value, span);
+  if (value is List) return new YamlListWrapper._(value, span);
+  return new YamlScalar.internal(value, span);
+}
diff --git a/pkgs/yaml/lib/yaml.dart b/pkgs/yaml/lib/yaml.dart
index 80a6319..0afc2cc 100644
--- a/pkgs/yaml/lib/yaml.dart
+++ b/pkgs/yaml/lib/yaml.dart
@@ -72,5 +72,5 @@
   var nodes = pair.first
       .map((doc) => new Constructor(new Composer(doc).compose()).construct())
       .toList();
-  return new YamlList(nodes, pair.last);
+  return new YamlList.internal(nodes, pair.last);
 }
diff --git a/pkgs/yaml/pubspec.yaml b/pkgs/yaml/pubspec.yaml
index d8eefd4..5589695 100644
--- a/pkgs/yaml/pubspec.yaml
+++ b/pkgs/yaml/pubspec.yaml
@@ -1,10 +1,11 @@
 name: yaml
-version: 1.1.0-dev
+version: 1.1.0
 author: "Dart Team <misc@dartlang.org>"
 homepage: http://www.dartlang.org
 description: A parser for YAML.
 dependencies:
   collection: ">=0.9.2 <0.10.0"
+  path: ">=1.2.0 <2.0.0"
   string_scanner: ">=0.0.2 <0.1.0"
   source_maps: ">=0.9.2 <0.10.0"
 dev_dependencies:
diff --git a/pkgs/yaml/test/yaml_node_wrapper_test.dart b/pkgs/yaml/test/yaml_node_wrapper_test.dart
new file mode 100644
index 0000000..8bf4761
--- /dev/null
+++ b/pkgs/yaml/test/yaml_node_wrapper_test.dart
@@ -0,0 +1,163 @@
+// Copyright (c) 2012, 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.node_wrapper_test;
+
+import 'package:source_maps/source_maps.dart';
+import 'package:unittest/unittest.dart';
+import 'package:yaml/yaml.dart';
+
+main() {
+  test("YamlMap() with no sourceName", () {
+    var map = new YamlMap();
+    expect(map, isEmpty);
+    expect(map.nodes, isEmpty);
+    expect(map.span, isNullSpan(isNull));
+  });
+
+  test("YamlMap() with a sourceName", () {
+    var map = new YamlMap(sourceName: "source");
+    expect(map.span, isNullSpan("source"));
+  });
+
+  test("YamlList() with no sourceName", () {
+    var list = new YamlList();
+    expect(list, isEmpty);
+    expect(list.nodes, isEmpty);
+    expect(list.span, isNullSpan(isNull));
+  });
+
+  test("YamlList() with a sourceName", () {
+    var list = new YamlList(sourceName: "source");
+    expect(list.span, isNullSpan("source"));
+  });
+
+  test("YamlMap.wrap() with no sourceName", () {
+    var map = new YamlMap.wrap({
+      "list": [1, 2, 3],
+      "map": {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "scalar": "value"
+    });
+
+    expect(map, equals({
+      "list": [1, 2, 3],
+      "map": {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "scalar": "value"
+    }));
+
+    expect(map.span, isNullSpan(isNull));
+    expect(map["list"], new isInstanceOf<YamlList>());
+    expect(map["list"].nodes[0], new isInstanceOf<YamlScalar>());
+    expect(map["list"].span, isNullSpan(isNull));
+    expect(map["map"], new isInstanceOf<YamlMap>());
+    expect(map["map"].nodes["foo"], new isInstanceOf<YamlScalar>());
+    expect(map["map"]["nested"], new isInstanceOf<YamlList>());
+    expect(map["map"].span, isNullSpan(isNull));
+    expect(map.nodes["scalar"], new isInstanceOf<YamlScalar>());
+    expect(map.nodes["scalar"].value, "value");
+    expect(map.nodes["scalar"].span, isNullSpan(isNull));
+    expect(map["scalar"], "value");
+    expect(map.keys, unorderedEquals(["list", "map", "scalar"]));
+    expect(map.nodes.keys, everyElement(new isInstanceOf<YamlScalar>()));
+    expect(map.nodes[new YamlScalar.wrap("list")], equals([1, 2, 3]));
+  });
+
+  test("YamlMap.wrap() with a sourceName", () {
+    var map = new YamlMap.wrap({
+      "list": [1, 2, 3],
+      "map": {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "scalar": "value"
+    }, sourceName: "source");
+
+    expect(map.span, isNullSpan("source"));
+    expect(map["list"].span, isNullSpan("source"));
+    expect(map["map"].span, isNullSpan("source"));
+    expect(map.nodes["scalar"].span, isNullSpan("source"));
+  });
+
+  test("YamlList.wrap() with no sourceName", () {
+    var list = new YamlList.wrap([
+      [1, 2, 3],
+      {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "value"
+    ]);
+
+    expect(list, equals([
+      [1, 2, 3],
+      {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "value"
+    ]));
+
+    expect(list.span, isNullSpan(isNull));
+    expect(list[0], new isInstanceOf<YamlList>());
+    expect(list[0].nodes[0], new isInstanceOf<YamlScalar>());
+    expect(list[0].span, isNullSpan(isNull));
+    expect(list[1], new isInstanceOf<YamlMap>());
+    expect(list[1].nodes["foo"], new isInstanceOf<YamlScalar>());
+    expect(list[1]["nested"], new isInstanceOf<YamlList>());
+    expect(list[1].span, isNullSpan(isNull));
+    expect(list.nodes[2], new isInstanceOf<YamlScalar>());
+    expect(list.nodes[2].value, "value");
+    expect(list.nodes[2].span, isNullSpan(isNull));
+    expect(list[2], "value");
+  });
+
+  test("YamlList.wrap() with a sourceName", () {
+    var list = new YamlList.wrap([
+      [1, 2, 3],
+      {
+        "foo": "bar",
+        "nested": [4, 5, 6]
+      },
+      "value"
+    ]);
+
+    expect(list.span, isNullSpan(isNull));
+    expect(list[0].span, isNullSpan(isNull));
+    expect(list[1].span, isNullSpan(isNull));
+    expect(list.nodes[2].span, isNullSpan(isNull));
+  });
+
+  solo_test("re-wrapped objects equal one another", () {
+    var list = new YamlList.wrap([
+      [1, 2, 3],
+      {"foo": "bar"}
+    ]);
+
+    expect(list[0] == list[0], isTrue);
+    expect(list[0].nodes == list[0].nodes, isTrue);
+    expect(list[0] == new YamlList.wrap([1, 2, 3]), isFalse);
+    expect(list[1] == list[1], isTrue);
+    expect(list[1].nodes == list[1].nodes, isTrue);
+    expect(list[1] == new YamlMap.wrap({"foo": "bar"}), isFalse);
+  });
+}
+
+Matcher isNullSpan(sourceUrl) => predicate((span) {
+  expect(span, new isInstanceOf<Span>());
+  expect(span.length, equals(0));
+  expect(span.text, isEmpty);
+  expect(span.isIdentifier, isFalse);
+  expect(span.start, equals(span.end));
+  expect(span.start.offset, equals(0));
+  expect(span.start.line, equals(0));
+  expect(span.start.column, equals(0));
+  expect(span.sourceUrl, sourceUrl);
+  return true;
+});