Move pub over to using the pub.dartlang.org API v2.

R=rnystrom@google.com

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

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@23615 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
nweiz@google.com
2013-06-04 21:05:00 +00:00
parent d1fbe2117d
commit c2500c3aa0
20 changed files with 347 additions and 183 deletions
@@ -54,8 +54,8 @@ class LishCommand extends PubCommand {
return log.progress('Uploading', () {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var newUri = server.resolve("/packages/versions/new.json");
return client.get(newUri).then((response) {
var newUri = server.resolve("/api/packages/versions/new");
return client.get(newUri, headers: PUB_API_HEADERS).then((response) {
var parameters = parseJsonResponse(response);
var url = _expectField(parameters, 'url', response);
@@ -78,7 +78,7 @@ class LishCommand extends PubCommand {
var location = response.headers['location'];
if (location == null) throw new PubHttpException(response);
return location;
}).then((location) => client.get(location))
}).then((location) => client.get(location, headers: PUB_API_HEADERS))
.then(handleJsonSuccess);
});
}).catchError((error) {
@@ -63,13 +63,16 @@ class UploaderCommand extends PubCommand {
var uploader = commandOptions.rest[0];
return oauth2.withClient(cache, (client) {
if (command == 'add') {
var url = server.resolve("/packages/${Uri.encodeComponent(package)}"
"/uploaders.json");
return client.post(url, fields: {"email": uploader});
var url = server.resolve("/api/packages/"
"${Uri.encodeComponent(package)}/uploaders");
return client.post(url,
headers: PUB_API_HEADERS,
fields: {"email": uploader});
} else { // command == 'remove'
var url = server.resolve("/packages/${Uri.encodeComponent(package)}"
"/uploaders/${Uri.encodeComponent(uploader)}.json");
return client.delete(url);
var url = server.resolve("/api/packages/"
"${Uri.encodeComponent(package)}/uploaders/"
"${Uri.encodeComponent(uploader)}");
return client.delete(url, headers: PUB_API_HEADERS);
}
});
}).then(handleJsonSuccess)
@@ -34,13 +34,13 @@ class HostedSource extends Source {
/// site.
Future<List<Version>> getVersions(String name, description) {
var url = _makeUrl(description,
(server, package) => "$server/packages/$package.json");
(server, package) => "$server/api/packages/$package");
log.io("Get versions from $url.");
return httpClient.read(url).then((body) {
return httpClient.read(url, headers: PUB_API_HEADERS).then((body) {
var doc = json.parse(body);
return doc['versions']
.map((version) => new Version.parse(version))
.map((version) => new Version.parse(version['version']))
.toList();
}).catchError((ex) {
var parsed = _parseDescription(description);
@@ -53,15 +53,17 @@ class HostedSource extends Source {
Future<Pubspec> describeUncached(PackageId id) {
// Request it from the server.
var url = _makeVersionUrl(id, (server, package, version) =>
"$server/packages/$package/versions/$version.yaml");
"$server/api/packages/$package/versions/$version");
log.io("Describe package at $url.");
return httpClient.read(url).then((yaml) {
return httpClient.read(url, headers: PUB_API_HEADERS).then((version) {
version = json.parse(version);
// TODO(rnystrom): After this is pulled down, we could place it in
// a secondary cache of just pubspecs. This would let us have a
// persistent cache for pubspecs for packages that haven't actually
// been installed.
return new Pubspec.parse(null, yaml, systemCache.sources);
return new Pubspec.fromMap(version['pubspec'], systemCache.sources);
}).catchError((ex) {
var parsed = _parseDescription(id.description);
_throwFriendlyError(ex, id, parsed.last);
+15 -2
View File
@@ -14,6 +14,7 @@ import 'package:http/http.dart' as http;
import 'io.dart';
import 'log.dart' as log;
import 'oauth2.dart' as oauth2;
import 'sdk.dart' as sdk;
import 'utils.dart';
// TODO(nweiz): make this configurable
@@ -24,6 +25,13 @@ final HTTP_TIMEOUT = 30 * 1000;
/// Headers and field names that should be censored in the log output.
final _CENSORED_FIELDS = const ['refresh_token', 'authorization'];
/// Headers required for pub.dartlang.org API requests.
///
/// The Accept header tells pub.dartlang.org which version of the API we're
/// expecting, so it can either serve that version or give us a 406 error if
/// it's not supported.
final PUB_API_HEADERS = const {'Accept': 'application/vnd.pub.v2+json'};
/// Whether dart:io's SecureSocket has been initialized with pub's resources
/// yet.
bool _initializedSecureSocket = false;
@@ -54,8 +62,6 @@ class PubHttpClient extends http.BaseClient {
stackTrace = localStackTrace;
}
// TODO(nweiz): Ideally the timeout would extend to reading from the
// response input stream, but until issue 3657 is fixed that's not feasible.
return timeout(inner.send(request).then((streamedResponse) {
_logResponse(streamedResponse);
@@ -69,6 +75,13 @@ class PubHttpClient extends http.BaseClient {
return streamedResponse;
}
if (status == 406 &&
request.headers['Accept'] == PUB_API_HEADERS['Accept']) {
fail("Pub ${sdk.version} is incompatible with the current version of "
"${request.url.host}.\n"
"Upgrade pub to the latest version and try again.");
}
return http.Response.fromStream(streamedResponse).then((response) {
throw new PubHttpException(response);
});
+130 -120
View File
@@ -74,6 +74,13 @@ class Pubspec {
bool get isEmpty =>
name == null && version == Version.none && dependencies.isEmpty;
/// Returns a Pubspec object for an already-parsed map representing its
/// contents.
///
/// This will validate that [contents] is a valid pubspec.
factory Pubspec.fromMap(Map contents, SourceRegistry sources) =>
_parseMap(null, contents, sources);
// TODO(rnystrom): Instead of allowing a null argument here, split this up
// into load(), parse(), and _parse() like LockFile does.
/// Parses the pubspec stored at [filePath] whose text is [contents]. If the
@@ -82,9 +89,6 @@ class Pubspec {
/// file system.
factory Pubspec.parse(String filePath, String contents,
SourceRegistry sources) {
var name = null;
var version = Version.none;
if (contents.trim() == '') return new Pubspec.empty();
var parsedPubspec = loadYaml(contents);
@@ -94,123 +98,7 @@ class Pubspec {
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(filePath, sources,
parsedPubspec['dependencies']);
var devDependencies = _parseDependencies(filePath, sources,
parsedPubspec['dev_dependencies']);
// Make sure the same package doesn't appear as both a regular and dev
// dependency.
var dependencyNames = dependencies.map((dep) => dep.name).toSet();
var collisions = dependencyNames.intersection(
devDependencies.map((dep) => dep.name).toSet());
if (!collisions.isEmpty) {
var packageNames;
if (collisions.length == 1) {
packageNames = 'Package "${collisions.first}"';
} else {
var names = collisions.toList();
names.sort();
var buffer = new StringBuffer();
buffer.write("Packages ");
for (var i = 0; i < names.length; i++) {
buffer.write('"');
buffer.write(names[i]);
buffer.write('"');
if (i == names.length - 2) {
buffer.write(", ");
} else if (i == names.length - 1) {
buffer.write(", and ");
}
}
packageNames = buffer.toString();
}
throw new FormatException(
'$packageNames cannot appear in both "dependencies" and '
'"dev_dependencies".');
}
var environmentYaml = parsedPubspec['environment'];
var sdkConstraint = VersionConstraint.any;
if (environmentYaml != null) {
if (environmentYaml is! Map) {
throw new FormatException(
'The pubspec "environment" field should be a map, but was '
'"$environmentYaml".');
}
var sdkYaml = environmentYaml['sdk'];
if (sdkYaml is! String) {
throw new FormatException(
'The "sdk" field of "environment" should be a string, but was '
'"$sdkYaml".');
}
sdkConstraint = new VersionConstraint.parse(sdkYaml);
}
var environment = new PubspecEnvironment(sdkConstraint);
// 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')) {
_validateFieldUrl(parsedPubspec['homepage'], 'homepage');
}
if (parsedPubspec.containsKey('documentation')) {
_validateFieldUrl(parsedPubspec['documentation'], 'documentation');
}
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, devDependencies,
environment, parsedPubspec);
return _parseMap(filePath, parsedPubspec, sources);
}
}
@@ -231,6 +119,128 @@ void _validateFieldUrl(url, String field) {
}
}
Pubspec _parseMap(String filePath, Map map, SourceRegistry sources) {
var name = null;
var version = Version.none;
if (map.containsKey('name')) {
name = map['name'];
if (name is! String) {
throw new FormatException(
'The pubspec "name" field should be a string, but was "$name".');
}
}
if (map.containsKey('version')) {
version = new Version.parse(map['version']);
}
var dependencies = _parseDependencies(filePath, sources,
map['dependencies']);
var devDependencies = _parseDependencies(filePath, sources,
map['dev_dependencies']);
// Make sure the same package doesn't appear as both a regular and dev
// dependency.
var dependencyNames = dependencies.map((dep) => dep.name).toSet();
var collisions = dependencyNames.intersection(
devDependencies.map((dep) => dep.name).toSet());
if (!collisions.isEmpty) {
var packageNames;
if (collisions.length == 1) {
packageNames = 'Package "${collisions.first}"';
} else {
var names = collisions.toList();
names.sort();
var buffer = new StringBuffer();
buffer.write("Packages ");
for (var i = 0; i < names.length; i++) {
buffer.write('"');
buffer.write(names[i]);
buffer.write('"');
if (i == names.length - 2) {
buffer.write(", ");
} else if (i == names.length - 1) {
buffer.write(", and ");
}
}
packageNames = buffer.toString();
}
throw new FormatException(
'$packageNames cannot appear in both "dependencies" and '
'"dev_dependencies".');
}
var environmentYaml = map['environment'];
var sdkConstraint = VersionConstraint.any;
if (environmentYaml != null) {
if (environmentYaml is! Map) {
throw new FormatException(
'The pubspec "environment" field should be a map, but was '
'"$environmentYaml".');
}
var sdkYaml = environmentYaml['sdk'];
if (sdkYaml is! String) {
throw new FormatException(
'The "sdk" field of "environment" should be a string, but was '
'"$sdkYaml".');
}
sdkConstraint = new VersionConstraint.parse(sdkYaml);
}
var environment = new PubspecEnvironment(sdkConstraint);
// 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 (map.containsKey('homepage')) {
_validateFieldUrl(map['homepage'], 'homepage');
}
if (map.containsKey('documentation')) {
_validateFieldUrl(map['documentation'], 'documentation');
}
if (map.containsKey('author') && map['author'] is! String) {
throw new FormatException(
'The "author" field should be a string, but was '
'${map["author"]}.');
}
if (map.containsKey('authors')) {
var authors = map['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 (map.containsKey('author')) {
throw new FormatException('A pubspec should not have both an "author" '
'and an "authors" field.');
}
}
return new Pubspec(name, version, dependencies, devDependencies,
environment, map);
}
List<PackageDep> _parseDependencies(String pubspecPath, SourceRegistry sources,
yaml) {
var dependencies = <PackageDep>[];
@@ -19,11 +19,24 @@ main() {
currentSchedule.timeout *= 3;
serve([
d.dir('api', [
d.dir('packages', [
d.file('browser', json.stringify({
'versions': [packageVersionApiMap(packageMap('browser', '1.0.0'))]
})),
d.dir('browser', [
d.dir('versions', [
d.file('1.0.0', json.stringify(
packageVersionApiMap(
packageMap('browser', '1.0.0'),
full: true)))
])
])
])
]),
d.dir('packages', [
d.file('browser.json', json.stringify({'versions': ['1.0.0']})),
d.dir('browser', [
d.dir('versions', [
d.file('1.0.0.yaml', yaml(packageMap("browser", "1.0.0"))),
d.tar('1.0.0.tar.gz', [
d.file('pubspec.yaml', yaml(packageMap("browser", "1.0.0"))),
d.dir('lib', [
@@ -0,0 +1,68 @@
// 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 pub_tests;
import 'dart:io';
import 'package:scheduled_test/scheduled_server.dart';
import 'package:scheduled_test/scheduled_test.dart';
import '../descriptor.dart' as d;
import '../test_pub.dart';
main() {
initConfig();
forBothPubInstallAndUpdate((command) {
integration('sends the correct Accept header', () {
var server = new ScheduledServer();
d.appDir([{
"hosted": {
"name": "foo",
"url": server.url.then((url) => url.toString())
}
}]).create();
var pub = startPub(args: [command.name]);
server.handle('GET', '/api/packages/foo', (request) {
expect(request.headers['Accept'], ['application/vnd.pub.v2+json']);
});
pub.kill();
});
integration('prints a friendly error if the version is out-of-date', () {
var server = new ScheduledServer();
d.appDir([{
"hosted": {
"name": "foo",
"url": server.url.then((url) => url.toString())
}
}]).create();
var pub = startPub(args: [command.name]);
server.handle('GET', '/api/packages/foo', (request) {
request.response.statusCode = 406;
request.response.close();
});
// TODO(nweiz): this shouldn't request the versions twice (issue 11077).
server.handle('GET', '/api/packages/foo', (request) {
request.response.statusCode = 406;
request.response.close();
});
pub.shouldExit(1);
expect(pub.remainingStderr(), completion(equals(
"Pub 0.1.2+3 is incompatible with the current version of localhost.\n"
"Upgrade pub to the latest version and try again.")));
});
});
}
@@ -22,7 +22,7 @@ main() {
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
request.response.statusCode = 400;
request.response.write(json.stringify({
'error': {'message': 'your request sucked'}
@@ -22,7 +22,7 @@ main() {
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
request.response.write('{not json');
request.response.close();
});
+1 -1
View File
@@ -14,7 +14,7 @@ import '../../lib/src/io.dart';
import '../test_pub.dart';
void handleUploadForm(ScheduledServer server, [Map body]) {
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
return server.url.then((url) {
expect(request.headers.value('authorization'),
equals('Bearer access token'));
@@ -29,7 +29,7 @@ main() {
confirmPublish(pub);
authorizePub(pub, server, "new access token");
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer new access token'));
@@ -24,7 +24,7 @@ main() {
var pub = startPublish(server);
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer access token'));
@@ -45,7 +45,7 @@ main() {
expect(pub.nextLine(), completion(matches(r'Uploading\.\.\.+')));
authorizePub(pub, server, 'new access token');
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer new access token'));
@@ -45,7 +45,7 @@ main() {
});
});
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer new access token'));
@@ -32,7 +32,7 @@ main() {
"packages has expired and can't be automatically refreshed.")));
authorizePub(pub, server, "new access token");
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer new access token'));
@@ -24,7 +24,7 @@ main() {
confirmPublish(pub);
authorizePub(pub, server);
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
expect(request.headers.value('authorization'),
equals('Bearer access token'));
@@ -25,7 +25,7 @@ main() {
confirmPublish(pub);
server.handle('GET', '/packages/versions/new.json', (request) {
server.handle('GET', '/api/packages/versions/new', (request) {
var response = request.response;
response.statusCode = 401;
response.headers.set('www-authenticate', 'Bearer error="invalid_token",'
@@ -58,7 +58,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
server.handle('POST', '/packages/pkg/uploaders.json', (request) {
server.handle('POST', '/api/packages/pkg/uploaders', (request) {
expect(new ByteStream(request).toBytes().then((bodyBytes) {
expect(new String.fromCharCodes(bodyBytes), equals('email=email'));
@@ -80,7 +80,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['--package', 'pkg', 'remove', 'email']);
server.handle('DELETE', '/packages/pkg/uploaders/email.json', (request) {
server.handle('DELETE', '/api/packages/pkg/uploaders/email', (request) {
request.response.headers.contentType =
new ContentType("application", "json");
request.response.write(json.stringify({
@@ -100,7 +100,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['add', 'email']);
server.handle('POST', '/packages/test_pkg/uploaders.json', (request) {
server.handle('POST', '/api/packages/test_pkg/uploaders', (request) {
request.response.headers.contentType =
new ContentType("application", "json");
request.response.write(json.stringify({
@@ -118,7 +118,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
server.handle('POST', '/packages/pkg/uploaders.json', (request) {
server.handle('POST', '/api/packages/pkg/uploaders', (request) {
request.response.statusCode = 400;
request.response.headers.contentType =
new ContentType("application", "json");
@@ -138,7 +138,7 @@ main() {
var pub = startPubUploader(server,
['--package', 'pkg', 'remove', 'e/mail']);
server.handle('DELETE', '/packages/pkg/uploaders/e%2Fmail.json', (request) {
server.handle('DELETE', '/api/packages/pkg/uploaders/e%2Fmail', (request) {
request.response.statusCode = 400;
request.response.headers.contentType =
new ContentType("application", "json");
@@ -157,7 +157,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
server.handle('POST', '/packages/pkg/uploaders.json', (request) {
server.handle('POST', '/api/packages/pkg/uploaders', (request) {
request.response.write("{not json");
request.response.close();
});
@@ -172,7 +172,7 @@ main() {
d.credentialsFile(server, 'access token').create();
var pub = startPubUploader(server, ['--package', 'pkg', 'remove', 'email']);
server.handle('DELETE', '/packages/pkg/uploaders/email.json', (request) {
server.handle('DELETE', '/api/packages/pkg/uploaders/email', (request) {
request.response.write("{not json");
request.response.close();
});
+79 -25
View File
@@ -147,16 +147,26 @@ Future _closeServer() {
return sleep(10);
}
/// The [d.DirectoryDescriptor] describing the server layout of packages that
/// are being served via [servePackages]. This is `null` if [servePackages] has
/// not yet been called for this test.
/// The [d.DirectoryDescriptor] describing the server layout of `/api/packages`
/// on the test server.
///
/// This contains metadata for packages that are being served via
/// [servePackages]. It's `null` if [servePackages] has not yet been called for
/// this test.
d.DirectoryDescriptor _servedApiPackageDir;
/// The [d.DirectoryDescriptor] describing the server layout of `/packages` on
/// the test server.
///
/// This contains the tarballs for packages that are being served via
/// [servePackages]. It's `null` if [servePackages] has not yet been called for
/// this test.
d.DirectoryDescriptor _servedPackageDir;
/// A map from package names to version numbers to YAML-serialized pubspecs for
/// those packages. This represents the packages currently being served by
/// [servePackages], and is `null` if [servePackages] has not yet been called
/// for this test.
Map<String, Map<String, String>> _servedPackages;
/// A map from package names to parsed pubspec maps for those packages. This
/// represents the packages currently being served by [servePackages], and is
/// `null` if [servePackages] has not yet been called for this test.
Map<String, List<Map>> _servedPackages;
/// Creates an HTTP server that replicates the structure of pub.dartlang.org.
/// [pubspecs] is a list of unserialized pubspecs representing the packages to
@@ -166,12 +176,17 @@ Map<String, Map<String, String>> _servedPackages;
/// are being served. Previous packages will continue to be served.
void servePackages(List<Map> pubspecs) {
if (_servedPackages == null || _servedPackageDir == null) {
_servedPackages = <String, Map<String, String>>{};
_servedPackages = <String, List<Map>>{};
_servedApiPackageDir = d.dir('packages', []);
_servedPackageDir = d.dir('packages', []);
serve([_servedPackageDir]);
serve([
d.dir('api', [_servedApiPackageDir]),
_servedPackageDir
]);
currentSchedule.onComplete.schedule(() {
_servedPackages = null;
_servedApiPackageDir = null;
_servedPackageDir = null;
}, 'cleaning up served packages');
}
@@ -181,28 +196,36 @@ void servePackages(List<Map> pubspecs) {
for (var spec in resolvedPubspecs) {
var name = spec['name'];
var version = spec['version'];
var versions = _servedPackages.putIfAbsent(
name, () => <String, String>{});
versions[version] = yaml(spec);
var versions = _servedPackages.putIfAbsent(name, () => []);
versions.add(spec);
}
_servedApiPackageDir.contents.clear();
_servedPackageDir.contents.clear();
for (var name in _servedPackages.keys) {
var versions = _servedPackages[name].keys.toList();
_servedPackageDir.contents.addAll([
d.file('$name.json', json.stringify({'versions': versions})),
_servedApiPackageDir.contents.addAll([
d.file('$name', json.stringify({
'name': name,
'uploaders': ['nweiz@google.com'],
'versions': _servedPackages[name].map(packageVersionApiMap).toList()
})),
d.dir(name, [
d.dir('versions', flatten(versions.map((version) {
return [
d.file('$version.yaml', _servedPackages[name][version]),
d.tar('$version.tar.gz', [
d.file('pubspec.yaml', _servedPackages[name][version]),
d.libDir(name, '$name $version')
])
];
})))
d.dir('versions', _servedPackages[name].map((pubspec) {
return d.file(pubspec['version'], json.stringify(
packageVersionApiMap(pubspec, full: true)));
}))
])
]);
_servedPackageDir.contents.add(d.dir(name, [
d.dir('versions', _servedPackages[name].map((pubspec) {
var version = pubspec['version'];
return d.tar('$version.tar.gz', [
d.file('pubspec.yaml', json.stringify(pubspec)),
d.libDir(name, '$name $version')
]);
}))
]));
}
});
}, 'initializing the package server');
@@ -632,6 +655,37 @@ String _packageName(String sourceName, description) {
}
}
/// Returns a Map in the format used by the pub.dartlang.org API to represent a
/// package version.
///
/// [pubspec] is the parsed pubspec of the package version. If [full] is true,
/// this returns the complete map, including metadata that's only included when
/// requesting the package version directly.
Map packageVersionApiMap(Map pubspec, {bool full: false}) {
var name = pubspec['name'];
var version = pubspec['version'];
var map = {
'pubspec': pubspec,
'version': version,
'url': '/api/packages/$name/versions/$version',
'archive_url': '/packages/$name/versions/$version.tar.gz',
'new_dartdoc_url': '/api/packages/$name/versions/$version'
'/new_dartdoc',
'package_url': '/api/packages/$name'
};
if (full) {
mapAddAll(map, {
'downloads': 0,
'created': '2012-09-25T18:38:28.685260',
'libraries': ['$name.dart'],
'uploader': ['nweiz@google.com']
});
}
return map;
}
/// Compares the [actual] output from running pub with [expected]. For [String]
/// patterns, ignores leading and trailing whitespace differences and tries to
/// report the offending difference in a nice way. For other [Pattern]s, just
@@ -35,7 +35,7 @@ expectDependencyValidationWarning(String warning) {
setUpDependency(Map dep, {List<String> hostedVersions}) {
useMockClient(new MockClient((request) {
expect(request.method, equals("GET"));
expect(request.url.path, equals("/packages/foo.json"));
expect(request.url.path, equals("/api/packages/foo"));
if (hostedVersions == null) {
return new Future.value(new http.Response("not found", 404));
@@ -43,7 +43,8 @@ setUpDependency(Map dep, {List<String> hostedVersions}) {
return new Future.value(new http.Response(json.stringify({
"name": "foo",
"uploaders": ["nweiz@google.com"],
"versions": hostedVersions
"versions": hostedVersions.map((version) =>
packageVersionApiMap(packageMap('foo', version))).toList()
}), 200));
}
}));