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