diff --git a/Cargo.lock b/Cargo.lock
index b253a8cf7cd..72504dbc1e3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -489,6 +489,7 @@ dependencies = [
"openssl 0.9.4 (registry+https://github.com/rust-lang/crates.io-index)",
"phf 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)",
"postgres-protocol 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
]
diff --git a/Cargo.toml b/Cargo.toml
index 0ce7a391666..cf656ef8cef 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,7 @@ git2 = "0.6"
flate2 = "0.2"
semver = "0.5"
url = "1.2.1"
-postgres = { version = "0.13", features = ["with-time", "with-openssl"] }
+postgres = { version = "0.13", features = ["with-time", "with-openssl", "with-rustc-serialize"] }
r2d2 = "0.7.0"
r2d2_postgres = "0.11"
openssl = "0.9"
diff --git a/app/components/badge-appveyor.js b/app/components/badge-appveyor.js
new file mode 100644
index 00000000000..fddf752fcd3
--- /dev/null
+++ b/app/components/badge-appveyor.js
@@ -0,0 +1,16 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: ['badge'],
+ repository: Ember.computed.alias('badge.attributes.repository'),
+ branch: Ember.computed('badge.attributes.branch', function() {
+ return this.get('badge.attributes.branch') || 'master';
+ }),
+ service: Ember.computed('badge.attributes.service', function() {
+ return this.get('badge.attributes.service') || 'github';
+ }),
+ text: Ember.computed('badge', function() {
+ return `Appveyor build status for the ${ this.get('branch') } branch`;
+ })
+});
diff --git a/app/components/badge-travis-ci.js b/app/components/badge-travis-ci.js
new file mode 100644
index 00000000000..774d1a29afe
--- /dev/null
+++ b/app/components/badge-travis-ci.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: ['badge'],
+ repository: Ember.computed.alias('badge.attributes.repository'),
+ branch: Ember.computed('badge.attributes.branch', function() {
+ return this.get('badge.attributes.branch') || 'master';
+ }),
+ text: Ember.computed('branch', function() {
+ return `Travis CI build status for the ${ this.get('branch') } branch`;
+ })
+});
diff --git a/app/controllers/crate/version.js b/app/controllers/crate/version.js
index 99216589ed0..4dd6e0e81a7 100644
--- a/app/controllers/crate/version.js
+++ b/app/controllers/crate/version.js
@@ -20,6 +20,7 @@ export default Ember.Controller.extend({
requestedVersion: null,
keywords: computed.alias('crate.keywords'),
categories: computed.alias('crate.categories'),
+ badges: computed.alias('crate.badges'),
sortedVersions: computed.readOnly('crate.versions'),
diff --git a/app/mirage/fixtures/search.js b/app/mirage/fixtures/search.js
index 38889e3a683..1a9b0826927 100644
--- a/app/mirage/fixtures/search.js
+++ b/app/mirage/fixtures/search.js
@@ -19,6 +19,21 @@ export default {
"name": "rust_mixin",
"repository": "https://github.com/huonw/external_mixin",
"updated_at": "2015-02-27T11:52:13Z",
+ "badges": [
+ {
+ "attributes": {
+ "repository": "huonw/external_mixin"
+ },
+ "badge_type": "appveyor"
+ },
+ {
+ "attributes": {
+ "branch": "master",
+ "repository": "huonw/external_mixin"
+ },
+ "badge_type": "travis-ci"
+ }
+ ],
"versions": null
}, {
"created_at": "2015-02-27T11:51:58Z",
diff --git a/app/models/crate.js b/app/models/crate.js
index 9065b323f7e..e5c07f40af8 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -1,4 +1,5 @@
import DS from 'ember-data';
+import Ember from 'ember';
export default DS.Model.extend({
name: DS.attr('string'),
@@ -17,6 +18,16 @@ export default DS.Model.extend({
license: DS.attr('string'),
versions: DS.hasMany('versions', { async: true }),
+ badges: DS.attr(),
+ enhanced_badges: Ember.computed.map('badges', badge => ({
+ // jshint ignore:start
+ // needed until https://github.com/jshint/jshint/issues/2991 is fixed
+ ...badge,
+ // jshint ignore:end
+ component_name: `badge-${badge.badge_type}`
+ })),
+ badge_sort: ['badge_type'],
+ annotated_badges: Ember.computed.sort('enhanced_badges', 'badge_sort'),
owners: DS.hasMany('users', { async: true }),
version_downloads: DS.hasMany('version-download', { async: true }),
keywords: DS.hasMany('keywords', { async: true }),
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 6bc03d4eb51..05e2b453280 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -119,9 +119,6 @@
}
.vers {
margin-left: 10px;
- img {
- margin-bottom: -4px;
- }
}
.stats {
diff --git a/app/templates/components/badge-appveyor.hbs b/app/templates/components/badge-appveyor.hbs
new file mode 100644
index 00000000000..58495f33960
--- /dev/null
+++ b/app/templates/components/badge-appveyor.hbs
@@ -0,0 +1,6 @@
+
+
+
diff --git a/app/templates/components/badge-travis-ci.hbs b/app/templates/components/badge-travis-ci.hbs
new file mode 100644
index 00000000000..05985aa3421
--- /dev/null
+++ b/app/templates/components/badge-travis-ci.hbs
@@ -0,0 +1,6 @@
+
+
+
diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs
index 1c8bfb07529..80c27e3e65e 100644
--- a/app/templates/components/crate-row.hbs
+++ b/app/templates/components/crate-row.hbs
@@ -7,6 +7,9 @@
alt="{{ crate.max_version }}"
title="{{ crate.name }}’s current version badge" />
+ {{#each crate.annotated_badges as |badge|}}
+ {{component badge.component_name badge=badge}}
+ {{/each}}
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index 419f52d5a80..30dccfaf25f 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -71,6 +71,12 @@
alt="{{ crate.name }}’s current version badge"
title="{{ crate.name }}’s current version badge" />
+
+ {{#each crate.annotated_badges as |badge|}}
+
+ {{component badge.component_name badge=badge}}
+
+ {{/each}}
diff --git a/src/badge.rs b/src/badge.rs
new file mode 100644
index 00000000000..7a94847d9ed
--- /dev/null
+++ b/src/badge.rs
@@ -0,0 +1,194 @@
+use util::CargoResult;
+use krate::Crate;
+use Model;
+
+use std::collections::HashMap;
+use pg::GenericConnection;
+use pg::rows::Row;
+use rustc_serialize::json::Json;
+
+#[derive(Debug, PartialEq, Clone)]
+pub enum Badge {
+ TravisCi {
+ repository: String, branch: Option
,
+ },
+ Appveyor {
+ repository: String, branch: Option, service: Option,
+ },
+}
+
+#[derive(RustcEncodable, RustcDecodable, PartialEq, Debug)]
+pub struct EncodableBadge {
+ pub badge_type: String,
+ pub attributes: HashMap,
+}
+
+impl Model for Badge {
+ fn from_row(row: &Row) -> Badge {
+ let attributes: Json = row.get("attributes");
+ if let Json::Object(attributes) = attributes {
+ let badge_type: String = row.get("badge_type");
+ match badge_type.as_str() {
+ "travis-ci" => {
+ Badge::TravisCi {
+ branch: attributes.get("branch")
+ .and_then(Json::as_string)
+ .map(str::to_string),
+ repository: attributes.get("repository")
+ .and_then(Json::as_string)
+ .map(str::to_string)
+ .expect("Invalid TravisCi badge \
+ without repository in the \
+ database"),
+ }
+ },
+ "appveyor" => {
+ Badge::Appveyor {
+ service: attributes.get("service")
+ .and_then(Json::as_string)
+ .map(str::to_string),
+ branch: attributes.get("branch")
+ .and_then(Json::as_string)
+ .map(str::to_string),
+ repository: attributes.get("repository")
+ .and_then(Json::as_string)
+ .map(str::to_string)
+ .expect("Invalid Appveyor badge \
+ without repository in the \
+ database"),
+ }
+ },
+ _ => {
+ panic!("Unknown badge type {} in the database", badge_type);
+ },
+ }
+ } else {
+ panic!(
+ "badge attributes {:?} in the database was not a JSON object",
+ attributes
+ );
+ }
+ }
+ fn table_name(_: Option) -> &'static str { "badges" }
+}
+
+impl Badge {
+ pub fn encodable(self) -> EncodableBadge {
+ EncodableBadge {
+ badge_type: self.badge_type().to_string(),
+ attributes: self.attributes(),
+ }
+ }
+
+ pub fn badge_type(&self) -> &'static str {
+ match *self {
+ Badge::TravisCi {..} => "travis-ci",
+ Badge::Appveyor {..} => "appveyor",
+ }
+ }
+
+ pub fn json_attributes(self) -> Json {
+ Json::Object(self.attributes().into_iter().map(|(k, v)| {
+ (k, Json::String(v))
+ }).collect())
+ }
+
+ fn attributes(self) -> HashMap {
+ let mut attributes = HashMap::new();
+
+ match self {
+ Badge::TravisCi { branch, repository } => {
+ attributes.insert(String::from("repository"), repository);
+ if let Some(branch) = branch {
+ attributes.insert(
+ String::from("branch"),
+ branch
+ );
+ }
+ },
+ Badge::Appveyor { service, branch, repository } => {
+ attributes.insert(String::from("repository"), repository);
+ if let Some(branch) = branch {
+ attributes.insert(
+ String::from("branch"),
+ branch
+ );
+ }
+ if let Some(service) = service {
+ attributes.insert(
+ String::from("service"),
+ service
+ );
+ }
+ }
+ }
+
+ attributes
+ }
+
+ fn from_attributes(badge_type: &str,
+ attributes: &HashMap)
+ -> Result {
+ match badge_type {
+ "travis-ci" => {
+ match attributes.get("repository") {
+ Some(repository) => {
+ Ok(Badge::TravisCi {
+ repository: repository.to_string(),
+ branch: attributes.get("branch")
+ .map(String::to_string),
+ })
+ },
+ None => Err(badge_type.to_string()),
+ }
+ },
+ "appveyor" => {
+ match attributes.get("repository") {
+ Some(repository) => {
+ Ok(Badge::Appveyor {
+ repository: repository.to_string(),
+ branch: attributes.get("branch")
+ .map(String::to_string),
+ service: attributes.get("service")
+ .map(String::to_string),
+
+ })
+ },
+ None => Err(badge_type.to_string()),
+ }
+ },
+ _ => Err(badge_type.to_string()),
+ }
+ }
+
+ pub fn update_crate(conn: &GenericConnection,
+ krate: &Crate,
+ badges: HashMap>)
+ -> CargoResult> {
+
+ let mut invalid_badges = vec![];
+
+ let badges: Vec<_> = badges.iter().filter_map(|(k, v)| {
+ Badge::from_attributes(k, v).map_err(|invalid_badge| {
+ invalid_badges.push(invalid_badge)
+ }).ok()
+ }).collect();
+
+ conn.execute("\
+ DELETE FROM badges \
+ WHERE crate_id = $1;",
+ &[&krate.id]
+ )?;
+
+ for badge in badges {
+ conn.execute("\
+ INSERT INTO badges (crate_id, badge_type, attributes) \
+ VALUES ($1, $2, $3) \
+ ON CONFLICT (crate_id, badge_type) DO UPDATE \
+ SET attributes = EXCLUDED.attributes;",
+ &[&krate.id, &badge.badge_type(), &badge.json_attributes()]
+ )?;
+ }
+ Ok(invalid_badges)
+ }
+}
diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs
index 9d1cb86db67..ce635d3db5a 100644
--- a/src/bin/migrate.rs
+++ b/src/bin/migrate.rs
@@ -819,6 +819,18 @@ fn migrations() -> Vec {
ON crates_categories;"));
Ok(())
}),
+ Migration::add_table(20170102131034, "badges", " \
+ crate_id INTEGER NOT NULL, \
+ badge_type VARCHAR NOT NULL, \
+ attributes JSONB NOT NULL"),
+ Migration::new(20170102145236, |tx| {
+ try!(tx.execute("CREATE UNIQUE INDEX badges_crate_type \
+ ON badges (crate_id, badge_type)", &[]));
+ Ok(())
+ }, |tx| {
+ try!(tx.execute("DROP INDEX badges_crate_type", &[]));
+ Ok(())
+ }),
];
// NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`
diff --git a/src/krate.rs b/src/krate.rs
index 0a3247b04d9..1fc5a486a79 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -20,7 +20,7 @@ use semver;
use time::{Timespec, Duration};
use url::Url;
-use {Model, User, Keyword, Version, Category};
+use {Model, User, Keyword, Version, Category, Badge};
use app::{App, RequestApp};
use db::RequestTransaction;
use dependency::{Dependency, EncodableDependency};
@@ -28,6 +28,7 @@ use download::{VersionDownload, EncodableVersionDownload};
use git;
use keyword::EncodableKeyword;
use category::EncodableCategory;
+use badge::EncodableBadge;
use upload;
use user::RequestUser;
use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights};
@@ -61,6 +62,7 @@ pub struct EncodableCrate {
pub versions: Option>,
pub keywords: Option>,
pub categories: Option>,
+ pub badges: Option>,
pub created_at: String,
pub downloads: i32,
pub max_version: String,
@@ -229,14 +231,16 @@ impl Crate {
parts.next().is_none()
}
- pub fn minimal_encodable(self) -> EncodableCrate {
- self.encodable(None, None, None)
+ pub fn minimal_encodable(self,
+ badges: Option>) -> EncodableCrate {
+ self.encodable(None, None, None, badges)
}
pub fn encodable(self,
versions: Option>,
keywords: Option<&[Keyword]>,
- categories: Option<&[Category]>)
+ categories: Option<&[Category]>,
+ badges: Option>)
-> EncodableCrate {
let Crate {
name, created_at, updated_at, downloads, max_version, description,
@@ -249,6 +253,9 @@ impl Crate {
};
let keyword_ids = keywords.map(|kws| kws.iter().map(|kw| kw.keyword.clone()).collect());
let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect());
+ let badges = badges.map(|bs| {
+ bs.iter().map(|b| b.clone().encodable()).collect()
+ });
EncodableCrate {
id: name.clone(),
name: name.clone(),
@@ -258,6 +265,7 @@ impl Crate {
versions: versions,
keywords: keyword_ids,
categories: category_ids,
+ badges: badges,
max_version: max_version.to_string(),
documentation: documentation,
homepage: homepage,
@@ -405,6 +413,13 @@ impl Crate {
Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
}
+ pub fn badges(&self, conn: &GenericConnection) -> CargoResult> {
+ let stmt = try!(conn.prepare("SELECT badges.* from badges \
+ WHERE badges.crate_id = $1"));
+ let rows = try!(stmt.query(&[&self.id]));
+ Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
+ }
+
/// Returns (dependency, dependent crate name)
pub fn reverse_dependencies(&self,
conn: &GenericConnection,
@@ -587,7 +602,8 @@ pub fn index(req: &mut Request) -> CargoResult {
let mut crates = Vec::new();
for row in try!(stmt.query(&args)).iter() {
let krate: Crate = Model::from_row(&row);
- crates.push(krate.minimal_encodable());
+ let badges = krate.badges(conn);
+ crates.push(krate.minimal_encodable(badges.ok()));
}
// Query for the total count of crates
@@ -622,7 +638,7 @@ pub fn summary(req: &mut Request) -> CargoResult {
let rows = try!(stmt.query(&[]));
Ok(rows.iter().map(|r| {
let krate: Crate = Model::from_row(&r);
- krate.minimal_encodable()
+ krate.minimal_encodable(None)
}).collect::>())
};
let new_crates = try!(tx.prepare("SELECT * FROM crates \
@@ -660,6 +676,7 @@ pub fn show(req: &mut Request) -> CargoResult {
let ids = versions.iter().map(|v| v.id).collect();
let kws = try!(krate.keywords(conn));
let cats = try!(krate.categories(conn));
+ let badges = try!(krate.badges(conn));
#[derive(RustcEncodable)]
struct R {
@@ -669,7 +686,9 @@ pub fn show(req: &mut Request) -> CargoResult {
categories: Vec,
}
Ok(req.json(&R {
- krate: krate.clone().encodable(Some(ids), Some(&kws), Some(&cats)),
+ krate: krate.clone().encodable(
+ Some(ids), Some(&kws), Some(&cats), Some(badges)
+ ),
versions: versions.into_iter().map(|v| {
v.encodable(&krate.name)
}).collect(),
@@ -746,6 +765,16 @@ pub fn new(req: &mut Request) -> CargoResult {
Category::update_crate(try!(req.tx()), &krate, &categories)
);
+ // Update all badges for this crate, collecting any invalid badges in
+ // order to be able to warn about them
+ let ignored_invalid_badges = try!(
+ Badge::update_crate(
+ try!(req.tx()),
+ &krate,
+ new_crate.badges.unwrap_or_else(HashMap::new)
+ )
+ );
+
// Upload the crate to S3
let mut handle = req.app().handle();
let path = krate.s3_path(&vers.to_string());
@@ -803,14 +832,21 @@ pub fn new(req: &mut Request) -> CargoResult {
bomb.path = None;
#[derive(RustcEncodable)]
- struct Warnings { invalid_categories: Vec }
+ struct Warnings {
+ invalid_categories: Vec,
+ invalid_badges: Vec,
+ }
let warnings = Warnings {
invalid_categories: ignored_invalid_categories,
+ invalid_badges: ignored_invalid_badges,
};
#[derive(RustcEncodable)]
struct R { krate: EncodableCrate, warnings: Warnings }
- Ok(req.json(&R { krate: krate.minimal_encodable(), warnings: warnings }))
+ Ok(req.json(&R {
+ krate: krate.minimal_encodable(None),
+ warnings: warnings
+ }))
}
fn parse_new_headers(req: &mut Request) -> CargoResult<(upload::NewCrate, User)> {
diff --git a/src/lib.rs b/src/lib.rs
index 550cb7d88a3..f3efe3fedb2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -34,6 +34,7 @@ extern crate conduit_router;
extern crate conduit_static;
pub use app::App;
+pub use self::badge::Badge;
pub use self::category::Category;
pub use config::Config;
pub use self::dependency::Dependency;
@@ -53,6 +54,7 @@ use conduit_middleware::MiddlewareBuilder;
use util::{C, R, R404};
pub mod app;
+pub mod badge;
pub mod categories;
pub mod category;
pub mod config;
diff --git a/src/tests/all.rs b/src/tests/all.rs
index 4f288adc969..a008de37d67 100755
--- a/src/tests/all.rs
+++ b/src/tests/all.rs
@@ -63,6 +63,7 @@ struct Error { detail: String }
#[derive(RustcDecodable)]
struct Bad { errors: Vec }
+mod badge;
mod category;
mod git;
mod keyword;
@@ -272,30 +273,49 @@ fn new_req(app: Arc, krate: &str, version: &str) -> MockRequest {
fn new_req_full(app: Arc, krate: Crate, version: &str,
deps: Vec) -> MockRequest {
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
- req.with_body(&new_req_body(krate, version, deps, Vec::new(), Vec::new()));
+ req.with_body(&new_req_body(
+ krate, version, deps, Vec::new(), Vec::new(), HashMap::new()
+ ));
return req;
}
fn new_req_with_keywords(app: Arc, krate: Crate, version: &str,
kws: Vec) -> MockRequest {
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
- req.with_body(&new_req_body(krate, version, Vec::new(), kws, Vec::new()));
+ req.with_body(&new_req_body(
+ krate, version, Vec::new(), kws, Vec::new(), HashMap::new()
+ ));
return req;
}
fn new_req_with_categories(app: Arc, krate: Crate, version: &str,
cats: Vec) -> MockRequest {
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
- req.with_body(&new_req_body(krate, version, Vec::new(), Vec::new(), cats));
+ req.with_body(&new_req_body(
+ krate, version, Vec::new(), Vec::new(), cats, HashMap::new()
+ ));
+ return req;
+}
+
+fn new_req_with_badges(app: Arc, krate: Crate, version: &str,
+ badges: HashMap>)
+ -> MockRequest {
+ let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
+ req.with_body(&new_req_body(
+ krate, version, Vec::new(), Vec::new(), Vec::new(), badges
+ ));
return req;
}
fn new_req_body_version_2(krate: Crate) -> Vec {
- new_req_body(krate, "2.0.0", Vec::new(), Vec::new(), Vec::new())
+ new_req_body(
+ krate, "2.0.0", Vec::new(), Vec::new(), Vec::new(), HashMap::new()
+ )
}
fn new_req_body(krate: Crate, version: &str, deps: Vec,
- kws: Vec, cats: Vec) -> Vec {
+ kws: Vec, cats: Vec,
+ badges: HashMap>) -> Vec {
let kws = kws.into_iter().map(u::Keyword).collect();
let cats = cats.into_iter().map(u::Category).collect();
new_crate_to_body(&u::NewCrate {
@@ -313,6 +333,7 @@ fn new_req_body(krate: Crate, version: &str, deps: Vec,
license: Some("MIT".to_string()),
license_file: None,
repository: krate.repository,
+ badges: Some(badges),
}, &[])
}
diff --git a/src/tests/badge.rs b/src/tests/badge.rs
new file mode 100644
index 00000000000..43c5618b989
--- /dev/null
+++ b/src/tests/badge.rs
@@ -0,0 +1,154 @@
+use conduit::{Request, Method};
+use postgres::GenericConnection;
+
+use cargo_registry::db::RequestTransaction;
+use cargo_registry::badge::Badge;
+
+use std::collections::HashMap;
+
+fn tx(req: &Request) -> &GenericConnection { req.tx().unwrap() }
+
+#[test]
+fn update_crate() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/badged_crate");
+
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, _) = ::mock_crate(&mut req, ::krate("badged_crate"));
+
+ let appveyor = Badge::Appveyor {
+ service: Some(String::from("github")),
+ branch: None,
+ repository: String::from("rust-lang/cargo"),
+ };
+ let mut badge_attributes_appveyor = HashMap::new();
+ badge_attributes_appveyor.insert(
+ String::from("service"),
+ String::from("github")
+ );
+ badge_attributes_appveyor.insert(
+ String::from("repository"),
+ String::from("rust-lang/cargo")
+ );
+
+ let travis_ci = Badge::TravisCi {
+ branch: Some(String::from("beta")),
+ repository: String::from("rust-lang/rust"),
+ };
+ let mut badge_attributes_travis_ci = HashMap::new();
+ badge_attributes_travis_ci.insert(
+ String::from("branch"),
+ String::from("beta")
+ );
+ badge_attributes_travis_ci.insert(
+ String::from("repository"),
+ String::from("rust-lang/rust")
+ );
+
+ let mut badges = HashMap::new();
+
+ // Updating with no badges has no effect
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]);
+
+ // Happy path adding one badge
+ badges.insert(
+ String::from("appveyor"),
+ badge_attributes_appveyor.clone()
+ );
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![appveyor.clone()]);
+
+ // Replacing one badge with another
+ badges.clear();
+ badges.insert(
+ String::from("travis-ci"),
+ badge_attributes_travis_ci.clone()
+ );
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![travis_ci.clone()]);
+
+ // Updating badge attributes
+ let travis_ci2 = Badge::TravisCi {
+ branch: None,
+ repository: String::from("rust-lang/rust"),
+ };
+ let mut badge_attributes_travis_ci2 = HashMap::new();
+ badge_attributes_travis_ci2.insert(
+ String::from("repository"),
+ String::from("rust-lang/rust")
+ );
+ badges.insert(
+ String::from("travis-ci"),
+ badge_attributes_travis_ci2.clone()
+ );
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![travis_ci2.clone()]);
+
+ // Removing one badge
+ badges.clear();
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]);
+
+ // Adding 2 badges
+ badges.insert(
+ String::from("appveyor"),
+ badge_attributes_appveyor.clone()
+ );
+ badges.insert(
+ String::from("travis-ci"),
+ badge_attributes_travis_ci.clone()
+ );
+ Badge::update_crate(
+ tx(&req), &krate, badges.clone()
+ ).unwrap();
+
+ let current_badges = krate.badges(tx(&req)).unwrap();
+ assert_eq!(current_badges.len(), 2);
+ assert!(current_badges.contains(&appveyor));
+ assert!(current_badges.contains(&travis_ci));
+
+ // Removing all badges
+ badges.clear();
+ Badge::update_crate(tx(&req), &krate, badges.clone()).unwrap();
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![]);
+
+ // Attempting to add one valid badge (appveyor) and two invalid badges
+ // (travis-ci without a required attribute and an unknown badge type)
+
+ // Extra invalid keys are fine, we'll just ignore those
+ badge_attributes_appveyor.insert(
+ String::from("extra"),
+ String::from("info")
+ );
+ badges.insert(
+ String::from("appveyor"),
+ badge_attributes_appveyor.clone()
+ );
+
+ // Repository is a required key
+ badge_attributes_travis_ci.remove("repository");
+ badges.insert(
+ String::from("travis-ci"),
+ badge_attributes_travis_ci.clone()
+ );
+
+ // This is not a badge that crates.io knows about
+ let mut invalid_attributes = HashMap::new();
+ invalid_attributes.insert(
+ String::from("not-a-badge-attribute"),
+ String::from("not-a-badge-value")
+ );
+ badges.insert(
+ String::from("not-a-badge"),
+ invalid_attributes.clone()
+ );
+
+ let invalid_badges = Badge::update_crate(
+ tx(&req), &krate, badges.clone()
+ ).unwrap();
+ assert_eq!(invalid_badges.len(), 2);
+ assert!(invalid_badges.contains(&String::from("travis-ci")));
+ assert!(invalid_badges.contains(&String::from("not-a-badge")));
+ assert_eq!(krate.badges(tx(&req)).unwrap(), vec![appveyor.clone()]);
+}
diff --git a/src/tests/http-data/krate_good_badges b/src/tests/http-data/krate_good_badges
new file mode 100644
index 00000000000..d0871b53db2
--- /dev/null
+++ b/src/tests/http-data/krate_good_badges
@@ -0,0 +1,21 @@
+===REQUEST 343
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foobadger/foobadger-1.0.0.crate HTTP/1.1
+Accept: */*
+Proxy-Connection: Keep-Alive
+Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk=
+Content-Length: 0
+Host: alexcrichton-test.s3.amazonaws.com
+Content-Type: application/x-tar
+Date: Sun, 28 Jun 2015 14:07:17 -0700
+
+
+===RESPONSE 258
+HTTP/1.1 200
+x-amz-request-id: CB0E925D8E3AB3E8
+x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g=
+date: Sun, 28 Jun 2015 21:07:51 GMT
+etag: "d41d8cd98f00b204e9800998ecf8427e"
+content-length: 0
+server: AmazonS3
+
+
diff --git a/src/tests/http-data/krate_ignored_badges b/src/tests/http-data/krate_ignored_badges
new file mode 100644
index 00000000000..425ca782969
--- /dev/null
+++ b/src/tests/http-data/krate_ignored_badges
@@ -0,0 +1,21 @@
+===REQUEST 359
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_ignored_badge/foo_ignored_badge-1.0.0.crate HTTP/1.1
+Accept: */*
+Proxy-Connection: Keep-Alive
+Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk=
+Content-Length: 0
+Host: alexcrichton-test.s3.amazonaws.com
+Content-Type: application/x-tar
+Date: Sun, 28 Jun 2015 14:07:17 -0700
+
+
+===RESPONSE 258
+HTTP/1.1 200
+x-amz-request-id: CB0E925D8E3AB3E8
+x-amz-id-2: SiaMwszM1p2TzXlLauvZ6kRKcUCg7HoyBW29vts42w9ArrLwkJWl8vuvPuGFkpM6XGH+YXN852g=
+date: Sun, 28 Jun 2015 21:07:51 GMT
+etag: "d41d8cd98f00b204e9800998ecf8427e"
+content-length: 0
+server: AmazonS3
+
+
diff --git a/src/tests/krate.rs b/src/tests/krate.rs
index 92b1971a854..752d4e7cb7f 100644
--- a/src/tests/krate.rs
+++ b/src/tests/krate.rs
@@ -3,6 +3,7 @@ use std::io::prelude::*;
use std::fs::{self, File};
use conduit::{Handler, Request, Method};
+
use git2;
use postgres::GenericConnection;
use rustc_serialize::json;
@@ -26,7 +27,7 @@ struct CrateMeta { total: i32 }
#[derive(RustcDecodable)]
struct GitCrate { name: String, vers: String, deps: Vec, cksum: String }
#[derive(RustcDecodable)]
-struct Warnings { invalid_categories: Vec }
+struct Warnings { invalid_categories: Vec, invalid_badges: Vec }
#[derive(RustcDecodable)]
struct GoodCrate { krate: EncodableCrate, warnings: Warnings }
#[derive(RustcDecodable)]
@@ -54,6 +55,7 @@ fn new_crate(name: &str) -> u::NewCrate {
license: Some("MIT".to_string()),
license_file: None,
repository: None,
+ badges: None,
}
}
@@ -915,6 +917,86 @@ fn ignored_categories() {
assert_eq!(json.warnings.invalid_categories, vec!["bar".to_string()]);
}
+#[test]
+fn good_badges() {
+ let krate = ::krate("foobadger");
+ let mut badges = HashMap::new();
+ let mut badge_attributes = HashMap::new();
+ badge_attributes.insert(
+ String::from("repository"),
+ String::from("rust-lang/crates.io")
+ );
+ badges.insert(String::from("travis-ci"), badge_attributes);
+
+ let (_b, app, middle) = ::app();
+ let mut req = ::new_req_with_badges(app, krate.clone(), "1.0.0", badges);
+
+ ::mock_user(&mut req, ::user("foo"));
+ let mut response = ok_resp!(middle.call(&mut req));
+
+ let json: GoodCrate = ::json(&mut response);
+ assert_eq!(json.krate.name, "foobadger");
+ assert_eq!(json.krate.max_version, "1.0.0");
+
+ let mut response = ok_resp!(
+ middle.call(req.with_method(Method::Get)
+ .with_path("/api/v1/crates/foobadger")));
+
+ let json: CrateResponse = ::json(&mut response);
+
+ let badges = json.krate.badges.unwrap();
+ assert_eq!(badges.len(), 1);
+ assert_eq!(badges[0].badge_type, "travis-ci");
+ assert_eq!(
+ badges[0].attributes.get("repository").unwrap(),
+ &String::from("rust-lang/crates.io")
+ );
+}
+
+#[test]
+fn ignored_badges() {
+ let krate = ::krate("foo_ignored_badge");
+ let mut badges = HashMap::new();
+
+ // Known badge type, missing required repository attribute
+ let mut badge_attributes = HashMap::new();
+ badge_attributes.insert(
+ String::from("branch"),
+ String::from("master")
+ );
+ badges.insert(String::from("travis-ci"), badge_attributes);
+
+ // Unknown badge type
+ let mut unknown_badge_attributes = HashMap::new();
+ unknown_badge_attributes.insert(
+ String::from("repository"),
+ String::from("rust-lang/rust")
+ );
+ badges.insert(String::from("not-a-badge"), unknown_badge_attributes);
+
+ let (_b, app, middle) = ::app();
+ let mut req = ::new_req_with_badges(app, krate.clone(), "1.0.0", badges);
+
+ ::mock_user(&mut req, ::user("foo"));
+ let mut response = ok_resp!(middle.call(&mut req));
+
+ let json: GoodCrate = ::json(&mut response);
+ assert_eq!(json.krate.name, "foo_ignored_badge");
+ assert_eq!(json.krate.max_version, "1.0.0");
+ assert_eq!(json.warnings.invalid_badges.len(), 2);
+ assert!(json.warnings.invalid_badges.contains(&"travis-ci".to_string()));
+ assert!(json.warnings.invalid_badges.contains(&"not-a-badge".to_string()));
+
+ let mut response = ok_resp!(
+ middle.call(req.with_method(Method::Get)
+ .with_path("/api/v1/crates/foo_ignored_badge")));
+
+ let json: CrateResponse = ::json(&mut response);
+
+ let badges = json.krate.badges.unwrap();
+ assert_eq!(badges.len(), 0);
+}
+
#[test]
fn reverse_dependencies() {
let (_b, app, middle) = ::app();
@@ -982,3 +1064,4 @@ fn author_license_and_description_required() {
!json.errors[0].detail.contains("license"),
"{:?}", json.errors);
}
+
diff --git a/src/upload.rs b/src/upload.rs
index 72a8ee32ba7..3a775645f6e 100644
--- a/src/upload.rs
+++ b/src/upload.rs
@@ -24,6 +24,7 @@ pub struct NewCrate {
pub license: Option,
pub license_file: Option,
pub repository: Option,
+ pub badges: Option>>,
}
#[derive(PartialEq, Eq, Hash)]
diff --git a/src/user/mod.rs b/src/user/mod.rs
index 1b3ea1853d4..8e9bdf1e928 100644
--- a/src/user/mod.rs
+++ b/src/user/mod.rs
@@ -336,7 +336,9 @@ pub fn updates(req: &mut Request) -> CargoResult {
}
// Encode everything!
- let crates = crates.into_iter().map(|c| c.minimal_encodable()).collect();
+ let crates = crates.into_iter().map(|c| {
+ c.minimal_encodable(None)
+ }).collect();
let versions = versions.into_iter().map(|v| {
let id = v.crate_id;
v.encodable(&map[&id])
diff --git a/tests/acceptance/search-test.js b/tests/acceptance/search-test.js
index 082b4279194..7bfa0b9dcc9 100644
--- a/tests/acceptance/search-test.js
+++ b/tests/acceptance/search-test.js
@@ -23,6 +23,10 @@ test('searching for "rust"', function(assert) {
hasText(assert, '#crates .row:first .desc .info', 'rust_mixin');
findWithAssert('#crates .row:first .desc .info .vers img[alt="0.0.1"]');
+
+ findWithAssert('#crates .row:first .desc .info .badge:first a img[src="https://ci.appveyor.com/api/projects/status/github/huonw/external_mixin?svg=true&branch=master"]');
+ findWithAssert('#crates .row:first .desc .info .badge:eq(1) a img[src="https://travis-ci.org/huonw/external_mixin.svg?branch=master"]');
+
hasText(assert, '#crates .row:first .desc .summary', 'Yo dawg, use Rust to generate Rust, right in your Rust. (See `external_mixin` to use scripting languages.)');
hasText(assert, '#crates .row:first .downloads', '477');
});