diff --git a/app/models/crate.js b/app/models/crate.js index 02e32bfd3cb..75fb5b64acb 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -20,4 +20,5 @@ export default DS.Model.extend({ owners: DS.hasMany('users', {async: true}), version_downloads: DS.hasMany('version-download', {async: true}), keywords: DS.hasMany('keywords', {async: true}), + reverse_dependencies: DS.hasMany('reverse-dependency', {async: true}), }); diff --git a/app/models/reverse-dependency.js b/app/models/reverse-dependency.js new file mode 100644 index 00000000000..1f44a3b5d42 --- /dev/null +++ b/app/models/reverse-dependency.js @@ -0,0 +1,3 @@ +import Dependency from 'cargo/models/dependency'; + +export default Dependency; diff --git a/app/router.js b/app/router.js index d725d9df23b..4aa49567b8a 100644 --- a/app/router.js +++ b/app/router.js @@ -14,6 +14,7 @@ Router.map(function() { this.resource('crate', { path: '/crates/*crate_id' }, function() { this.route('download'); this.route('versions'); + this.route('reverse_dependencies'); }); this.route('me', function() { this.route('crates'); diff --git a/app/routes/crate/reverse-dependencies.js b/app/routes/crate/reverse-dependencies.js new file mode 100644 index 00000000000..5a765b1ebf2 --- /dev/null +++ b/app/routes/crate/reverse-dependencies.js @@ -0,0 +1,20 @@ +import Ember from 'ember'; +import Crate from 'cargo/models/crate'; + +export default Ember.Route.extend({ + afterModel: function(data) { + console.log("afterModel"); + if (data instanceof Crate) { + return data.get('reverse_dependencies'); + } else { + return data.crate.get('reverse_dependencies'); + } + }, + + setupController: function(controller, data) { + if (data instanceof Crate) { + data = {crate: data, reverse_dependencies: null}; + } + this._super(controller, data.crate); + }, +}); diff --git a/app/templates/crate/reverse-dependencies.hbs b/app/templates/crate/reverse-dependencies.hbs new file mode 100644 index 00000000000..bcd37b17a1d --- /dev/null +++ b/app/templates/crate/reverse-dependencies.hbs @@ -0,0 +1,24 @@ +
+ {{#link-to 'crate' this}}⬅ Back to Main Page{{/link-to}} +
+ +
+ + All {{ reverse_dependencies.length }} + reverse dependencies of {{ name }} + +
+ +
+ {{#each model.reverse_dependencies}} +
+
+ {{#link-to 'crate' crate_id}}{{crate_id}}{{/link-to}} requires {{req}} + {{#link-to 'crate' this}}{{ num }}{{/link-to}} +
+ {{#link-to 'crate' this class='arrow'}} + + {{/link-to}} +
+ {{/each}} +
diff --git a/src/krate.rs b/src/krate.rs index f15ab7e529b..3340273dc31 100644 --- a/src/krate.rs +++ b/src/krate.rs @@ -17,6 +17,7 @@ use url::{mod, Url}; use {Model, User, Keyword, Version}; use app::{App, RequestApp}; use db::{Connection, RequestTransaction}; +use dependency::{Dependency, EncodableDependency}; use download::{VersionDownload, EncodableVersionDownload}; use git; use keyword::EncodableKeyword; @@ -68,6 +69,7 @@ pub struct CrateLinks { pub version_downloads: String, pub versions: Option, pub owners: Option, + pub reverse_dependencies: String, } impl Crate { @@ -253,6 +255,7 @@ impl Crate { version_downloads: format!("/api/v1/crates/{}/downloads", name), versions: versions_link, owners: Some(format!("/api/v1/crates/{}/owners", name)), + reverse_dependencies: format!("/api/v1/crates/{}/reverse_dependencies", name) }, } } @@ -333,6 +336,22 @@ impl Crate { let rows = try!(stmt.query(&[&self.id])); Ok(rows.map(|r| Model::from_row(&r)).collect()) } + + /// Returns (dependency, dependent crate name) + pub fn reverse_dependencies(&self, conn: &Connection) -> CargoResult> { + let stmt = try!(conn.prepare("SELECT dependencies.*, + crates.name AS crate_name + FROM dependencies + INNER JOIN versions + ON versions.id = dependencies.version_id + INNER JOIN crates + ON crates.id = versions.crate_id + WHERE dependencies.crate_id = $1 + AND versions.num = crates.max_version")); + Ok(try!(stmt.query(&[&self.id])).map(|r| { + (Model::from_row(&r), r.get("crate_name")) + }).collect()) + } } impl Model for Crate { @@ -860,3 +879,18 @@ fn modify_owners(req: &mut Request, add: bool) -> CargoResult { struct R { ok: bool } Ok(req.json(&R{ ok: true })) } + +pub fn reverse_dependencies(req: &mut Request) -> CargoResult { + let name = &req.params()["crate_id"]; + let conn = try!(req.tx()); + let krate = try!(Crate::find_by_name(conn, name.as_slice())); + let tx = try!(req.tx()); + let rev_deps = try!(krate.reverse_dependencies(tx)); + let rev_deps = rev_deps.into_iter().map(|(dep, crate_name)| { + dep.encodable(crate_name.as_slice()) + }).collect(); + + #[deriving(Encodable)] + struct R { reverse_dependencies: Vec } + Ok(req.json(&R{ reverse_dependencies: rev_deps })) +} diff --git a/src/lib.rs b/src/lib.rs old mode 100644 new mode 100755 index 92b1b131783..015b91218b0 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder { api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners)); api_router.delete("/crates/:crate_id/:version/yank", C(version::yank)); api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank)); + api_router.get("/crates/:crate_id/reverse_dependencies", C(krate::reverse_dependencies)); api_router.get("/versions", C(version::index)); api_router.get("/versions/:version_id", C(version::show)); api_router.get("/keywords", C(keyword::index)); diff --git a/src/tests/all.rs b/src/tests/all.rs old mode 100644 new mode 100755 index 3af9818b389..55f9a824663 --- a/src/tests/all.rs +++ b/src/tests/all.rs @@ -24,6 +24,7 @@ use conduit_test::MockRequest; use cargo_registry::app::App; use cargo_registry::db::{mod, RequestTransaction}; use cargo_registry::{User, Crate, Version, Keyword}; +use cargo_registry::util::CargoResult; macro_rules! t( ($e:expr) => ( match $e { @@ -213,8 +214,14 @@ fn mock_user(req: &mut Request, u: User) -> User { } fn mock_crate(req: &mut Request, krate: Crate) -> Crate { + let (c, v) = mock_crate_vers(req, krate, &semver::Version::parse("1.0.0").unwrap()); + v.unwrap(); + c +} +fn mock_crate_vers(req: &mut Request, krate: Crate, v: &semver::Version) + -> (Crate, CargoResult) { let user = req.extensions().find::().unwrap(); - let krate = Crate::find_or_insert(req.tx().unwrap(), krate.name.as_slice(), + let mut krate = Crate::find_or_insert(req.tx().unwrap(), krate.name.as_slice(), user.id, &krate.description, &krate.homepage, &krate.documentation, @@ -224,10 +231,8 @@ fn mock_crate(req: &mut Request, krate: Crate) -> Crate { &krate.license).unwrap(); Keyword::update_crate(req.tx().unwrap(), &krate, krate.keywords.as_slice()).unwrap(); - Version::insert(req.tx().unwrap(), krate.id, - &semver::Version::parse("1.0.0").unwrap(), - &HashMap::new(), &[]).unwrap(); - return krate; + let v = krate.add_version(req.tx().unwrap(), v, &HashMap::new(), &[]); + (krate, v) } fn mock_keyword(req: &mut Request, name: &str) -> Keyword { diff --git a/src/tests/krate.rs b/src/tests/krate.rs index 96515508921..3c0f446670e 100644 --- a/src/tests/krate.rs +++ b/src/tests/krate.rs @@ -32,6 +32,8 @@ struct CrateResponse { krate: EncodableCrate, versions: Vec } #[deriving(Decodable)] struct Deps { dependencies: Vec } #[deriving(Decodable)] +struct RevDeps { reverse_dependencies: Vec } +#[deriving(Decodable)] struct Downloads { version_downloads: Vec } #[test] @@ -504,7 +506,7 @@ fn dependencies() { let c1 = ::krate("foo"); let c2 = ::krate("bar"); middle.add(::middleware::MockUser(user.clone())); - middle.add(::middleware::MockDependency(c1.clone(), c2.clone())); + middle.add(::middleware::MockDependency(c1.clone(), "1.0.0", c2.clone())); let rel = format!("/api/v1/crates/{}/1.0.0/dependencies", c1.name); let mut req = MockRequest::new(conduit::Get, rel.as_slice()); let mut response = ok_resp!(middle.call(&mut req)); @@ -700,3 +702,32 @@ fn bad_keywords() { ::json::<::Bad>(&mut response); } } + +#[test] +fn reverse_dependencies() { + let (_b, _app, mut middle) = ::app(); + let user = ::user("foo"); + let c1 = ::krate("foo"); + let c2 = ::krate("bar"); + middle.add(::middleware::MockUser(user.clone())); + // multiple dependencies of c1 on c2, to detect we handle this + // correctly. + middle.add(::middleware::MockDependency(c1.clone(), "1.0.0", c2.clone())); + middle.add(::middleware::MockDependency(c1.clone(), "1.1.0", c2.clone())); + + let rel = format!("/api/v1/crates/{}/reverse_dependencies", c2.name); + let mut req = MockRequest::new(conduit::Get, rel.as_slice()); + let mut response = ok_resp!(middle.call(&mut req)); + let deps = ::json::(&mut response); + assert_eq!(deps.reverse_dependencies.len(), 1); + assert_eq!(deps.reverse_dependencies[0].crate_id.as_slice(), &*c1.name); + drop(req); + + // c1 has no dependent crates. + let rel = format!("/api/v1/crates/{}/reverse_dependencies", c1.name); + let mut req = MockRequest::new(conduit::Get, rel.as_slice()); + let mut response = ok_resp!(middle.call(&mut req)); + let deps = ::json::(&mut response); + assert_eq!(deps.reverse_dependencies.len(), 0); + drop(req); +} diff --git a/src/tests/middleware.rs b/src/tests/middleware.rs index 1ea0472b8a6..557189c49ec 100644 --- a/src/tests/middleware.rs +++ b/src/tests/middleware.rs @@ -28,15 +28,19 @@ impl Middleware for MockCrate { } } -pub struct MockDependency(pub Crate, pub Crate); +pub struct MockDependency(pub Crate, pub &'static str, pub Crate); impl Middleware for MockDependency { fn before(&self, req: &mut Request) -> Result<(), Box> { - let MockDependency(ref a, ref b) = *self; - let crate_a = ::mock_crate(req, a.clone()); - let crate_b = ::mock_crate(req, b.clone()); - let va = crate_a.versions(req.tx().unwrap()).unwrap()[0].id; - Dependency::insert(req.tx().unwrap(), va, crate_b.id, + let MockDependency(ref a, version, ref b) = *self; + let vers = semver::Version::parse(version).unwrap(); + let (_crate_a, va) = ::mock_crate_vers(req, a.clone(), &vers); + + // don't panic on duplicate uploads + let (crate_b, _) = ::mock_crate_vers(req, b.clone(), + &semver::Version::parse("1.0.0").unwrap()); + + Dependency::insert(req.tx().unwrap(), va.unwrap().id, crate_b.id, &semver::VersionReq::parse(">= 0").unwrap(), Kind::Normal, false, true, &[], &None).unwrap();