blob: 20f15a1d989b6953ed35bbcc0b16eeea5c7db9ef [file] [log] [blame]
// 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 pubspec;
import 'package.dart';
import 'source.dart';
import 'source_registry.dart';
import 'utils.dart';
import 'version.dart';
import 'yaml/yaml.dart';
/**
* The parsed and validated contents of a pubspec file.
*/
class Pubspec {
/**
* This package's name.
*/
final String name;
/**
* This package's version.
*/
final Version version;
/**
* The packages this package depends on.
*/
List<PackageRef> dependencies;
Pubspec(this.name, this.version, this.dependencies);
Pubspec.empty()
: name = null,
version = Version.none,
dependencies = <PackageRef>[];
/** Whether or not the pubspec has no contents. */
bool get isEmpty =>
name == null && version == Version.none && dependencies.isEmpty;
/**
* Parses the pubspec whose text is [contents]. If the pubspec doesn't define
* version for itself, it defaults to [Version.none].
*/
factory Pubspec.parse(String contents, SourceRegistry sources) {
var name = null;
var version = Version.none;
if (contents.trim() == '') return new Pubspec.empty();
var parsedPubspec = loadYaml(contents);
if (parsedPubspec == null) return new Pubspec.empty();
if (parsedPubspec is! Map) {
throw new FormatException('The pubspec must be a YAML mapping.');
}
if (parsedPubspec.containsKey('name')) {
name = parsedPubspec['name'];
if (name is! String) {
throw new FormatException(
'The pubspec "name" field should be a string, but was "$name".');
}
}
if (parsedPubspec.containsKey('version')) {
version = new Version.parse(parsedPubspec['version']);
}
var dependencies = _parseDependencies(sources,
parsedPubspec['dependencies']);
// Even though the pub app itself doesn't use these fields, we validate
// them here so that users find errors early before they try to upload to
// the server:
// TODO(rnystrom): We should split this validation into separate layers:
// 1. Stuff that is required in any pubspec to perform any command. Things
// like "must have a name". That should go here.
// 2. Stuff that is required to upload a package. Things like "homepage
// must use a valid scheme". That should go elsewhere. pub upload should
// call it, and we should provide a separate command to show the user,
// and also expose it to the editor in some way.
if (parsedPubspec.containsKey('homepage')) {
var homepage = parsedPubspec['homepage'];
if (homepage is! String) {
throw new FormatException(
'The "homepage" field should be a string, but was "$homepage".');
}
var goodScheme = new RegExp(r'^https?:');
if (!goodScheme.hasMatch(homepage)) {
throw new FormatException(
'The "homepage" field should be an "http:" or "https:" URL, but '
'was "$homepage".');
}
}
if (parsedPubspec.containsKey('author') &&
parsedPubspec['author'] is! String) {
throw new FormatException(
'The "author" field should be a string, but was '
'${parsedPubspec["author"]}.');
}
if (parsedPubspec.containsKey('authors')) {
var authors = parsedPubspec['authors'];
if (authors is List) {
// All of the elements must be strings.
if (!authors.every((author) => author is String)) {
throw new FormatException('The "authors" field should be a string '
'or a list of strings, but was "$authors".');
}
} else if (authors is! String) {
throw new FormatException('The pubspec "authors" field should be a '
'string or a list of strings, but was "$authors".');
}
if (parsedPubspec.containsKey('author')) {
throw new FormatException('A pubspec should not have both an "author" '
'and an "authors" field.');
}
}
return new Pubspec(name, version, dependencies);
}
}
List<PackageRef> _parseDependencies(SourceRegistry sources, yaml) {
var dependencies = <PackageRef>[];
// Allow an empty dependencies key.
if (yaml == null) return dependencies;
if (yaml is! Map || yaml.keys.some((e) => e is! String)) {
throw new FormatException(
'The pubspec dependencies should be a map of package names, but '
'was ${yaml}.');
}
yaml.forEach((name, spec) {
var description, source;
var versionConstraint = new VersionRange();
if (spec == null) {
description = name;
source = sources.defaultSource;
} else if (spec is String) {
description = name;
source = sources.defaultSource;
versionConstraint = new VersionConstraint.parse(spec);
} else if (spec is Map) {
if (spec.containsKey('version')) {
versionConstraint = new VersionConstraint.parse(spec.remove('version'));
}
var sourceNames = spec.keys;
if (sourceNames.length > 1) {
throw new FormatException(
'Dependency $name may only have one source: $sourceNames.');
}
var sourceName = only(sourceNames);
if (sourceName is! String) {
throw new FormatException(
'Source name $sourceName should be a string.');
}
source = sources[sourceName];
description = spec[sourceName];
} else {
throw new FormatException(
'Dependency specification $spec should be a string or a mapping.');
}
source.validateDescription(description, fromLockFile: false);
dependencies.add(new PackageRef(
name, source, versionConstraint, description));
});
return dependencies;
}