Add support for default package and metadata. (#53)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e802d7d..db4bb00 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## 1.1.0
+
+- Allow parsing files with default-package entries and metadata.
+ A default-package entry has an empty key and a valid package name
+ as value.
+ Metadata is attached as fragments to base URIs.
+
## 1.0.5
- Fix usage of SDK constants.
diff --git a/lib/discovery_analysis.dart b/lib/discovery_analysis.dart
index 67798e1..d623303 100644
--- a/lib/discovery_analysis.dart
+++ b/lib/discovery_analysis.dart
@@ -59,22 +59,22 @@
static PackageContext findAll(Directory directory,
{Packages root: Packages.noPackages}) {
if (!directory.existsSync()) {
- throw new ArgumentError("Directory not found: $directory");
+ throw ArgumentError("Directory not found: $directory");
}
var contexts = <PackageContext>[];
void findRoots(Directory directory) {
Packages packages;
List<PackageContext> oldContexts;
- File packagesFile = new File(path.join(directory.path, ".packages"));
+ File packagesFile = File(path.join(directory.path, ".packages"));
if (packagesFile.existsSync()) {
packages = _loadPackagesFile(packagesFile);
oldContexts = contexts;
contexts = [];
} else {
Directory packagesDir =
- new Directory(path.join(directory.path, "packages"));
+ Directory(path.join(directory.path, "packages"));
if (packagesDir.existsSync()) {
- packages = new FilePackagesDirectoryPackages(packagesDir);
+ packages = FilePackagesDirectoryPackages(packagesDir);
oldContexts = contexts;
contexts = [];
}
@@ -87,7 +87,7 @@
}
}
if (packages != null) {
- oldContexts.add(new _PackageContext(directory, packages, contexts));
+ oldContexts.add(_PackageContext(directory, packages, contexts));
contexts = oldContexts;
}
}
@@ -97,7 +97,7 @@
if (contexts.length == 1 && contexts[0].directory == directory) {
return contexts[0];
}
- return new _PackageContext(directory, root, contexts);
+ return _PackageContext(directory, root, contexts);
}
}
@@ -106,10 +106,10 @@
final Packages packages;
final List<PackageContext> children;
_PackageContext(this.directory, this.packages, List<PackageContext> children)
- : children = new List<PackageContext>.unmodifiable(children);
+ : children = List<PackageContext>.unmodifiable(children);
Map<Directory, Packages> asMap() {
- var result = new HashMap<Directory, Packages>();
+ var result = HashMap<Directory, Packages>();
recurse(_PackageContext current) {
result[current.directory] = current.packages;
for (var child in current.children) {
@@ -124,7 +124,7 @@
PackageContext operator [](Directory directory) {
String path = directory.path;
if (!path.startsWith(this.directory.path)) {
- throw new ArgumentError("Not inside $path: $directory");
+ throw ArgumentError("Not inside $path: $directory");
}
_PackageContext current = this;
// The current path is know to agree with directory until deltaIndex.
@@ -160,8 +160,8 @@
}
Packages _loadPackagesFile(File file) {
- var uri = new Uri.file(file.path);
+ var uri = Uri.file(file.path);
var bytes = file.readAsBytesSync();
var map = pkgfile.parse(bytes, uri);
- return new MapPackages(map);
+ return MapPackages(map);
}
diff --git a/lib/packages.dart b/lib/packages.dart
index 890f448..886fbc8 100644
--- a/lib/packages.dart
+++ b/lib/packages.dart
@@ -45,6 +45,32 @@
/// and getting `packages` from such a `Packages` object will throw.
Iterable<String> get packages;
+ /// Retrieve metadata associated with a package.
+ ///
+ /// Metadata have string keys and values, and are looked up by key.
+ ///
+ /// Returns `null` if the argument is not a valid package name,
+ /// or if the package is not one of the packages configured by
+ /// this packages object, or if the package does not have associated
+ /// metadata with the provided [key].
+ ///
+ /// Not all `Packages` objects can support metadata.
+ /// Those will always return `null`.
+ String packageMetadata(String packageName, String key);
+
+ /// Retrieve metadata associated with a library.
+ ///
+ /// If [libraryUri] is a `package:` URI, the returned value
+ /// is the same that would be returned by [packageMetadata] with
+ /// the package's name and the same key.
+ ///
+ /// If [libraryUri] is not a `package:` URI, and this [Packages]
+ /// object has a [defaultPackageName], then the [key] is looked
+ /// up on the default package instead.
+ ///
+ /// Otherwise the result is `null`.
+ String libraryMetadata(Uri libraryUri, String key);
+
/// Return the names-to-base-URI mapping of the available packages.
///
/// Returns a map from package name to a base URI.
@@ -55,4 +81,14 @@
/// Some `Packages` objects are unable to find the package names,
/// and calling `asMap` on such a `Packages` object will throw.
Map<String, Uri> asMap();
+
+ /// The name of the "default package".
+ ///
+ /// A default package is a package that *non-package* libraries
+ /// may be considered part of for some purposes.
+ ///
+ /// The value is `null` if there is no default package.
+ /// Not all implementations of [Packages] supports a default package,
+ /// and will always have a `null` value for those.
+ String get defaultPackageName;
}
diff --git a/lib/packages_file.dart b/lib/packages_file.dart
index 85de194..284d8e9 100644
--- a/lib/packages_file.dart
+++ b/lib/packages_file.dart
@@ -22,8 +22,15 @@
/// If the content was read from a file, `baseLocation` should be the
/// location of that file.
///
+/// If [allowDefaultPackage] is set to true, an entry with an empty package name
+/// is accepted. This entry does not correspond to a package, but instead
+/// represents a *default package* which non-package libraries may be considered
+/// part of in some cases. The value of that entry must be a valid package name.
+///
/// Returns a simple mapping from package name to package location.
-Map<String, Uri> parse(List<int> source, Uri baseLocation) {
+/// If default package is allowed, the map maps the empty string to the default package's name.
+Map<String, Uri> parse(List<int> source, Uri baseLocation,
+ {bool allowDefaultPackage = false}) {
int index = 0;
Map<String, Uri> result = <String, Uri>{};
while (index < source.length) {
@@ -36,7 +43,10 @@
continue;
}
if (char == $colon) {
- throw new FormatException("Missing package name", source, index - 1);
+ if (!allowDefaultPackage) {
+ throw FormatException("Missing package name", source, index - 1);
+ }
+ separatorIndex = index - 1;
}
isComment = char == $hash;
while (index < source.length) {
@@ -50,22 +60,36 @@
}
if (isComment) continue;
if (separatorIndex < 0) {
- throw new FormatException("No ':' on line", source, index - 1);
+ throw FormatException("No ':' on line", source, index - 1);
}
var packageName = new String.fromCharCodes(source, start, separatorIndex);
- if (!isValidPackageName(packageName)) {
- throw new FormatException("Not a valid package name", packageName, 0);
+ if (packageName.isEmpty
+ ? !allowDefaultPackage
+ : !isValidPackageName(packageName)) {
+ throw FormatException("Not a valid package name", packageName, 0);
}
- var packageUri = new String.fromCharCodes(source, separatorIndex + 1, end);
- var packageLocation = Uri.parse(packageUri);
- packageLocation = baseLocation.resolveUri(packageLocation);
- if (!packageLocation.path.endsWith('/')) {
- packageLocation =
- packageLocation.replace(path: packageLocation.path + "/");
+ var packageValue =
+ new String.fromCharCodes(source, separatorIndex + 1, end);
+ Uri packageLocation;
+ if (packageName.isEmpty) {
+ if (!isValidPackageName(packageValue)) {
+ throw FormatException(
+ "Default package entry value is not a valid package name");
+ }
+ packageLocation = Uri(path: packageValue);
+ } else {
+ packageLocation = baseLocation.resolve(packageValue);
+ if (!packageLocation.path.endsWith('/')) {
+ packageLocation =
+ packageLocation.replace(path: packageLocation.path + "/");
+ }
}
if (result.containsKey(packageName)) {
- throw new FormatException(
- "Same package name occured twice.", source, start);
+ if (packageName.isEmpty) {
+ throw FormatException(
+ "More than one default package entry", source, start);
+ }
+ throw FormatException("Same package name occured twice", source, start);
}
result[packageName] = packageLocation;
}
diff --git a/lib/src/packages_impl.dart b/lib/src/packages_impl.dart
index e89d94d..817002f 100644
--- a/lib/src/packages_impl.dart
+++ b/lib/src/packages_impl.dart
@@ -26,6 +26,12 @@
Iterable<String> get packages => new Iterable<String>.empty();
Map<String, Uri> asMap() => const <String, Uri>{};
+
+ String get defaultPackageName => null;
+
+ String packageMetadata(String packageName, String key) => null;
+
+ String libraryMetadata(Uri libraryUri, String key) => null;
}
/// Base class for [Packages] implementations.
@@ -51,6 +57,12 @@
/// Returns `null` if no package exists with that name, and that can be
/// determined.
Uri getBase(String packageName);
+
+ String get defaultPackageName => null;
+
+ String packageMetadata(String packageName, String key) => null;
+
+ String libraryMetadata(Uri libraryUri, String key) => null;
}
/// A [Packages] implementation based on an existing map.
@@ -58,11 +70,34 @@
final Map<String, Uri> _mapping;
MapPackages(this._mapping);
- Uri getBase(String packageName) => _mapping[packageName];
+ Uri getBase(String packageName) =>
+ packageName.isEmpty ? null : _mapping[packageName];
Iterable<String> get packages => _mapping.keys;
Map<String, Uri> asMap() => new UnmodifiableMapView<String, Uri>(_mapping);
+
+ String get defaultPackageName => _mapping[""]?.toString();
+
+ String packageMetadata(String packageName, String key) {
+ if (packageName.isEmpty) return null;
+ Uri uri = _mapping[packageName];
+ if (uri == null || !uri.hasFragment) return null;
+ // This can be optimized, either by caching the map or by
+ // parsing incrementally instead of parsing the entire fragment.
+ return Uri.splitQueryString(uri.fragment)[key];
+ }
+
+ String libraryMetadata(Uri libraryUri, String key) {
+ if (libraryUri.isScheme("package")) {
+ return packageMetadata(libraryUri.pathSegments.first, key);
+ }
+ var defaultPackageNameUri = _mapping[""];
+ if (defaultPackageNameUri != null) {
+ return packageMetadata(defaultPackageNameUri.toString(), key);
+ }
+ return null;
+ }
}
/// A [Packages] implementation based on a remote (e.g., HTTP) directory.
diff --git a/pubspec.yaml b/pubspec.yaml
index 4bad1cf..a69096d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: package_config
-version: 1.0.5
+version: 1.1.0-pre
description: Support for working with Package Resolution config files.
author: Dart Team <misc@dartlang.org>
homepage: https://github.com/dart-lang/package_config
diff --git a/test/parse_test.dart b/test/parse_test.dart
index fb3a6fa..b9b1bb5 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -116,6 +116,82 @@
() => doParse(singleRelativeSample * 2, base), throwsFormatException);
});
+ test("disallow default package", () {
+ expect(() => doParse(":foo", base, allowDefaultPackage: false),
+ throwsFormatException);
+ });
+
+ test("allow default package", () {
+ var packages = doParse(":foo", base, allowDefaultPackage: true);
+ expect(packages.defaultPackageName, "foo");
+ });
+
+ test("allow default package name with dot", () {
+ var packages = doParse(":foo.bar", base, allowDefaultPackage: true);
+ expect(packages.defaultPackageName, "foo.bar");
+ });
+
+ test("not two default packages", () {
+ expect(() => doParse(":foo\n:bar", base, allowDefaultPackage: true),
+ throwsFormatException);
+ });
+
+ test("default package invalid package name", () {
+ // Not a valid *package name*.
+ expect(() => doParse(":foo/bar", base, allowDefaultPackage: true),
+ throwsFormatException);
+ });
+
+ group("metadata", () {
+ var packages = doParse(
+ ":foo\n"
+ "foo:foo#metafoo=1\n"
+ "bar:bar#metabar=2\n"
+ "baz:baz\n"
+ "qux:qux#metaqux1=3&metaqux2=4\n",
+ base,
+ allowDefaultPackage: true);
+ test("non-existing", () {
+ // non-package name.
+ expect(packages.packageMetadata("///", "f"), null);
+ expect(packages.packageMetadata("", "f"), null);
+ // unconfigured package name.
+ expect(packages.packageMetadata("absent", "f"), null);
+ // package name without that metadata
+ expect(packages.packageMetadata("foo", "notfoo"), null);
+ });
+ test("lookup", () {
+ expect(packages.packageMetadata("foo", "metafoo"), "1");
+ expect(packages.packageMetadata("bar", "metabar"), "2");
+ expect(packages.packageMetadata("qux", "metaqux1"), "3");
+ expect(packages.packageMetadata("qux", "metaqux2"), "4");
+ });
+ test("by library URI", () {
+ expect(
+ packages.libraryMetadata(
+ Uri.parse("package:foo/index.dart"), "metafoo"),
+ "1");
+ expect(
+ packages.libraryMetadata(
+ Uri.parse("package:bar/index.dart"), "metabar"),
+ "2");
+ expect(
+ packages.libraryMetadata(
+ Uri.parse("package:qux/index.dart"), "metaqux1"),
+ "3");
+ expect(
+ packages.libraryMetadata(
+ Uri.parse("package:qux/index.dart"), "metaqux2"),
+ "4");
+ });
+ test("by default package", () {
+ expect(
+ packages.libraryMetadata(
+ Uri.parse("file:///whatever.dart"), "metafoo"),
+ "1");
+ });
+ });
+
for (String invalidSample in invalid) {
test("invalid '$invalidSample'", () {
var result;
@@ -130,8 +206,10 @@
}
}
-Packages doParse(String sample, Uri baseUri) {
- Map<String, Uri> map = parse(sample.codeUnits, baseUri);
+Packages doParse(String sample, Uri baseUri,
+ {bool allowDefaultPackage = false}) {
+ Map<String, Uri> map = parse(sample.codeUnits, baseUri,
+ allowDefaultPackage: allowDefaultPackage);
return new MapPackages(map);
}