Skip to content

Commit 7d70f99

Browse files
committed
Add the ability to add a category to a crate while publishing
1 parent 5381423 commit 7d70f99

File tree

8 files changed

+220
-19
lines changed

8 files changed

+220
-19
lines changed

src/category.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
use std::collections::HashSet;
12
use time::Timespec;
23

34
use conduit::{Request, Response};
45
use conduit_router::RequestParams;
56
use pg::GenericConnection;
67
use pg::rows::Row;
78

8-
use Model;
9+
use {Model, Crate};
910
use db::RequestTransaction;
1011
use util::{RequestUtils, CargoResult, ChainError};
1112
use util::errors::NotFound;
@@ -35,6 +36,14 @@ impl Category {
3536
Ok(rows.iter().next().map(|r| Model::from_row(&r)))
3637
}
3738

39+
pub fn find_all_by_category(conn: &GenericConnection, names: &[String])
40+
-> CargoResult<Vec<Category>> {
41+
let stmt = try!(conn.prepare("SELECT * FROM categories \
42+
WHERE category = ANY($1)"));
43+
let rows = try!(stmt.query(&[&names]));
44+
Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
45+
}
46+
3847
pub fn encodable(self) -> EncodableCategory {
3948
let Category { id: _, crates_cnt, category, created_at } = self;
4049
EncodableCategory {
@@ -44,6 +53,52 @@ impl Category {
4453
category: category,
4554
}
4655
}
56+
57+
pub fn update_crate(conn: &GenericConnection,
58+
krate: &Crate,
59+
categories: &[String]) -> CargoResult<()> {
60+
let old_categories = try!(krate.categories(conn));
61+
let old_categories_ids: HashSet<_> = old_categories.iter().map(|cat| {
62+
cat.id
63+
}).collect();
64+
// If a new category specified is not in the database, filter
65+
// it out and don't add it.
66+
let new_categories = try!(
67+
Category::find_all_by_category(conn, categories)
68+
);
69+
let new_categories_ids: HashSet<_> = new_categories.iter().map(|cat| {
70+
cat.id
71+
}).collect();
72+
73+
let to_rm: Vec<_> = old_categories_ids
74+
.difference(&new_categories_ids)
75+
.cloned()
76+
.collect();
77+
let to_add: Vec<_> = new_categories_ids
78+
.difference(&old_categories_ids)
79+
.cloned()
80+
.collect();
81+
82+
if to_rm.len() > 0 {
83+
try!(conn.execute("DELETE FROM crates_categories \
84+
WHERE category_id = ANY($1) \
85+
AND crate_id = $2",
86+
&[&to_rm, &krate.id]));
87+
}
88+
89+
if !to_add.is_empty() {
90+
let insert: Vec<_> = to_add.into_iter().map(|id| {
91+
format!("({}, {})", krate.id, id)
92+
}).collect();
93+
let insert = insert.join(", ");
94+
try!(conn.execute(&format!("INSERT INTO crates_categories \
95+
(crate_id, category_id) VALUES {}",
96+
insert),
97+
&[]));
98+
}
99+
100+
Ok(())
101+
}
47102
}
48103

49104
impl Model for Category {

src/krate.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ use semver;
2020
use time::{Timespec, Duration};
2121
use url::Url;
2222

23-
use {Model, User, Keyword, Version};
23+
use {Model, User, Keyword, Version, Category};
2424
use app::{App, RequestApp};
2525
use db::RequestTransaction;
2626
use dependency::{Dependency, EncodableDependency};
2727
use download::{VersionDownload, EncodableVersionDownload};
2828
use git;
2929
use keyword::EncodableKeyword;
30+
use category::EncodableCategory;
3031
use upload;
3132
use user::RequestUser;
3233
use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights};
@@ -59,6 +60,7 @@ pub struct EncodableCrate {
5960
pub updated_at: String,
6061
pub versions: Option<Vec<i32>>,
6162
pub keywords: Option<Vec<String>>,
63+
pub categories: Option<Vec<String>>,
6264
pub created_at: String,
6365
pub downloads: i32,
6466
pub max_version: String,
@@ -229,7 +231,8 @@ impl Crate {
229231

230232
pub fn encodable(self,
231233
versions: Option<Vec<i32>>,
232-
keywords: Option<&[Keyword]>)
234+
keywords: Option<&[Keyword]>,
235+
categories: Option<&[Category]>)
233236
-> EncodableCrate {
234237
let Crate {
235238
name, created_at, updated_at, downloads, max_version, description,
@@ -241,6 +244,7 @@ impl Crate {
241244
None => Some(format!("/api/v1/crates/{}/versions", name)),
242245
};
243246
let keyword_ids = keywords.map(|kws| kws.iter().map(|kw| kw.keyword.clone()).collect());
247+
let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.category.clone()).collect());
244248
EncodableCrate {
245249
id: name.clone(),
246250
name: name.clone(),
@@ -249,6 +253,7 @@ impl Crate {
249253
downloads: downloads,
250254
versions: versions,
251255
keywords: keyword_ids,
256+
categories: category_ids,
252257
max_version: max_version.to_string(),
253258
documentation: documentation,
254259
homepage: homepage,
@@ -386,6 +391,16 @@ impl Crate {
386391
Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
387392
}
388393

394+
pub fn categories(&self, conn: &GenericConnection) -> CargoResult<Vec<Category>> {
395+
let stmt = try!(conn.prepare("SELECT categories.* FROM categories \
396+
LEFT JOIN crates_categories \
397+
ON categories.id = \
398+
crates_categories.category_id \
399+
WHERE crates_categories.crate_id = $1"));
400+
let rows = try!(stmt.query(&[&self.id]));
401+
Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
402+
}
403+
389404
/// Returns (dependency, dependent crate name)
390405
pub fn reverse_dependencies(&self,
391406
conn: &GenericConnection,
@@ -567,7 +582,7 @@ pub fn index(req: &mut Request) -> CargoResult<Response> {
567582
let mut crates = Vec::new();
568583
for row in try!(stmt.query(&args)).iter() {
569584
let krate: Crate = Model::from_row(&row);
570-
crates.push(krate.encodable(None, None));
585+
crates.push(krate.encodable(None, None, None));
571586
}
572587

573588
// Query for the total count of crates
@@ -602,7 +617,7 @@ pub fn summary(req: &mut Request) -> CargoResult<Response> {
602617
let rows = try!(stmt.query(&[]));
603618
Ok(rows.iter().map(|r| {
604619
let krate: Crate = Model::from_row(&r);
605-
krate.encodable(None, None)
620+
krate.encodable(None, None, None)
606621
}).collect::<Vec<EncodableCrate>>())
607622
};
608623
let new_crates = try!(tx.prepare("SELECT * FROM crates \
@@ -639,19 +654,22 @@ pub fn show(req: &mut Request) -> CargoResult<Response> {
639654
let versions = try!(krate.versions(conn));
640655
let ids = versions.iter().map(|v| v.id).collect();
641656
let kws = try!(krate.keywords(conn));
657+
let cats = try!(krate.categories(conn));
642658

643659
#[derive(RustcEncodable)]
644660
struct R {
645661
krate: EncodableCrate,
646662
versions: Vec<EncodableVersion>,
647663
keywords: Vec<EncodableKeyword>,
664+
categories: Vec<EncodableCategory>,
648665
}
649666
Ok(req.json(&R {
650-
krate: krate.clone().encodable(Some(ids), Some(&kws)),
667+
krate: krate.clone().encodable(Some(ids), Some(&kws), Some(&cats)),
651668
versions: versions.into_iter().map(|v| {
652669
v.encodable(&krate.name)
653670
}).collect(),
654671
keywords: kws.into_iter().map(|k| k.encodable()).collect(),
672+
categories: cats.into_iter().map(|k| k.encodable()).collect(),
655673
}))
656674
}
657675

@@ -669,6 +687,10 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
669687
.unwrap_or(&[]);
670688
let keywords = keywords.iter().map(|k| k[..].to_string()).collect::<Vec<_>>();
671689

690+
let categories = new_crate.categories.as_ref().map(|s| &s[..])
691+
.unwrap_or(&[]);
692+
let categories = categories.iter().map(|k| k[..].to_string()).collect::<Vec<_>>();
693+
672694
// Persist the new crate, if it doesn't already exist
673695
let mut krate = try!(Crate::find_or_insert(try!(req.tx()), name, user.id,
674696
&new_crate.description,
@@ -713,6 +735,9 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
713735
// Update all keywords for this crate
714736
try!(Keyword::update_crate(try!(req.tx()), &krate, &keywords));
715737

738+
// Update all categories for this crate
739+
try!(Category::update_crate(try!(req.tx()), &krate, &categories));
740+
716741
// Upload the crate to S3
717742
let mut handle = req.app().handle();
718743
let path = krate.s3_path(&vers.to_string());
@@ -769,7 +794,7 @@ pub fn new(req: &mut Request) -> CargoResult<Response> {
769794

770795
#[derive(RustcEncodable)]
771796
struct R { krate: EncodableCrate }
772-
Ok(req.json(&R { krate: krate.encodable(None, None) }))
797+
Ok(req.json(&R { krate: krate.encodable(None, None, None) }))
773798
}
774799

775800
fn parse_new_headers(req: &mut Request) -> CargoResult<(upload::NewCrate, User)> {

src/tests/all.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,20 +272,21 @@ fn new_req(app: Arc<App>, krate: &str, version: &str) -> MockRequest {
272272
fn new_req_full(app: Arc<App>, krate: Crate, version: &str,
273273
deps: Vec<u::CrateDependency>) -> MockRequest {
274274
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
275-
req.with_body(&new_req_body(krate, version, deps, Vec::new()));
275+
req.with_body(&new_req_body(krate, version, deps, Vec::new(), Vec::new()));
276276
return req;
277277
}
278278

279279
fn new_req_with_keywords(app: Arc<App>, krate: Crate, version: &str,
280280
kws: Vec<String>) -> MockRequest {
281281
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
282-
req.with_body(&new_req_body(krate, version, Vec::new(), kws));
282+
req.with_body(&new_req_body(krate, version, Vec::new(), kws, Vec::new()));
283283
return req;
284284
}
285285

286286
fn new_req_body(krate: Crate, version: &str, deps: Vec<u::CrateDependency>,
287-
kws: Vec<String>) -> Vec<u8> {
287+
kws: Vec<String>, cats: Vec<String>) -> Vec<u8> {
288288
let kws = kws.into_iter().map(u::Keyword).collect();
289+
let cats = cats.into_iter().map(u::Category).collect();
289290
new_crate_to_body(&u::NewCrate {
290291
name: u::CrateName(krate.name),
291292
vers: u::CrateVersion(semver::Version::parse(version).unwrap()),
@@ -297,6 +298,7 @@ fn new_req_body(krate: Crate, version: &str, deps: Vec<u::CrateDependency>,
297298
documentation: krate.documentation,
298299
readme: krate.readme,
299300
keywords: Some(u::KeywordList(kws)),
301+
categories: Some(u::CategoryList(cats)),
300302
license: Some("MIT".to_string()),
301303
license_file: None,
302304
repository: krate.repository,

src/tests/category.rs

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
use conduit::{Handler, Method};
1+
use postgres::GenericConnection;
2+
use conduit::{Handler, Request, Method};
3+
use conduit_test::MockRequest;
24

3-
use cargo_registry::category::EncodableCategory;
5+
use cargo_registry::db::RequestTransaction;
6+
use cargo_registry::category::{Category, EncodableCategory};
47

58
#[derive(RustcDecodable)]
69
struct CategoryList { categories: Vec<EncodableCategory>, meta: CategoryMeta }
@@ -38,3 +41,66 @@ fn show() {
3841
let json: GoodCategory = ::json(&mut response);
3942
assert_eq!(json.category.category, "foo");
4043
}
44+
45+
fn tx(req: &Request) -> &GenericConnection { req.tx().unwrap() }
46+
47+
#[test]
48+
fn update_crate() {
49+
let (_b, app, middle) = ::app();
50+
let mut req = ::req(app, Method::Get, "/api/v1/categories/foo");
51+
let cnt = |req: &mut MockRequest, cat: &str| {
52+
req.with_path(&format!("/api/v1/categories/{}", cat));
53+
let mut response = ok_resp!(middle.call(req));
54+
::json::<GoodCategory>(&mut response).category.crates_cnt as usize
55+
};
56+
::mock_user(&mut req, ::user("foo"));
57+
let (krate, _) = ::mock_crate(&mut req, ::krate("foo"));
58+
::mock_category(&mut req, "cat1");
59+
::mock_category(&mut req, "cat2");
60+
61+
// Updating with no categories has no effect
62+
Category::update_crate(tx(&req), &krate, &[]).unwrap();
63+
assert_eq!(cnt(&mut req, "cat1"), 0);
64+
assert_eq!(cnt(&mut req, "cat2"), 0);
65+
66+
// Happy path adding one category
67+
Category::update_crate(tx(&req), &krate, &["cat1".to_string()]).unwrap();
68+
assert_eq!(cnt(&mut req, "cat1"), 1);
69+
assert_eq!(cnt(&mut req, "cat2"), 0);
70+
71+
// Replacing one category with another
72+
Category::update_crate(tx(&req), &krate, &["cat2".to_string()]).unwrap();
73+
assert_eq!(cnt(&mut req, "cat1"), 0);
74+
assert_eq!(cnt(&mut req, "cat2"), 1);
75+
76+
// Removing one category
77+
Category::update_crate(tx(&req), &krate, &[]).unwrap();
78+
assert_eq!(cnt(&mut req, "cat1"), 0);
79+
assert_eq!(cnt(&mut req, "cat2"), 0);
80+
81+
// Adding 2 categories
82+
Category::update_crate(tx(&req), &krate, &["cat1".to_string(),
83+
"cat2".to_string()]).unwrap();
84+
assert_eq!(cnt(&mut req, "cat1"), 1);
85+
assert_eq!(cnt(&mut req, "cat2"), 1);
86+
87+
// Removing all categories
88+
Category::update_crate(tx(&req), &krate, &[]).unwrap();
89+
assert_eq!(cnt(&mut req, "cat1"), 0);
90+
assert_eq!(cnt(&mut req, "cat2"), 0);
91+
92+
// Attempting to add one valid category and one invalid category
93+
Category::update_crate(tx(&req), &krate, &["cat1".to_string(),
94+
"catnope".to_string()]).unwrap();
95+
96+
assert_eq!(cnt(&mut req, "cat1"), 1);
97+
assert_eq!(cnt(&mut req, "cat2"), 0);
98+
99+
// Does not add the invalid category to the category list
100+
// (unlike the behavior of keywords)
101+
req.with_path("/api/v1/categories");
102+
let mut response = ok_resp!(middle.call(&mut req));
103+
let json: CategoryList = ::json(&mut response);
104+
assert_eq!(json.categories.len(), 2);
105+
assert_eq!(json.meta.total, 2);
106+
}

src/tests/krate.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ fn new_crate(name: &str) -> u::NewCrate {
4848
documentation: None,
4949
readme: None,
5050
keywords: None,
51+
categories: None,
5152
license: Some("MIT".to_string()),
5253
license_file: None,
5354
repository: None,
@@ -190,7 +191,7 @@ fn exact_match_on_queries_with_sort() {
190191
krate4.description = Some("other const".to_string());
191192
krate4.downloads = 999999;
192193
let (k4, _) = ::mock_crate(&mut req, krate4.clone());
193-
194+
194195
{
195196
let req2: &mut Request = &mut req;
196197
let tx = req2.tx().unwrap();
@@ -461,7 +462,9 @@ fn new_crate_owner() {
461462
assert_eq!(::json::<CrateList>(&mut response).crates.len(), 1);
462463

463464
// And upload a new crate as the first user
464-
let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
465+
let body = ::new_req_body(
466+
::krate("foo"), "2.0.0", Vec::new(), Vec::new(), Vec::new()
467+
);
465468
req.mut_extensions().insert(u2);
466469
let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new")
467470
.with_method(Method::Put)

0 commit comments

Comments
 (0)