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;
+});