diff --git a/Cargo.lock b/Cargo.lock
index d1145af66e..c4676689ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32,6 +32,7 @@ dependencies = [
"s3 0.0.1",
"semver 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)",
+ "toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -609,6 +610,14 @@ dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "toml"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc-serialize 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "unicode-bidi"
version = "0.2.3"
@@ -722,6 +731,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03"
"checksum thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8576dbbfcaef9641452d5cf0df9b0e7eeab7694956dd33bb61515fb8f18cfdd5"
"checksum time 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "3c7ec6d62a20df54e07ab3b78b9a3932972f4b7981de295563686849eb3989af"
+"checksum toml 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "736b60249cb25337bc196faa43ee12c705e426f3d55c214d73a4e7be06f92cb4"
"checksum unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c1f7ceb96afdfeedee42bade65a0d585a6a0106f681b6749c8ff4daa8df30b3f"
"checksum unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "26643a2f83bac55f1976fb716c10234485f9202dcd65cfbdf9da49867b271172"
"checksum url 1.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f024e241a55f5c88401595adc1d4af0c9649e91da82d0e190fe55950231ae575"
diff --git a/Cargo.toml b/Cargo.toml
index 5ffbf6da6b..9cc60cc91a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,7 @@ env_logger = "0.3"
rustc-serialize = "0.3"
license-exprs = "^1.3"
dotenv = "0.8.0"
+toml = "0.2"
conduit = "0.8"
conduit-conditional-get = "0.8"
diff --git a/README.md b/README.md
index 8745e1a2c6..947cdf5002 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,21 @@ follows:
```
cargo test
```
+
+## Categories
+
+The list of categories available on crates.io is stored in
+`src/categories.toml`. To propose adding, removing, or changing a category,
+send a pull request making the appropriate change to that file as noted in the
+comment at the top of the file. Please add a description that will help others
+to know what crates are in that category.
+
+For new categories, it's helpful to note in your PR description examples of
+crates that would fit in that category, and describe what distinguishes the new
+category from existing categories.
+
+After your PR is accepted, the next time that crates.io is deployed the
+categories will be synced from this file.
## Deploying & Using a Mirror
diff --git a/app/adapters/category-slug.js b/app/adapters/category-slug.js
new file mode 100644
index 0000000000..109a644709
--- /dev/null
+++ b/app/adapters/category-slug.js
@@ -0,0 +1,11 @@
+import ApplicationAdapter from './application';
+import Ember from 'ember';
+
+export default ApplicationAdapter.extend({
+ pathForType(modelName) {
+ var decamelized = Ember.String.underscore(
+ Ember.String.decamelize(modelName)
+ );
+ return Ember.String.pluralize(decamelized);
+ }
+});
diff --git a/app/controllers/categories.js b/app/controllers/categories.js
new file mode 100644
index 0000000000..5cfc81b0f8
--- /dev/null
+++ b/app/controllers/categories.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+import PaginationMixin from '../mixins/pagination';
+
+const { computed } = Ember;
+
+export default Ember.Controller.extend(PaginationMixin, {
+ queryParams: ['page', 'per_page', 'sort'],
+ page: '1',
+ per_page: 10,
+ sort: 'alpha',
+
+ totalItems: computed.readOnly('model.meta.total'),
+
+ currentSortBy: computed('sort', function() {
+ return (this.get('sort') === 'crates') ? '# Crates' : 'Alphabetical';
+ }),
+});
diff --git a/app/controllers/category/index.js b/app/controllers/category/index.js
new file mode 100644
index 0000000000..bbc85947a7
--- /dev/null
+++ b/app/controllers/category/index.js
@@ -0,0 +1,17 @@
+import Ember from 'ember';
+import PaginationMixin from '../../mixins/pagination';
+
+const { computed } = Ember;
+
+export default Ember.Controller.extend(PaginationMixin, {
+ queryParams: ['page', 'per_page', 'sort'],
+ page: '1',
+ per_page: 10,
+ sort: 'downloads',
+
+ totalItems: computed.readOnly('model.meta.total'),
+
+ currentSortBy: computed('sort', function() {
+ return (this.get('sort') === 'downloads') ? 'Downloads' : 'Alphabetical';
+ }),
+});
diff --git a/app/controllers/crate/version.js b/app/controllers/crate/version.js
index ad0c5836a8..99216589ed 100644
--- a/app/controllers/crate/version.js
+++ b/app/controllers/crate/version.js
@@ -19,6 +19,7 @@ export default Ember.Controller.extend({
currentVersion: computed.alias('model'),
requestedVersion: null,
keywords: computed.alias('crate.keywords'),
+ categories: computed.alias('crate.categories'),
sortedVersions: computed.readOnly('crate.versions'),
@@ -49,6 +50,7 @@ export default Ember.Controller.extend({
}),
anyKeywords: computed.gt('keywords.length', 0),
+ anyCategories: computed.gt('categories.length', 0),
currentDependencies: computed('currentVersion.dependencies', function() {
var deps = this.get('currentVersion.dependencies');
diff --git a/app/models/category-slug.js b/app/models/category-slug.js
new file mode 100644
index 0000000000..167c436b97
--- /dev/null
+++ b/app/models/category-slug.js
@@ -0,0 +1,5 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+ slug: DS.attr('string'),
+});
\ No newline at end of file
diff --git a/app/models/category.js b/app/models/category.js
new file mode 100644
index 0000000000..3be263e0b1
--- /dev/null
+++ b/app/models/category.js
@@ -0,0 +1,13 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+ category: DS.attr('string'),
+ slug: DS.attr('string'),
+ description: DS.attr('string'),
+ created_at: DS.attr('date'),
+ crates_cnt: DS.attr('number'),
+
+ subcategories: DS.attr(),
+
+ crates: DS.hasMany('crate', { async: true })
+});
diff --git a/app/models/crate.js b/app/models/crate.js
index 73061e89a1..9065b323f7 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -20,6 +20,7 @@ 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 }),
+ categories: DS.hasMany('categories', { async: true }),
reverse_dependencies: DS.hasMany('dependency', { async: true }),
follow() {
diff --git a/app/router.js b/app/router.js
index 8e48c6c8ca..ee35611828 100644
--- a/app/router.js
+++ b/app/router.js
@@ -35,6 +35,11 @@ Router.map(function() {
this.route('keyword', { path: '/keywords/:keyword_id' }, function() {
this.route('index', { path: '/' });
});
+ this.route('categories');
+ this.route('category', { path: '/categories/:category_id' }, function() {
+ this.route('index', { path: '/' });
+ });
+ this.route('category_slugs');
this.route('catchAll', { path: '*path' });
});
diff --git a/app/routes/categories.js b/app/routes/categories.js
new file mode 100644
index 0000000000..a3b07940be
--- /dev/null
+++ b/app/routes/categories.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: { refreshModel: true },
+ sort: { refreshModel: true },
+ },
+
+ model(params) {
+ return this.store.query('category', params);
+ },
+});
diff --git a/app/routes/category-slugs.js b/app/routes/category-slugs.js
new file mode 100644
index 0000000000..8416e4d23d
--- /dev/null
+++ b/app/routes/category-slugs.js
@@ -0,0 +1,12 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: { refreshModel: true },
+ sort: { refreshModel: true },
+ },
+
+ model(params) {
+ return this.store.query('category-slug', params);
+ },
+});
diff --git a/app/routes/category.js b/app/routes/category.js
new file mode 100644
index 0000000000..a9ca79343d
--- /dev/null
+++ b/app/routes/category.js
@@ -0,0 +1,11 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ return this.store.find('category', params.category_id).catch(e => {
+ if (e.errors.any(e => e.detail === 'Not Found')) {
+ this.controllerFor('application').set('flashError', `Category '${params.category_id}' does not exist`);
+ }
+ });
+ }
+});
diff --git a/app/routes/category/index.js b/app/routes/category/index.js
new file mode 100644
index 0000000000..fd16edd91d
--- /dev/null
+++ b/app/routes/category/index.js
@@ -0,0 +1,18 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ queryParams: {
+ page: { refreshModel: true },
+ sort: { refreshModel: true },
+ },
+
+ model(params) {
+ params.category = this.modelFor('category').id;
+ return this.store.query('crate', params);
+ },
+
+ setupController(controller, model) {
+ controller.set('category', this.modelFor('category'));
+ this._super(controller, model);
+ },
+});
diff --git a/app/templates/categories.hbs b/app/templates/categories.hbs
new file mode 100644
index 0000000000..16689077a8
--- /dev/null
+++ b/app/templates/categories.hbs
@@ -0,0 +1,73 @@
+{{ title 'Categories' }}
+
+
+

+
All Categories
+
+
+
+
+
+ Displaying
+ {{ currentPageStart }}-{{ currentPageEnd }}
+ of {{ totalItems }} total results
+
+
+
+
+
Sort by
+ {{#rl-dropdown-container class="dropdown-container"}}
+ {{#rl-dropdown-toggle tagName="a" class="dropdown"}}
+

+ {{ currentSortBy }}
+
+ {{/rl-dropdown-toggle}}
+
+ {{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}}
+
+ {{#link-to (query-params sort="alpha")}}
+ Alphabetical
+ {{/link-to}}
+
+
+ {{#link-to (query-params sort="crates")}}
+ # Crates
+ {{/link-to}}
+
+ {{/rl-dropdown}}
+ {{/rl-dropdown-container}}
+
+
+
+
+ {{#each model as |category|}}
+
+
+
+ {{link-to category.category "category" category.slug}}
+
+ {{ format-num category.crates_cnt }}
+ crates
+
+
+
+
+ {{ truncate-text category.description }}
+
+
+
+
+ {{/each}}
+
+
+
diff --git a/app/templates/category/error.hbs b/app/templates/category/error.hbs
new file mode 100644
index 0000000000..d3e3b5bbcc
--- /dev/null
+++ b/app/templates/category/error.hbs
@@ -0,0 +1 @@
+{{ title 'Category Not Found' }}
\ No newline at end of file
diff --git a/app/templates/category/index.hbs b/app/templates/category/index.hbs
new file mode 100644
index 0000000000..6bd0f2812d
--- /dev/null
+++ b/app/templates/category/index.hbs
@@ -0,0 +1,85 @@
+{{ title category.category ' - Categories' }}
+
+
+

+
{{ category.category }}
+
+
+{{#if category.subcategories }}
+
+
Subcategories
+
+ {{#each category.subcategories as |subcategory| }}
+
+
+
+ {{link-to subcategory.category "category" subcategory.slug}}
+
+ {{ format-num subcategory.crates_cnt }}
+ crates
+
+
+
+
+ {{ truncate-text subcategory.description }}
+
+
+
+
+ {{/each}}
+
+
+{{/if}}
+
+Crates
+
+
+
+ Displaying
+ {{ currentPageStart }}-{{ currentPageEnd }}
+ of {{ totalItems }} total results
+
+
+
+
+
Sort by
+ {{#rl-dropdown-container class="dropdown-container"}}
+ {{#rl-dropdown-toggle tagName="a" class="dropdown"}}
+

+ {{ currentSortBy }}
+
+ {{/rl-dropdown-toggle}}
+
+ {{#rl-dropdown tagName="ul" class="dropdown" closeOnChildClick="a:link"}}
+
+ {{#link-to (query-params sort="alpha")}}
+ Alphabetical
+ {{/link-to}}
+
+
+ {{#link-to (query-params sort="downloads")}}
+ Downloads
+ {{/link-to}}
+
+ {{/rl-dropdown}}
+ {{/rl-dropdown-container}}
+
+
+
+
+ {{#each model as |crate|}}
+ {{crate-row crate=crate}}
+ {{/each}}
+
+
+
diff --git a/app/templates/category_slugs.hbs b/app/templates/category_slugs.hbs
new file mode 100644
index 0000000000..6e36210b1c
--- /dev/null
+++ b/app/templates/category_slugs.hbs
@@ -0,0 +1,14 @@
+{{ title 'Category Slugs' }}
+
+
+

+
All Valid Category Slugs
+
+
+
+
+ {{#each model as |slug|}}
+ - {{slug.slug}}
+ {{/each}}
+
+
\ No newline at end of file
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index 42356ec38e..419f52d5a8 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -104,6 +104,19 @@
{{/if}}
{{/unless}}
+ {{#unless crate.categories.isPending}}
+ {{#if anyCategories}}
+
+
Categories
+
+ {{#each categories as |category|}}
+ - {{link-to category.category 'category' category.slug}}
+ {{/each}}
+
+
+ {{/if}}
+ {{/unless}}
+
Owners
diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs
index 89a62b5289..8acd06e791 100644
--- a/src/bin/migrate.rs
+++ b/src/bin/migrate.rs
@@ -764,6 +764,63 @@ fn migrations() -> Vec {
try!(tx.execute("DROP INDEX users_gh_id", &[]));
Ok(())
}),
+ Migration::add_table(20161115110541, "categories", " \
+ id SERIAL PRIMARY KEY, \
+ category VARCHAR NOT NULL UNIQUE, \
+ slug VARCHAR NOT NULL UNIQUE, \
+ description VARCHAR NOT NULL DEFAULT '', \
+ crates_cnt INTEGER NOT NULL DEFAULT 0, \
+ created_at TIMESTAMP NOT NULL DEFAULT current_timestamp"),
+ Migration::add_table(20161115111828, "crates_categories", " \
+ crate_id INTEGER NOT NULL, \
+ category_id INTEGER NOT NULL"),
+ foreign_key(20161115111836, "crates_categories", "crate_id", "crates (id)"),
+ Migration::run(20161115111846, " \
+ ALTER TABLE crates_categories \
+ ADD CONSTRAINT fk_crates_categories_category_id \
+ FOREIGN KEY (category_id) REFERENCES categories (id) \
+ ON DELETE CASCADE", " \
+ ALTER TABLE crates_categories \
+ DROP CONSTRAINT fk_crates_categories_category_id"),
+ index(20161115111853, "crates_categories", "crate_id"),
+ index(20161115111900, "crates_categories", "category_id"),
+ Migration::new(20161115111957, |tx| {
+ try!(tx.batch_execute(" \
+ CREATE FUNCTION update_categories_crates_cnt() \
+ RETURNS trigger AS $$ \
+ BEGIN \
+ IF (TG_OP = 'INSERT') THEN \
+ UPDATE categories \
+ SET crates_cnt = crates_cnt + 1 \
+ WHERE id = NEW.category_id; \
+ return NEW; \
+ ELSIF (TG_OP = 'DELETE') THEN \
+ UPDATE categories \
+ SET crates_cnt = crates_cnt - 1 \
+ WHERE id = OLD.category_id; \
+ return OLD; \
+ END IF; \
+ END \
+ $$ LANGUAGE plpgsql; \
+ CREATE TRIGGER trigger_update_categories_crates_cnt \
+ BEFORE INSERT OR DELETE \
+ ON crates_categories \
+ FOR EACH ROW EXECUTE PROCEDURE update_categories_crates_cnt(); \
+ CREATE TRIGGER touch_crate_on_modify_categories \
+ AFTER INSERT OR DELETE ON crates_categories \
+ FOR EACH ROW \
+ EXECUTE PROCEDURE touch_crate(); \
+ "));
+ Ok(())
+ }, |tx| {
+ try!(tx.batch_execute(" \
+ DROP TRIGGER trigger_update_categories_crates_cnt \
+ ON crates_categories; \
+ DROP FUNCTION update_categories_crates_cnt(); \
+ DROP TRIGGER touch_crate_on_modify_categories \
+ ON crates_categories;"));
+ Ok(())
+ }),
];
// NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`
diff --git a/src/bin/server.rs b/src/bin/server.rs
index bf0749d65e..1af0ca68c8 100644
--- a/src/bin/server.rs
+++ b/src/bin/server.rs
@@ -62,6 +62,8 @@ fn main() {
let app = cargo_registry::App::new(&config);
let app = cargo_registry::middleware(Arc::new(app));
+ cargo_registry::categories::sync().unwrap();
+
let port = if heroku {
8888
} else {
diff --git a/src/categories.rs b/src/categories.rs
new file mode 100644
index 0000000000..40b9b4ac85
--- /dev/null
+++ b/src/categories.rs
@@ -0,0 +1,120 @@
+// Sync available crate categories from `src/categories.toml`.
+// Runs when the server is started.
+
+use toml;
+use pg;
+use env;
+use util::errors::{CargoResult, ChainError, internal};
+
+struct Category {
+ slug: String,
+ name: String,
+ description: String,
+}
+
+impl Category {
+ fn from_parent(slug: &str, name: &str, description: &str, parent: Option<&Category>)
+ -> Category {
+ match parent {
+ Some(parent) => {
+ Category {
+ slug: format!("{}::{}", parent.slug, slug),
+ name: format!("{}::{}", parent.name, name),
+ description: description.into(),
+ }
+ }
+ None => {
+ Category {
+ slug: slug.into(),
+ name: name.into(),
+ description: description.into(),
+ }
+ }
+ }
+ }
+}
+
+fn required_string_from_toml<'a>(toml: &'a toml::Table, key: &str) -> CargoResult<&'a str> {
+ toml.get(key)
+ .and_then(toml::Value::as_str)
+ .chain_error(|| {
+ internal(format!("Expected category TOML attribute '{}' to be a String", key))
+ })
+}
+
+fn optional_string_from_toml<'a>(toml: &'a toml::Table, key: &str) -> &'a str {
+ toml.get(key)
+ .and_then(toml::Value::as_str)
+ .unwrap_or("")
+}
+
+fn categories_from_toml(categories: &toml::Table, parent: Option<&Category>) -> CargoResult> {
+ let mut result = vec![];
+
+ for (slug, details) in categories {
+ let details = details.as_table().chain_error(|| {
+ internal(format!("category {} was not a TOML table", slug))
+ })?;
+
+ let category = Category::from_parent(
+ slug,
+ required_string_from_toml(&details, "name")?,
+ optional_string_from_toml(&details, "description"),
+ parent,
+ );
+
+ if let Some(categories) = details.get("categories") {
+ let categories = categories.as_table().chain_error(|| {
+ internal(format!("child categories of {} were not a table", slug))
+ })?;
+
+ result.extend(
+ categories_from_toml(categories, Some(&category))?
+ );
+ }
+
+ result.push(category)
+ }
+
+ Ok(result)
+}
+
+pub fn sync() -> CargoResult<()> {
+ let conn = pg::Connection::connect(&env("DATABASE_URL")[..],
+ pg::TlsMode::None).unwrap();
+ let tx = conn.transaction().unwrap();
+
+ let categories = include_str!("./categories.toml");
+ let toml = toml::Parser::new(categories).parse().expect(
+ "Could not parse categories.toml"
+ );
+
+ let categories = categories_from_toml(&toml, None).expect(
+ "Could not convert categories from TOML"
+ );
+
+ for category in categories.iter() {
+ tx.execute("\
+ INSERT INTO categories (slug, category, description) \
+ VALUES (LOWER($1), $2, $3) \
+ ON CONFLICT (slug) DO UPDATE \
+ SET category = EXCLUDED.category, \
+ description = EXCLUDED.description;",
+ &[&category.slug, &category.name, &category.description]
+ )?;
+ }
+
+ let in_clause = categories.iter().map(|ref category| {
+ format!("LOWER('{}')", category.slug)
+ }).collect::>().join(",");
+
+ tx.execute(&format!("\
+ DELETE FROM categories \
+ WHERE slug NOT IN ({});",
+ in_clause),
+ &[]
+ )?;
+ tx.set_commit();
+ tx.finish().unwrap();
+ Ok(())
+}
diff --git a/src/categories.toml b/src/categories.toml
new file mode 100644
index 0000000000..2fe84b57df
--- /dev/null
+++ b/src/categories.toml
@@ -0,0 +1,55 @@
+# This is where the categories available on crates.io are defined. To propose
+# a change to the categories, send a pull request with your change made to this
+# file.
+#
+# For help with TOML, see: https://github.com/toml-lang/toml
+#
+# Format:
+#
+# ```toml
+# [slug]
+# name = "Display name"
+# description = "Give an idea of the crates that belong in this category."
+#
+# [slug.categories.subcategory-slug]
+# name = "Subcategory display name, not including parent category display name"
+# description = "Give an idea of the crates that belong in this subcategory."
+# ```
+#
+# Notes:
+# - Slugs are the primary identifier. If you make a change to a category's slug,
+# crates that have been published with that slug will need to be updated to
+# use the new slug in order to stay in that category. If you only change
+# names and descriptions, those attributes CAN be updated without affecting
+# crates in that category.
+# - Slugs are used in the path of URLs, so they should not contain spaces, `/`,
+# `@`, `:`, or `.`. They should be all lowercase.
+#
+
+[development-tools]
+name = "Development Tools"
+description = "Ways to make developing in Rust better"
+
+ [development-tools.categories.testing]
+ name = "Testing"
+ description = "Additions to automated testing features"
+
+ [development-tools.categories.testing.categories.mocking]
+ name = "Mocking"
+ description = "Mocks are not the same as stubs!"
+
+[libraries]
+name = "Libraries"
+description = "Libraries are reusable pieces of code"
+
+ [libraries.categories.async]
+ name = "Async"
+ description = "Code that can take time to run but won't block"
+
+ [libraries.categories.date-and-time]
+ name = "Date and Time"
+ description = "Date and time math"
+
+[games]
+name = "Games"
+description = "Share fun things"
diff --git a/src/category.rs b/src/category.rs
new file mode 100644
index 0000000000..97cbde9ed4
--- /dev/null
+++ b/src/category.rs
@@ -0,0 +1,271 @@
+use std::collections::HashSet;
+use time::Timespec;
+
+use conduit::{Request, Response};
+use conduit_router::RequestParams;
+use pg::GenericConnection;
+use pg::rows::Row;
+
+use {Model, Crate};
+use db::RequestTransaction;
+use util::{RequestUtils, CargoResult, ChainError};
+use util::errors::NotFound;
+
+#[derive(Clone)]
+pub struct Category {
+ pub id: i32,
+ pub category: String,
+ pub slug: String,
+ pub description: String,
+ pub created_at: Timespec,
+ pub crates_cnt: i32,
+}
+
+#[derive(RustcEncodable, RustcDecodable)]
+pub struct EncodableCategory {
+ pub id: String,
+ pub category: String,
+ pub slug: String,
+ pub description: String,
+ pub created_at: String,
+ pub crates_cnt: i32,
+}
+
+#[derive(RustcEncodable, RustcDecodable)]
+pub struct EncodableCategoryWithSubcategories {
+ pub id: String,
+ pub category: String,
+ pub slug: String,
+ pub description: String,
+ pub created_at: String,
+ pub crates_cnt: i32,
+ pub subcategories: Vec,
+}
+
+impl Category {
+ pub fn find_by_category(conn: &GenericConnection, name: &str)
+ -> CargoResult {
+ let stmt = try!(conn.prepare("SELECT * FROM categories \
+ WHERE category = $1"));
+ let rows = try!(stmt.query(&[&name]));
+ rows.iter().next()
+ .chain_error(|| NotFound)
+ .map(|row| Model::from_row(&row))
+ }
+
+ pub fn find_by_slug(conn: &GenericConnection, slug: &str)
+ -> CargoResult {
+ let stmt = try!(conn.prepare("SELECT * FROM categories \
+ WHERE slug = LOWER($1)"));
+ let rows = try!(stmt.query(&[&slug]));
+ rows.iter().next()
+ .chain_error(|| NotFound)
+ .map(|row| Model::from_row(&row))
+ }
+
+ pub fn encodable(self) -> EncodableCategory {
+ let Category {
+ id: _, crates_cnt, category, slug, description, created_at
+ } = self;
+ EncodableCategory {
+ id: slug.clone(),
+ slug: slug.clone(),
+ description: description.clone(),
+ created_at: ::encode_time(created_at),
+ crates_cnt: crates_cnt,
+ category: category,
+ }
+ }
+
+ pub fn update_crate(conn: &GenericConnection,
+ krate: &Crate,
+ categories: &[String]) -> CargoResult> {
+ let old_categories = try!(krate.categories(conn));
+ let old_categories_ids: HashSet<_> = old_categories.iter().map(|cat| {
+ cat.id
+ }).collect();
+
+ // If a new category specified is not in the database, filter
+ // it out and don't add it. Return it to be able to warn about it.
+ let mut invalid_categories = vec![];
+ let new_categories: Vec = categories.iter().flat_map(|c| {
+ match Category::find_by_slug(conn, &c) {
+ Ok(cat) => Some(cat),
+ Err(_) => {
+ invalid_categories.push(c.to_string());
+ None
+ },
+ }
+ }).collect();
+
+ let new_categories_ids: HashSet<_> = new_categories.iter().map(|cat| {
+ cat.id
+ }).collect();
+
+ let to_rm: Vec<_> = old_categories_ids
+ .difference(&new_categories_ids)
+ .cloned()
+ .collect();
+ let to_add: Vec<_> = new_categories_ids
+ .difference(&old_categories_ids)
+ .cloned()
+ .collect();
+
+ if !to_rm.is_empty() {
+ try!(conn.execute("DELETE FROM crates_categories \
+ WHERE category_id = ANY($1) \
+ AND crate_id = $2",
+ &[&to_rm, &krate.id]));
+ }
+
+ if !to_add.is_empty() {
+ let insert: Vec<_> = to_add.into_iter().map(|id| {
+ format!("({}, {})", krate.id, id)
+ }).collect();
+ let insert = insert.join(", ");
+ try!(conn.execute(&format!("INSERT INTO crates_categories \
+ (crate_id, category_id) VALUES {}",
+ insert),
+ &[]));
+ }
+
+ Ok(invalid_categories)
+ }
+
+ pub fn count_toplevel(conn: &GenericConnection) -> CargoResult {
+ let sql = format!("\
+ SELECT COUNT(*) \
+ FROM {} \
+ WHERE category NOT LIKE '%::%'",
+ Model::table_name(None::
+ ));
+ let stmt = try!(conn.prepare(&sql));
+ let rows = try!(stmt.query(&[]));
+ Ok(rows.iter().next().unwrap().get("count"))
+ }
+
+ pub fn subcategories(&self, conn: &GenericConnection)
+ -> CargoResult> {
+ let stmt = try!(conn.prepare("\
+ SELECT c.id, c.category, c.slug, c.description, c.created_at, \
+ COALESCE (( \
+ SELECT sum(c2.crates_cnt)::int \
+ FROM categories as c2 \
+ WHERE c2.slug = c.slug \
+ OR c2.slug LIKE c.slug || '::%' \
+ ), 0) as crates_cnt \
+ FROM categories as c \
+ WHERE c.category ILIKE $1 || '::%' \
+ AND c.category NOT ILIKE $1 || '::%::%'"));
+
+ let rows = try!(stmt.query(&[&self.category]));
+ Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
+ }
+}
+
+impl Model for Category {
+ fn from_row(row: &Row) -> Category {
+ Category {
+ id: row.get("id"),
+ created_at: row.get("created_at"),
+ crates_cnt: row.get("crates_cnt"),
+ category: row.get("category"),
+ slug: row.get("slug"),
+ description: row.get("description"),
+ }
+ }
+ fn table_name(_: Option) -> &'static str { "categories" }
+}
+
+/// Handles the `GET /categories` route.
+pub fn index(req: &mut Request) -> CargoResult {
+ let conn = try!(req.tx());
+ let (offset, limit) = try!(req.pagination(10, 100));
+ let query = req.query();
+ let sort = query.get("sort").map_or("alpha", String::as_str);
+ let sort_sql = match sort {
+ "crates" => "ORDER BY crates_cnt DESC",
+ _ => "ORDER BY category ASC",
+ };
+
+ // Collect all the top-level categories and sum up the crates_cnt of
+ // the crates in all subcategories
+ let stmt = try!(conn.prepare(&format!(
+ "SELECT c.id, c.category, c.slug, c.description, c.created_at, \
+ COALESCE (( \
+ SELECT sum(c2.crates_cnt)::int \
+ FROM categories as c2 \
+ WHERE c2.slug = c.slug \
+ OR c2.slug LIKE c.slug || '::%' \
+ ), 0) as crates_cnt \
+ FROM categories as c \
+ WHERE c.category NOT LIKE '%::%' {} \
+ LIMIT $1 OFFSET $2",
+ sort_sql
+ )));
+
+ let categories: Vec<_> = try!(stmt.query(&[&limit, &offset]))
+ .iter()
+ .map(|row| {
+ let category: Category = Model::from_row(&row);
+ category.encodable()
+ })
+ .collect();
+
+ // Query for the total count of categories
+ let total = try!(Category::count_toplevel(conn));
+
+ #[derive(RustcEncodable)]
+ struct R { categories: Vec, meta: Meta }
+ #[derive(RustcEncodable)]
+ struct Meta { total: i64 }
+
+ Ok(req.json(&R {
+ categories: categories,
+ meta: Meta { total: total },
+ }))
+}
+
+/// Handles the `GET /categories/:category_id` route.
+pub fn show(req: &mut Request) -> CargoResult {
+ let slug = &req.params()["category_id"];
+ let conn = try!(req.tx());
+ let cat = try!(Category::find_by_slug(&*conn, &slug));
+ let subcats = try!(cat.subcategories(&*conn)).into_iter().map(|s| {
+ s.encodable()
+ }).collect();
+ let cat = cat.encodable();
+ let cat_with_subcats = EncodableCategoryWithSubcategories {
+ id: cat.id,
+ category: cat.category,
+ slug: cat.slug,
+ description: cat.description,
+ created_at: cat.created_at,
+ crates_cnt: cat.crates_cnt,
+ subcategories: subcats,
+ };
+
+ #[derive(RustcEncodable)]
+ struct R { category: EncodableCategoryWithSubcategories}
+ Ok(req.json(&R { category: cat_with_subcats }))
+}
+
+/// Handles the `GET /category_slugs` route.
+pub fn slugs(req: &mut Request) -> CargoResult {
+ let conn = try!(req.tx());
+ let stmt = try!(conn.prepare("SELECT slug FROM categories \
+ ORDER BY slug"));
+ let rows = try!(stmt.query(&[]));
+
+ #[derive(RustcEncodable)]
+ struct Slug { id: String, slug: String }
+
+ let slugs: Vec = rows.iter().map(|r| {
+ let slug: String = r.get("slug");
+ Slug { id: slug.clone(), slug: slug }
+ }).collect();
+
+ #[derive(RustcEncodable)]
+ struct R { category_slugs: Vec }
+ Ok(req.json(&R { category_slugs: slugs }))
+}
diff --git a/src/krate.rs b/src/krate.rs
index 2051977383..0a3247b04d 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -20,13 +20,14 @@ use semver;
use time::{Timespec, Duration};
use url::Url;
-use {Model, User, Keyword, Version};
+use {Model, User, Keyword, Version, Category};
use app::{App, RequestApp};
use db::RequestTransaction;
use dependency::{Dependency, EncodableDependency};
use download::{VersionDownload, EncodableVersionDownload};
use git;
use keyword::EncodableKeyword;
+use category::EncodableCategory;
use upload;
use user::RequestUser;
use owner::{EncodableOwner, Owner, Rights, OwnerKind, Team, rights};
@@ -59,6 +60,7 @@ pub struct EncodableCrate {
pub updated_at: String,
pub versions: Option>,
pub keywords: Option>,
+ pub categories: Option>,
pub created_at: String,
pub downloads: i32,
pub max_version: String,
@@ -227,9 +229,14 @@ impl Crate {
parts.next().is_none()
}
+ pub fn minimal_encodable(self) -> EncodableCrate {
+ self.encodable(None, None, None)
+ }
+
pub fn encodable(self,
versions: Option>,
- keywords: Option<&[Keyword]>)
+ keywords: Option<&[Keyword]>,
+ categories: Option<&[Category]>)
-> EncodableCrate {
let Crate {
name, created_at, updated_at, downloads, max_version, description,
@@ -241,6 +248,7 @@ impl Crate {
None => Some(format!("/api/v1/crates/{}/versions", name)),
};
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());
EncodableCrate {
id: name.clone(),
name: name.clone(),
@@ -249,6 +257,7 @@ impl Crate {
downloads: downloads,
versions: versions,
keywords: keyword_ids,
+ categories: category_ids,
max_version: max_version.to_string(),
documentation: documentation,
homepage: homepage,
@@ -386,6 +395,16 @@ impl Crate {
Ok(rows.iter().map(|r| Model::from_row(&r)).collect())
}
+ pub fn categories(&self, conn: &GenericConnection) -> CargoResult> {
+ let stmt = try!(conn.prepare("SELECT categories.* FROM categories \
+ LEFT JOIN crates_categories \
+ ON categories.id = \
+ crates_categories.category_id \
+ WHERE crates_categories.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,
@@ -502,6 +521,20 @@ pub fn index(req: &mut Request) -> CargoResult {
(format!("SELECT crates.* {} ORDER BY {} LIMIT $2 OFFSET $3", base, sort_sql),
format!("SELECT COUNT(crates.*) {}", base))
})
+ }).or_else(|| {
+ query.get("category").map(|cat| {
+ args.insert(0, cat);
+ let base = "FROM crates \
+ INNER JOIN crates_categories \
+ ON crates.id = crates_categories.crate_id \
+ INNER JOIN categories \
+ ON crates_categories.category_id = \
+ categories.id \
+ WHERE categories.slug = $1 OR \
+ categories.slug LIKE $1 || '::%'";
+ (format!("SELECT crates.* {} ORDER BY {} LIMIT $2 OFFSET $3", base, sort_sql),
+ format!("SELECT COUNT(crates.*) {}", base))
+ })
}).or_else(|| {
query.get("user_id").and_then(|s| s.parse::().ok()).map(|user_id| {
id = user_id;
@@ -554,7 +587,7 @@ 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.encodable(None, None));
+ crates.push(krate.minimal_encodable());
}
// Query for the total count of crates
@@ -589,7 +622,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.encodable(None, None)
+ krate.minimal_encodable()
}).collect::>())
};
let new_crates = try!(tx.prepare("SELECT * FROM crates \
@@ -626,19 +659,22 @@ pub fn show(req: &mut Request) -> CargoResult {
let versions = try!(krate.versions(conn));
let ids = versions.iter().map(|v| v.id).collect();
let kws = try!(krate.keywords(conn));
+ let cats = try!(krate.categories(conn));
#[derive(RustcEncodable)]
struct R {
krate: EncodableCrate,
versions: Vec,
keywords: Vec,
+ categories: Vec,
}
Ok(req.json(&R {
- krate: krate.clone().encodable(Some(ids), Some(&kws)),
+ krate: krate.clone().encodable(Some(ids), Some(&kws), Some(&cats)),
versions: versions.into_iter().map(|v| {
v.encodable(&krate.name)
}).collect(),
keywords: kws.into_iter().map(|k| k.encodable()).collect(),
+ categories: cats.into_iter().map(|k| k.encodable()).collect(),
}))
}
@@ -656,6 +692,10 @@ pub fn new(req: &mut Request) -> CargoResult {
.unwrap_or(&[]);
let keywords = keywords.iter().map(|k| k[..].to_string()).collect::>();
+ let categories = new_crate.categories.as_ref().map(|s| &s[..])
+ .unwrap_or(&[]);
+ let categories: Vec<_> = categories.iter().map(|k| k[..].to_string()).collect();
+
// Persist the new crate, if it doesn't already exist
let mut krate = try!(Crate::find_or_insert(try!(req.tx()), name, user.id,
&new_crate.description,
@@ -700,6 +740,12 @@ pub fn new(req: &mut Request) -> CargoResult {
// Update all keywords for this crate
try!(Keyword::update_crate(try!(req.tx()), &krate, &keywords));
+ // Update all categories for this crate, collecting any invalid categories
+ // in order to be able to warn about them
+ let ignored_invalid_categories = try!(
+ Category::update_crate(try!(req.tx()), &krate, &categories)
+ );
+
// Upload the crate to S3
let mut handle = req.app().handle();
let path = krate.s3_path(&vers.to_string());
@@ -757,8 +803,14 @@ pub fn new(req: &mut Request) -> CargoResult {
bomb.path = None;
#[derive(RustcEncodable)]
- struct R { krate: EncodableCrate }
- Ok(req.json(&R { krate: krate.encodable(None, None) }))
+ struct Warnings { invalid_categories: Vec }
+ let warnings = Warnings {
+ invalid_categories: ignored_invalid_categories,
+ };
+
+ #[derive(RustcEncodable)]
+ struct R { krate: EncodableCrate, warnings: Warnings }
+ Ok(req.json(&R { krate: krate.minimal_encodable(), warnings: warnings }))
}
fn parse_new_headers(req: &mut Request) -> CargoResult<(upload::NewCrate, User)> {
diff --git a/src/lib.rs b/src/lib.rs
index 33596374d8..550cb7d88a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -21,6 +21,7 @@ extern crate s3;
extern crate semver;
extern crate time;
extern crate url;
+extern crate toml;
extern crate conduit;
extern crate conduit_conditional_get;
@@ -33,6 +34,7 @@ extern crate conduit_router;
extern crate conduit_static;
pub use app::App;
+pub use self::category::Category;
pub use config::Config;
pub use self::dependency::Dependency;
pub use self::download::{CrateDownload, VersionDownload};
@@ -51,6 +53,8 @@ use conduit_middleware::MiddlewareBuilder;
use util::{C, R, R404};
pub mod app;
+pub mod categories;
+pub mod category;
pub mod config;
pub mod db;
pub mod dependency;
@@ -100,6 +104,9 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
api_router.get("/versions/:version_id", C(version::show));
api_router.get("/keywords", C(keyword::index));
api_router.get("/keywords/:keyword_id", C(keyword::show));
+ api_router.get("/categories", C(category::index));
+ api_router.get("/categories/:category_id", C(category::show));
+ api_router.get("/category_slugs", C(category::slugs));
api_router.get("/users/:user_id", C(user::show));
let api_router = Arc::new(R404(api_router));
diff --git a/src/tests/all.rs b/src/tests/all.rs
index 784e46ec0d..4f288adc96 100755
--- a/src/tests/all.rs
+++ b/src/tests/all.rs
@@ -26,7 +26,7 @@ use conduit_test::MockRequest;
use cargo_registry::app::App;
use cargo_registry::db::{self, RequestTransaction};
use cargo_registry::dependency::Kind;
-use cargo_registry::{User, Crate, Version, Keyword, Dependency};
+use cargo_registry::{User, Crate, Version, Keyword, Dependency, Category, Model};
use cargo_registry::upload as u;
macro_rules! t {
@@ -63,13 +63,14 @@ struct Error { detail: String }
#[derive(RustcDecodable)]
struct Bad { errors: Vec }
+mod category;
+mod git;
mod keyword;
mod krate;
-mod user;
mod record;
-mod git;
-mod version;
mod team;
+mod user;
+mod version;
fn app() -> (record::Bomb, Arc, conduit_middleware::MiddlewareBuilder) {
dotenv::dotenv().ok();
@@ -250,11 +251,20 @@ fn mock_keyword(req: &mut Request, name: &str) -> Keyword {
Keyword::find_or_insert(req.tx().unwrap(), name).unwrap()
}
+fn mock_category(req: &mut Request, name: &str, slug: &str) -> Category {
+ let conn = req.tx().unwrap();
+ let stmt = conn.prepare(" \
+ INSERT INTO categories (category, slug) \
+ VALUES ($1, $2) \
+ RETURNING *").unwrap();
+ let rows = stmt.query(&[&name, &slug]).unwrap();
+ Model::from_row(&rows.iter().next().unwrap())
+}
+
fn logout(req: &mut Request) {
req.mut_extensions().pop::();
}
-
fn new_req(app: Arc, krate: &str, version: &str) -> MockRequest {
new_req_full(app, ::krate(krate), version, Vec::new())
}
@@ -262,20 +272,32 @@ 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()));
+ req.with_body(&new_req_body(krate, version, deps, Vec::new(), Vec::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));
+ req.with_body(&new_req_body(krate, version, Vec::new(), kws, Vec::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));
+ return req;
+}
+
+fn new_req_body_version_2(krate: Crate) -> Vec {
+ new_req_body(krate, "2.0.0", Vec::new(), Vec::new(), Vec::new())
+}
+
fn new_req_body(krate: Crate, version: &str, deps: Vec,
- kws: Vec) -> Vec {
+ kws: Vec, cats: Vec) -> 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 {
name: u::CrateName(krate.name),
vers: u::CrateVersion(semver::Version::parse(version).unwrap()),
@@ -287,6 +309,7 @@ fn new_req_body(krate: Crate, version: &str, deps: Vec,
documentation: krate.documentation,
readme: krate.readme,
keywords: Some(u::KeywordList(kws)),
+ categories: Some(u::CategoryList(cats)),
license: Some("MIT".to_string()),
license_file: None,
repository: krate.repository,
diff --git a/src/tests/category.rs b/src/tests/category.rs
new file mode 100644
index 0000000000..be0448b291
--- /dev/null
+++ b/src/tests/category.rs
@@ -0,0 +1,139 @@
+use postgres::GenericConnection;
+use conduit::{Handler, Request, Method};
+use conduit_test::MockRequest;
+
+use cargo_registry::db::RequestTransaction;
+use cargo_registry::category::{Category, EncodableCategory, EncodableCategoryWithSubcategories};
+
+#[derive(RustcDecodable)]
+struct CategoryList { categories: Vec, meta: CategoryMeta }
+#[derive(RustcDecodable)]
+struct CategoryMeta { total: i32 }
+#[derive(RustcDecodable)]
+struct GoodCategory { category: EncodableCategory }
+#[derive(RustcDecodable)]
+struct CategoryWithSubcategories {
+ category: EncodableCategoryWithSubcategories
+}
+
+#[test]
+fn index() {
+ let (_b, app, middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/categories");
+
+ // List 0 categories if none exist
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: CategoryList = ::json(&mut response);
+ assert_eq!(json.categories.len(), 0);
+ assert_eq!(json.meta.total, 0);
+
+ // Create a category and a subcategory
+ ::mock_category(&mut req, "foo", "foo");
+ ::mock_category(&mut req, "foo::bar", "foo::bar");
+
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: CategoryList = ::json(&mut response);
+
+ // Only the top-level categories should be on the page
+ assert_eq!(json.categories.len(), 1);
+ assert_eq!(json.meta.total, 1);
+ assert_eq!(json.categories[0].category, "foo");
+}
+
+#[test]
+fn show() {
+ let (_b, app, middle) = ::app();
+
+ // Return not found if a category doesn't exist
+ let mut req = ::req(app, Method::Get, "/api/v1/categories/foo-bar");
+ let response = t_resp!(middle.call(&mut req));
+ assert_eq!(response.status.0, 404);
+
+ // Create a category and a subcategory
+ ::mock_category(&mut req, "Foo Bar", "foo-bar");
+ ::mock_category(&mut req, "Foo Bar::Baz", "foo-bar::baz");
+
+ // The category and its subcategories should be in the json
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: CategoryWithSubcategories = ::json(&mut response);
+ assert_eq!(json.category.category, "Foo Bar");
+ assert_eq!(json.category.slug, "foo-bar");
+ assert_eq!(json.category.subcategories.len(), 1);
+ assert_eq!(json.category.subcategories[0].category, "Foo Bar::Baz");
+}
+
+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/categories/foo");
+ let cnt = |req: &mut MockRequest, cat: &str| {
+ req.with_path(&format!("/api/v1/categories/{}", cat));
+ let mut response = ok_resp!(middle.call(req));
+ ::json::(&mut response).category.crates_cnt as usize
+ };
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, _) = ::mock_crate(&mut req, ::krate("foocat"));
+ ::mock_category(&mut req, "cat1", "cat1");
+ ::mock_category(&mut req, "Category 2", "category-2");
+
+ // Updating with no categories has no effect
+ Category::update_crate(tx(&req), &krate, &[]).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 0);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+ // Happy path adding one category
+ Category::update_crate(tx(&req), &krate, &["cat1".to_string()]).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 1);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+ // Replacing one category with another
+ Category::update_crate(
+ tx(&req), &krate, &["category-2".to_string()]
+ ).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 0);
+ assert_eq!(cnt(&mut req, "category-2"), 1);
+
+ // Removing one category
+ Category::update_crate(tx(&req), &krate, &[]).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 0);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+ // Adding 2 categories
+ Category::update_crate(
+ tx(&req), &krate, &["cat1".to_string(),
+ "category-2".to_string()]).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 1);
+ assert_eq!(cnt(&mut req, "category-2"), 1);
+
+ // Removing all categories
+ Category::update_crate(tx(&req), &krate, &[]).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 0);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+ // Attempting to add one valid category and one invalid category
+ let invalid_categories = Category::update_crate(
+ tx(&req), &krate, &["cat1".to_string(),
+ "catnope".to_string()]
+ ).unwrap();
+ assert_eq!(invalid_categories, vec!["catnope".to_string()]);
+ assert_eq!(cnt(&mut req, "cat1"), 1);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+ // Does not add the invalid category to the category list
+ // (unlike the behavior of keywords)
+ req.with_path("/api/v1/categories");
+ let mut response = ok_resp!(middle.call(&mut req));
+ let json: CategoryList = ::json(&mut response);
+ assert_eq!(json.categories.len(), 2);
+ assert_eq!(json.meta.total, 2);
+
+ // Attempting to add a category by display text; must use slug
+ Category::update_crate(
+ tx(&req), &krate, &["Category 2".to_string()]
+ ).unwrap();
+ assert_eq!(cnt(&mut req, "cat1"), 0);
+ assert_eq!(cnt(&mut req, "category-2"), 0);
+
+}
diff --git a/src/tests/http-data/krate_good_categories b/src/tests/http-data/krate_good_categories
new file mode 100644
index 0000000000..789c51acc1
--- /dev/null
+++ b/src/tests/http-data/krate_good_categories
@@ -0,0 +1,21 @@
+===REQUEST 349
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_good_cat/foo_good_cat-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_categories b/src/tests/http-data/krate_ignored_categories
new file mode 100644
index 0000000000..273ea4252e
--- /dev/null
+++ b/src/tests/http-data/krate_ignored_categories
@@ -0,0 +1,21 @@
+===REQUEST 355
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_ignored_cat/foo_ignored_cat-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_new_crate_owner b/src/tests/http-data/krate_new_crate_owner
index e6649ac466..d1e6f3bfb0 100644
--- a/src/tests/http-data/krate_new_crate_owner
+++ b/src/tests/http-data/krate_new_crate_owner
@@ -1,5 +1,5 @@
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1
+===REQUEST 344
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_owner/foo_owner-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Host: alexcrichton-test.s3.amazonaws.com
@@ -19,8 +19,8 @@ etag: "d41d8cd98f00b204e9800998ecf8427e"
date: Sun, 28 Jun 2015 21:07:51 GMT
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1
+===REQUEST 344
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_owner/foo_owner-2.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Authorization: AWS AKIAJF3GEK7N44BACDZA:wz9J18+QxEhMIqIDnzwP6fGGeGY=
diff --git a/src/tests/http-data/krate_new_krate b/src/tests/http-data/krate_new_krate
index 97520e5859..204372eb89 100644
--- a/src/tests/http-data/krate_new_krate
+++ b/src/tests/http-data/krate_new_krate
@@ -1,5 +1,5 @@
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1
+===REQUEST 339
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_new/foo_new-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk=
diff --git a/src/tests/http-data/krate_new_krate_git_upload b/src/tests/http-data/krate_new_krate_git_upload
index 8f8a5c52bf..ff4d5d2813 100644
--- a/src/tests/http-data/krate_new_krate_git_upload
+++ b/src/tests/http-data/krate_new_krate_git_upload
@@ -1,5 +1,5 @@
===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/fgt/fgt-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Host: alexcrichton-test.s3.amazonaws.com
diff --git a/src/tests/http-data/krate_new_krate_git_upload_appends b/src/tests/http-data/krate_new_krate_git_upload_appends
index 67669e898f..9f82760e61 100644
--- a/src/tests/http-data/krate_new_krate_git_upload_appends
+++ b/src/tests/http-data/krate_new_krate_git_upload_appends
@@ -1,5 +1,5 @@
===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/FOO/FOO-1.0.0.crate HTTP/1.1
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/FPP/FPP-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Authorization: AWS AKIAJF3GEK7N44BACDZA:mLugvy/BSBS8JgZ6pK7tURu3OTU=
diff --git a/src/tests/http-data/krate_new_krate_git_upload_with_conflicts b/src/tests/http-data/krate_new_krate_git_upload_with_conflicts
index df973179f2..620c578fec 100644
--- a/src/tests/http-data/krate_new_krate_git_upload_with_conflicts
+++ b/src/tests/http-data/krate_new_krate_git_upload_with_conflicts
@@ -1,5 +1,5 @@
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1
+===REQUEST 351
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_conflicts/foo_conflicts-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Authorization: AWS AKIAJF3GEK7N44BACDZA:GDxGb6r3SIqo9wXuzHrgMNWekwk=
diff --git a/src/tests/http-data/krate_new_krate_too_big_but_whitelisted b/src/tests/http-data/krate_new_krate_too_big_but_whitelisted
index 67dd235b30..ef328d5578 100644
--- a/src/tests/http-data/krate_new_krate_too_big_but_whitelisted
+++ b/src/tests/http-data/krate_new_krate_too_big_but_whitelisted
@@ -1,5 +1,5 @@
-===REQUEST 2334
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.1.0.crate HTTP/1.1
+===REQUEST 2354
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_whitelist/foo_whitelist-1.1.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Content-Type: application/x-tar
diff --git a/src/tests/http-data/krate_new_krate_twice b/src/tests/http-data/krate_new_krate_twice
index f139153e1e..f60be292d0 100644
--- a/src/tests/http-data/krate_new_krate_twice
+++ b/src/tests/http-data/krate_new_krate_twice
@@ -1,5 +1,5 @@
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1
+===REQUEST 343
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_twice/foo_twice-2.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Content-Length: 0
diff --git a/src/tests/http-data/krate_new_krate_weird_version b/src/tests/http-data/krate_new_krate_weird_version
index 15cb1daddb..e34a7eaf22 100644
--- a/src/tests/http-data/krate_new_krate_weird_version
+++ b/src/tests/http-data/krate_new_krate_weird_version
@@ -1,5 +1,5 @@
-===REQUEST 335
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-0.0.0-pre.crate HTTP/1.1
+===REQUEST 347
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_weird/foo_weird-0.0.0-pre.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Host: alexcrichton-test.s3.amazonaws.com
diff --git a/src/tests/http-data/krate_new_krate_with_dependency b/src/tests/http-data/krate_new_krate_with_dependency
index e8bab5311c..5eaa3b20fa 100644
--- a/src/tests/http-data/krate_new_krate_with_dependency
+++ b/src/tests/http-data/krate_new_krate_with_dependency
@@ -1,5 +1,5 @@
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/new/new-1.0.0.crate HTTP/1.1
+===REQUEST 339
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/new_dep/new_dep-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Date: Sun, 28 Jun 2015 14:07:18 -0700
diff --git a/src/tests/http-data/krate_yank b/src/tests/http-data/krate_yank
index 1e79509257..bda50a860c 100644
--- a/src/tests/http-data/krate_yank
+++ b/src/tests/http-data/krate_yank
@@ -1,5 +1,5 @@
===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate HTTP/1.1
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/fyk/fyk-1.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Date: Sun, 28 Jun 2015 14:07:18 -0700
diff --git a/src/tests/http-data/team_publish_owned b/src/tests/http-data/team_publish_owned
index 32ef51aab4..4f1aa9fa91 100644
--- a/src/tests/http-data/team_publish_owned
+++ b/src/tests/http-data/team_publish_owned
@@ -155,8 +155,8 @@ cache-control: private, max-age=60, s-maxage=60
x-content-type-options: nosniff
{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"}
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1
+===REQUEST 353
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_team_owned/foo_team_owned-2.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Host: alexcrichton-test.s3.amazonaws.com
diff --git a/src/tests/http-data/team_remove_team_as_team_owner b/src/tests/http-data/team_remove_team_as_team_owner
index 9b3b67d073..fc00c0d583 100644
--- a/src/tests/http-data/team_remove_team_as_team_owner
+++ b/src/tests/http-data/team_remove_team_as_team_owner
@@ -194,8 +194,8 @@ x-frame-options: deny
x-ratelimit-reset: 1439881924
{"state":"active","url":"https://api.github.com/teams/1699377/memberships/crates-tester-1"}
-===REQUEST 331
-PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-2.0.0.crate HTTP/1.1
+===REQUEST 367
+PUT http://alexcrichton-test.s3.amazonaws.com/crates/foo_remove_team_owner/foo_remove_team_owner-2.0.0.crate HTTP/1.1
Accept: */*
Proxy-Connection: Keep-Alive
Date: Mon, 17 Aug 2015 23:12:52 -0700
diff --git a/src/tests/keyword.rs b/src/tests/keyword.rs
index 5465d78b1a..c1f50bf38f 100644
--- a/src/tests/keyword.rs
+++ b/src/tests/keyword.rs
@@ -65,7 +65,7 @@ fn update_crate() {
::json::(&mut response).keyword.crates_cnt as usize
};
::mock_user(&mut req, ::user("foo"));
- let (krate, _) = ::mock_crate(&mut req, ::krate("foo"));
+ let (krate, _) = ::mock_crate(&mut req, ::krate("fookey"));
::mock_keyword(&mut req, "kw1");
::mock_keyword(&mut req, "kw2");
diff --git a/src/tests/krate.rs b/src/tests/krate.rs
index e9ff94d06e..92b1971a85 100644
--- a/src/tests/krate.rs
+++ b/src/tests/krate.rs
@@ -26,7 +26,9 @@ struct CrateMeta { total: i32 }
#[derive(RustcDecodable)]
struct GitCrate { name: String, vers: String, deps: Vec, cksum: String }
#[derive(RustcDecodable)]
-struct GoodCrate { krate: EncodableCrate }
+struct Warnings { invalid_categories: Vec }
+#[derive(RustcDecodable)]
+struct GoodCrate { krate: EncodableCrate, warnings: Warnings }
#[derive(RustcDecodable)]
struct CrateResponse { krate: EncodableCrate, versions: Vec, keywords: Vec }
#[derive(RustcDecodable)]
@@ -48,6 +50,7 @@ fn new_crate(name: &str) -> u::NewCrate {
documentation: None,
readme: None,
keywords: None,
+ categories: None,
license: Some("MIT".to_string()),
license_file: None,
repository: None,
@@ -63,7 +66,7 @@ fn index() {
assert_eq!(json.crates.len(), 0);
assert_eq!(json.meta.total, 0);
- let krate = ::krate("foo");
+ let krate = ::krate("fooindex");
::mock_user(&mut req, ::user("foo"));
::mock_crate(&mut req, krate.clone());
let mut response = ok_resp!(middle.call(&mut req));
@@ -82,11 +85,11 @@ fn index_queries() {
let mut req = ::req(app, Method::Get, "/api/v1/crates");
let u = ::mock_user(&mut req, ::user("foo"));
- let mut krate = ::krate("foo");
+ let mut krate = ::krate("foo_index_queries");
krate.readme = Some("readme".to_string());
krate.description = Some("description".to_string());
let (krate, _) = ::mock_crate(&mut req, krate.clone());
- let krate2 = ::krate("BAR");
+ let krate2 = ::krate("BAR_INDEX_QUERIES");
let (krate2, _) = ::mock_crate(&mut req, krate2.clone());
Keyword::update_crate(tx(&req), &krate, &["kw1".into()]).unwrap();
Keyword::update_crate(tx(&req), &krate2, &["KW1".into()]).unwrap();
@@ -133,39 +136,39 @@ fn exact_match_first_on_queries() {
let mut req = ::req(app, Method::Get, "/api/v1/crates");
let _ = ::mock_user(&mut req, ::user("foo"));
- let mut krate = ::krate("foo");
- krate.description = Some("bar baz".to_string());
+ let mut krate = ::krate("foo_exact");
+ krate.description = Some("bar_exact baz_exact".to_string());
let (_, _) = ::mock_crate(&mut req, krate.clone());
- let mut krate2 = ::krate("bar");
- krate2.description = Some("foo baz foo baz".to_string());
+ let mut krate2 = ::krate("bar_exact");
+ krate2.description = Some("foo_exact baz_exact foo_exact baz_exact".to_string());
let (_, _) = ::mock_crate(&mut req, krate2.clone());
- let mut krate3 = ::krate("baz");
- krate3.description = Some("foo bar foo bar foo bar".to_string());
+ let mut krate3 = ::krate("baz_exact");
+ krate3.description = Some("foo_exact bar_exact foo_exact bar_exact foo_exact bar_exact".to_string());
let (_, _) = ::mock_crate(&mut req, krate3.clone());
- let mut krate4 = ::krate("other");
- krate4.description = Some("other".to_string());
+ let mut krate4 = ::krate("other_exact");
+ krate4.description = Some("other_exact".to_string());
let (_, _) = ::mock_crate(&mut req, krate4.clone());
- let mut response = ok_resp!(middle.call(req.with_query("q=foo")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=foo_exact")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "foo");
- assert_eq!(json.crates[1].name, "baz");
- assert_eq!(json.crates[2].name, "bar");
+ assert_eq!(json.crates[0].name, "foo_exact");
+ assert_eq!(json.crates[1].name, "baz_exact");
+ assert_eq!(json.crates[2].name, "bar_exact");
- let mut response = ok_resp!(middle.call(req.with_query("q=bar")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=bar_exact")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "bar");
- assert_eq!(json.crates[1].name, "baz");
- assert_eq!(json.crates[2].name, "foo");
+ assert_eq!(json.crates[0].name, "bar_exact");
+ assert_eq!(json.crates[1].name, "baz_exact");
+ assert_eq!(json.crates[2].name, "foo_exact");
- let mut response = ok_resp!(middle.call(req.with_query("q=baz")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=baz_exact")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "baz");
- assert_eq!(json.crates[1].name, "bar");
- assert_eq!(json.crates[2].name, "foo");
+ assert_eq!(json.crates[0].name, "baz_exact");
+ assert_eq!(json.crates[1].name, "bar_exact");
+ assert_eq!(json.crates[2].name, "foo_exact");
}
#[test]
@@ -174,23 +177,23 @@ fn exact_match_on_queries_with_sort() {
let mut req = ::req(app, Method::Get, "/api/v1/crates");
let _ = ::mock_user(&mut req, ::user("foo"));
- let mut krate = ::krate("foo");
- krate.description = Some("bar baz const".to_string());
+ let mut krate = ::krate("foo_sort");
+ krate.description = Some("bar_sort baz_sort const".to_string());
krate.downloads = 50;
let (k, _) = ::mock_crate(&mut req, krate.clone());
- let mut krate2 = ::krate("bar");
- krate2.description = Some("foo baz foo baz const".to_string());
+ let mut krate2 = ::krate("bar_sort");
+ krate2.description = Some("foo_sort baz_sort foo_sort baz_sort const".to_string());
krate2.downloads = 3333;
let (k2, _) = ::mock_crate(&mut req, krate2.clone());
- let mut krate3 = ::krate("baz");
- krate3.description = Some("foo bar foo bar foo bar const".to_string());
+ let mut krate3 = ::krate("baz_sort");
+ krate3.description = Some("foo_sort bar_sort foo_sort bar_sort foo_sort bar_sort const".to_string());
krate3.downloads = 100000;
let (k3, _) = ::mock_crate(&mut req, krate3.clone());
- let mut krate4 = ::krate("other");
- krate4.description = Some("other const".to_string());
+ let mut krate4 = ::krate("other_sort");
+ krate4.description = Some("other_sort const".to_string());
krate4.downloads = 999999;
let (k4, _) = ::mock_crate(&mut req, krate4.clone());
-
+
{
let req2: &mut Request = &mut req;
let tx = req2.tx().unwrap();
@@ -204,42 +207,42 @@ fn exact_match_on_queries_with_sort() {
WHERE id = $2", &[&krate4.downloads, &k4.id]).unwrap();
}
- let mut response = ok_resp!(middle.call(req.with_query("q=foo&sort=downloads")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=foo_sort&sort=downloads")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "foo");
- assert_eq!(json.crates[1].name, "baz");
- assert_eq!(json.crates[2].name, "bar");
+ assert_eq!(json.crates[0].name, "foo_sort");
+ assert_eq!(json.crates[1].name, "baz_sort");
+ assert_eq!(json.crates[2].name, "bar_sort");
- let mut response = ok_resp!(middle.call(req.with_query("q=bar&sort=downloads")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=bar_sort&sort=downloads")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "bar");
- assert_eq!(json.crates[1].name, "baz");
- assert_eq!(json.crates[2].name, "foo");
+ assert_eq!(json.crates[0].name, "bar_sort");
+ assert_eq!(json.crates[1].name, "baz_sort");
+ assert_eq!(json.crates[2].name, "foo_sort");
- let mut response = ok_resp!(middle.call(req.with_query("q=baz&sort=downloads")));
+ let mut response = ok_resp!(middle.call(req.with_query("q=baz_sort&sort=downloads")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 3);
- assert_eq!(json.crates[0].name, "baz");
- assert_eq!(json.crates[1].name, "bar");
- assert_eq!(json.crates[2].name, "foo");
+ assert_eq!(json.crates[0].name, "baz_sort");
+ assert_eq!(json.crates[1].name, "bar_sort");
+ assert_eq!(json.crates[2].name, "foo_sort");
let mut response = ok_resp!(middle.call(req.with_query("q=const&sort=downloads")));
let json: CrateList = ::json(&mut response);
assert_eq!(json.meta.total, 4);
- assert_eq!(json.crates[0].name, "other");
- assert_eq!(json.crates[1].name, "baz");
- assert_eq!(json.crates[2].name, "bar");
- assert_eq!(json.crates[3].name, "foo");
+ assert_eq!(json.crates[0].name, "other_sort");
+ assert_eq!(json.crates[1].name, "baz_sort");
+ assert_eq!(json.crates[2].name, "bar_sort");
+ assert_eq!(json.crates[3].name, "foo_sort");
}
#[test]
fn show() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_show");
::mock_user(&mut req, ::user("foo"));
- let mut krate = ::krate("foo");
+ let mut krate = ::krate("foo_show");
krate.description = Some(format!("description"));
krate.documentation = Some(format!("https://example.com"));
krate.homepage = Some(format!("http://example.com"));
@@ -260,7 +263,7 @@ fn show() {
assert_eq!(json.versions[0].id, versions[0]);
assert_eq!(json.versions[0].krate, json.krate.id);
assert_eq!(json.versions[0].num, "1.0.0".to_string());
- let suffix = "/api/v1/crates/foo/1.0.0/download";
+ let suffix = "/api/v1/crates/foo_show/1.0.0/download";
assert!(json.versions[0].dl_path.ends_with(suffix),
"bad suffix {}", json.versions[0].dl_path);
assert_eq!(1, json.keywords.len());
@@ -270,9 +273,9 @@ fn show() {
#[test]
fn versions() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/versions");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_versions/versions");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_versions"));
let mut response = ok_resp!(middle.call(&mut req));
let json: VersionsList = ::json(&mut response);
assert_eq!(json.versions.len(), 1);
@@ -317,26 +320,26 @@ fn new_bad_names() {
#[test]
fn new_krate() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_new", "1.0.0");
let user = ::mock_user(&mut req, ::user("foo"));
::logout(&mut req);
req.header("Authorization", &user.api_token);
let mut response = ok_resp!(middle.call(&mut req));
let json: GoodCrate = ::json(&mut response);
- assert_eq!(json.krate.name, "foo");
+ assert_eq!(json.krate.name, "foo_new");
assert_eq!(json.krate.max_version, "1.0.0");
}
#[test]
fn new_krate_weird_version() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "0.0.0-pre");
+ let mut req = ::new_req(app, "foo_weird", "0.0.0-pre");
let user = ::mock_user(&mut req, ::user("foo"));
::logout(&mut req);
req.header("Authorization", &user.api_token);
let mut response = ok_resp!(middle.call(&mut req));
let json: GoodCrate = ::json(&mut response);
- assert_eq!(json.krate.name, "foo");
+ assert_eq!(json.krate.name, "foo_weird");
assert_eq!(json.krate.max_version, "0.0.0-pre");
}
@@ -344,7 +347,7 @@ fn new_krate_weird_version() {
fn new_krate_with_dependency() {
let (_b, app, middle) = ::app();
let dep = u::CrateDependency {
- name: u::CrateName("foo".to_string()),
+ name: u::CrateName("foo_dep".to_string()),
optional: false,
default_features: true,
features: Vec::new(),
@@ -352,9 +355,9 @@ fn new_krate_with_dependency() {
target: None,
kind: None,
};
- let mut req = ::new_req_full(app, ::krate("new"), "1.0.0", vec![dep]);
+ let mut req = ::new_req_full(app, ::krate("new_dep"), "1.0.0", vec![dep]);
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_dep"));
let mut response = ok_resp!(middle.call(&mut req));
::json::(&mut response);
}
@@ -363,7 +366,7 @@ fn new_krate_with_dependency() {
fn new_krate_with_wildcard_dependency() {
let (_b, app, middle) = ::app();
let dep = u::CrateDependency {
- name: u::CrateName("foo".to_string()),
+ name: u::CrateName("foo_wild".to_string()),
optional: false,
default_features: true,
features: Vec::new(),
@@ -371,9 +374,9 @@ fn new_krate_with_wildcard_dependency() {
target: None,
kind: None,
};
- let mut req = ::new_req_full(app, ::krate("new"), "1.0.0", vec![dep]);
+ let mut req = ::new_req_full(app, ::krate("new_wild"), "1.0.0", vec![dep]);
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_wild"));
let json = bad_resp!(middle.call(&mut req));
assert!(json.errors[0].detail.contains("dependency constraints"), "{:?}", json.errors);
}
@@ -381,11 +384,11 @@ fn new_krate_with_wildcard_dependency() {
#[test]
fn new_krate_twice() {
let (_b, app, middle) = ::app();
- let mut krate = ::krate("foo");
+ let mut krate = ::krate("foo_twice");
krate.description = Some("description".to_string());
let mut req = ::new_req_full(app, krate.clone(), "2.0.0", Vec::new());
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_twice"));
let mut response = ok_resp!(middle.call(&mut req));
let json: GoodCrate = ::json(&mut response);
assert_eq!(json.krate.name, krate.name);
@@ -396,11 +399,11 @@ fn new_krate_twice() {
fn new_krate_wrong_user() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "2.0.0");
+ let mut req = ::new_req(app, "foo_wrong", "2.0.0");
// Create the 'foo' crate with one user
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_wrong"));
// But log in another
::mock_user(&mut req, ::user("bar"));
@@ -437,7 +440,7 @@ fn new_crate_owner() {
let (_b, app, middle) = ::app();
// Create a crate under one user
- let mut req = ::new_req(app.clone(), "foo", "1.0.0");
+ let mut req = ::new_req(app.clone(), "foo_owner", "1.0.0");
let u2 = ::mock_user(&mut req, ::user("bar"));
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
@@ -445,11 +448,11 @@ fn new_crate_owner() {
// Flag the second user as an owner
let body = r#"{"users":["bar"]}"#;
- let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_owner/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(::json::(&mut response).ok);
- bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_owner/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
@@ -461,7 +464,7 @@ fn new_crate_owner() {
assert_eq!(::json::(&mut response).crates.len(), 1);
// And upload a new crate as the first user
- let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
+ let body = ::new_req_body_version_2(::krate("foo_owner"));
req.mut_extensions().insert(u2);
let mut response = ok_resp!(middle.call(req.with_path("/api/v1/crates/new")
.with_method(Method::Put)
@@ -481,21 +484,21 @@ fn valid_feature_names() {
#[test]
fn new_krate_too_big() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_big", "1.0.0");
::mock_user(&mut req, ::user("foo"));
- let body = ::new_crate_to_body(&new_crate("foo"), &[b'a'; 2000]);
+ let body = ::new_crate_to_body(&new_crate("foo_big"), &[b'a'; 2000]);
bad_resp!(middle.call(req.with_body(&body)));
}
#[test]
fn new_krate_too_big_but_whitelisted() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.1.0");
+ let mut req = ::new_req(app, "foo_whitelist", "1.1.0");
::mock_user(&mut req, ::user("foo"));
- let mut krate = ::krate("foo");
+ let mut krate = ::krate("foo_whitelist");
krate.max_upload_size = Some(2 * 1000 * 1000);
::mock_crate(&mut req, krate);
- let body = ::new_crate_to_body(&new_crate("foo"), &[b'a'; 2000]);
+ let body = ::new_crate_to_body(&new_crate("foo_whitelist"), &[b'a'; 2000]);
let mut response = ok_resp!(middle.call(req.with_body(&body)));
::json::(&mut response);
}
@@ -503,9 +506,9 @@ fn new_krate_too_big_but_whitelisted() {
#[test]
fn new_krate_duplicate_version() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_dupe", "1.0.0");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_dupe"));
let json = bad_resp!(middle.call(&mut req));
assert!(json.errors[0].detail.contains("already uploaded"),
"{:?}", json.errors);
@@ -514,9 +517,9 @@ fn new_krate_duplicate_version() {
#[test]
fn new_crate_similar_name() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.1.0");
+ let mut req = ::new_req(app, "foo_similar", "1.1.0");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("Foo"));
+ ::mock_crate(&mut req, ::krate("Foo_similar"));
let json = bad_resp!(middle.call(&mut req));
assert!(json.errors[0].detail.contains("previously named"),
"{:?}", json.errors);
@@ -526,18 +529,18 @@ fn new_crate_similar_name() {
fn new_crate_similar_name_hyphen() {
{
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo-bar", "1.1.0");
+ let mut req = ::new_req(app, "foo-bar-hyphen", "1.1.0");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo_bar"));
+ ::mock_crate(&mut req, ::krate("foo_bar_hyphen"));
let json = bad_resp!(middle.call(&mut req));
assert!(json.errors[0].detail.contains("previously named"),
"{:?}", json.errors);
}
{
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo_bar", "1.1.0");
+ let mut req = ::new_req(app, "foo_bar_underscore", "1.1.0");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo-bar"));
+ ::mock_crate(&mut req, ::krate("foo-bar-underscore"));
let json = bad_resp!(middle.call(&mut req));
assert!(json.errors[0].detail.contains("previously named"),
"{:?}", json.errors);
@@ -547,17 +550,17 @@ fn new_crate_similar_name_hyphen() {
#[test]
fn new_krate_git_upload() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "fgt", "1.0.0");
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
::json::(&mut response);
- let path = ::git::checkout().join("3/f/foo");
+ let path = ::git::checkout().join("3/f/fgt");
assert!(path.exists());
let mut contents = String::new();
File::open(&path).unwrap().read_to_string(&mut contents).unwrap();
let p: GitCrate = json::decode(&contents).unwrap();
- assert_eq!(p.name, "foo");
+ assert_eq!(p.name, "fgt");
assert_eq!(p.vers, "1.0.0");
assert!(p.deps.is_empty());
assert_eq!(p.cksum,
@@ -567,13 +570,13 @@ fn new_krate_git_upload() {
#[test]
fn new_krate_git_upload_appends() {
let (_b, app, middle) = ::app();
- let path = ::git::checkout().join("3/f/foo");
+ let path = ::git::checkout().join("3/f/fpp");
fs::create_dir_all(path.parent().unwrap()).unwrap();
File::create(&path).unwrap().write_all(
- br#"{"name":"FOO","vers":"0.0.1","deps":[],"cksum":"3j3"}
+ br#"{"name":"FPP","vers":"0.0.1","deps":[],"cksum":"3j3"}
"#).unwrap();
- let mut req = ::new_req(app, "FOO", "1.0.0");
+ let mut req = ::new_req(app, "FPP", "1.0.0");
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
::json::(&mut response);
@@ -584,10 +587,10 @@ fn new_krate_git_upload_appends() {
let p1: GitCrate = json::decode(lines.next().unwrap().trim()).unwrap();
let p2: GitCrate = json::decode(lines.next().unwrap().trim()).unwrap();
assert!(lines.next().is_none());
- assert_eq!(p1.name, "FOO");
+ assert_eq!(p1.name, "FPP");
assert_eq!(p1.vers, "0.0.1");
assert!(p1.deps.is_empty());
- assert_eq!(p2.name, "FOO");
+ assert_eq!(p2.name, "FPP");
assert_eq!(p2.vers, "1.0.0");
assert!(p2.deps.is_empty());
}
@@ -606,7 +609,7 @@ fn new_krate_git_upload_with_conflicts() {
&[&parent]).unwrap();
}
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_conflicts", "1.0.0");
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
::json::(&mut response);
@@ -618,18 +621,18 @@ fn new_krate_dependency_missing() {
let dep = u::CrateDependency {
optional: false,
default_features: true,
- name: u::CrateName("bar".to_string()),
+ name: u::CrateName("bar_missing".to_string()),
features: Vec::new(),
version_req: u::CrateVersionReq(semver::VersionReq::parse(">= 0.0.0").unwrap()),
target: None,
kind: None,
};
- let mut req = ::new_req_full(app, ::krate("foo"), "1.0.0", vec![dep]);
+ let mut req = ::new_req_full(app, ::krate("foo_missing"), "1.0.0", vec![dep]);
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
let json = ::json::<::Bad>(&mut response);
assert!(json.errors[0].detail
- .contains("no known crate named `bar`"));
+ .contains("no known crate named `bar_missing`"));
}
#[test]
@@ -642,22 +645,22 @@ fn summary_doesnt_die() {
#[test]
fn download() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/1.0.0/download");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_download/1.0.0/download");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_download"));
let resp = t_resp!(middle.call(&mut req));
assert_eq!(resp.status.0, 302);
- req.with_path("/api/v1/crates/foo/1.0.0/downloads");
+ req.with_path("/api/v1/crates/foo_download/1.0.0/downloads");
let mut resp = ok_resp!(middle.call(&mut req));
let downloads = ::json::(&mut resp);
assert_eq!(downloads.version_downloads.len(), 1);
- req.with_path("/api/v1/crates/FOO/1.0.0/download");
+ req.with_path("/api/v1/crates/FOO_DOWNLOAD/1.0.0/download");
let resp = t_resp!(middle.call(&mut req));
assert_eq!(resp.status.0, 302);
- req.with_path("/api/v1/crates/FOO/1.0.0/downloads");
+ req.with_path("/api/v1/crates/FOO_DOWNLOAD/1.0.0/downloads");
let mut resp = ok_resp!(middle.call(&mut req));
let downloads = ::json::(&mut resp);
assert_eq!(downloads.version_downloads.len(), 1);
@@ -666,9 +669,9 @@ fn download() {
#[test]
fn download_bad() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/0.1.0/download");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_bad/0.1.0/download");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_bad"));
let mut response = ok_resp!(middle.call(&mut req));
::json::<::Bad>(&mut response);
}
@@ -677,17 +680,17 @@ fn download_bad() {
fn dependencies() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/1.0.0/dependencies");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_deps/1.0.0/dependencies");
::mock_user(&mut req, ::user("foo"));
- let (_, v) = ::mock_crate(&mut req, ::krate("foo"));
- let (c, _) = ::mock_crate(&mut req, ::krate("bar"));
+ let (_, v) = ::mock_crate(&mut req, ::krate("foo_deps"));
+ let (c, _) = ::mock_crate(&mut req, ::krate("bar_deps"));
::mock_dep(&mut req, &v, &c, None);
let mut response = ok_resp!(middle.call(&mut req));
let deps = ::json::(&mut response);
- assert_eq!(deps.dependencies[0].crate_id, "bar");
+ assert_eq!(deps.dependencies[0].crate_id, "bar_deps");
- req.with_path("/api/v1/crates/foo/1.0.2/dependencies");
+ req.with_path("/api/v1/crates/foo_deps/1.0.2/dependencies");
let mut response = ok_resp!(middle.call(&mut req));
::json::<::Bad>(&mut response);
}
@@ -698,21 +701,21 @@ fn following() {
#[derive(RustcDecodable)] struct O { ok: bool }
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/following");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_following/following");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_following"));
let mut response = ok_resp!(middle.call(&mut req));
assert!(!::json::(&mut response).following);
- req.with_path("/api/v1/crates/foo/follow")
+ req.with_path("/api/v1/crates/foo_following/follow")
.with_method(Method::Put);
let mut response = ok_resp!(middle.call(&mut req));
assert!(::json::(&mut response).ok);
let mut response = ok_resp!(middle.call(&mut req));
assert!(::json::(&mut response).ok);
- req.with_path("/api/v1/crates/foo/following")
+ req.with_path("/api/v1/crates/foo_following/following")
.with_method(Method::Get);
let mut response = ok_resp!(middle.call(&mut req));
assert!(::json::(&mut response).following);
@@ -723,14 +726,14 @@ fn following() {
let l = ::json::(&mut response);
assert_eq!(l.crates.len(), 1);
- req.with_path("/api/v1/crates/foo/follow")
+ req.with_path("/api/v1/crates/foo_following/follow")
.with_method(Method::Delete);
let mut response = ok_resp!(middle.call(&mut req));
assert!(::json::(&mut response).ok);
let mut response = ok_resp!(middle.call(&mut req));
assert!(::json::(&mut response).ok);
- req.with_path("/api/v1/crates/foo/following")
+ req.with_path("/api/v1/crates/foo_following/following")
.with_method(Method::Get);
let mut response = ok_resp!(middle.call(&mut req));
assert!(!::json::(&mut response).following);
@@ -748,11 +751,11 @@ fn owners() {
#[derive(RustcDecodable)] struct O { ok: bool }
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/owners");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_owners/owners");
let other = ::user("foobar");
::mock_user(&mut req, other);
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_owners"));
let mut response = ok_resp!(middle.call(&mut req));
let r: R = ::json(&mut response);
@@ -796,10 +799,10 @@ fn yank() {
#[derive(RustcDecodable)] struct O { ok: bool }
#[derive(RustcDecodable)] struct V { version: EncodableVersion }
let (_b, app, middle) = ::app();
- let path = ::git::checkout().join("3/f/foo");
+ let path = ::git::checkout().join("3/f/fyk");
// Upload a new crate, putting it in the git index
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "fyk", "1.0.0");
::mock_user(&mut req, ::user("foo"));
let mut response = ok_resp!(middle.call(&mut req));
::json::(&mut response);
@@ -809,38 +812,38 @@ fn yank() {
// make sure it's not yanked
let mut r = ok_resp!(middle.call(req.with_method(Method::Get)
- .with_path("/api/v1/crates/foo/1.0.0")));
+ .with_path("/api/v1/crates/fyk/1.0.0")));
assert!(!::json::(&mut r).version.yanked);
// yank it
let mut r = ok_resp!(middle.call(req.with_method(Method::Delete)
- .with_path("/api/v1/crates/foo/1.0.0/yank")));
+ .with_path("/api/v1/crates/fyk/1.0.0/yank")));
assert!(::json::(&mut r).ok);
let mut contents = String::new();
File::open(&path).unwrap().read_to_string(&mut contents).unwrap();
assert!(contents.contains("\"yanked\":true"));
let mut r = ok_resp!(middle.call(req.with_method(Method::Get)
- .with_path("/api/v1/crates/foo/1.0.0")));
+ .with_path("/api/v1/crates/fyk/1.0.0")));
assert!(::json::(&mut r).version.yanked);
// un-yank it
let mut r = ok_resp!(middle.call(req.with_method(Method::Put)
- .with_path("/api/v1/crates/foo/1.0.0/unyank")));
+ .with_path("/api/v1/crates/fyk/1.0.0/unyank")));
assert!(::json::(&mut r).ok);
let mut contents = String::new();
File::open(&path).unwrap().read_to_string(&mut contents).unwrap();
assert!(contents.contains("\"yanked\":false"));
let mut r = ok_resp!(middle.call(req.with_method(Method::Get)
- .with_path("/api/v1/crates/foo/1.0.0")));
+ .with_path("/api/v1/crates/fyk/1.0.0")));
assert!(!::json::(&mut r).version.yanked);
}
#[test]
fn yank_not_owner() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Delete, "/api/v1/crates/foo/1.0.0/yank");
+ let mut req = ::req(app, Method::Delete, "/api/v1/crates/foo_not/1.0.0/yank");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_not"));
::mock_user(&mut req, ::user("bar"));
let mut response = ok_resp!(middle.call(&mut req));
::json::<::Bad>(&mut response);
@@ -850,7 +853,7 @@ fn yank_not_owner() {
fn bad_keywords() {
let (_b, app, middle) = ::app();
{
- let krate = ::krate("foo");
+ let krate = ::krate("foo_bad_key");
let kws = vec!["super-long-keyword-name-oh-no".into()];
let mut req = ::new_req_with_keywords(app.clone(), krate, "1.0.0", kws);
::mock_user(&mut req, ::user("foo"));
@@ -858,7 +861,7 @@ fn bad_keywords() {
::json::<::Bad>(&mut response);
}
{
- let krate = ::krate("foo");
+ let krate = ::krate("foo_bad_key2");
let kws = vec!["?@?%".into()];
let mut req = ::new_req_with_keywords(app.clone(), krate, "1.0.0", kws);
::mock_user(&mut req, ::user("foo"));
@@ -866,7 +869,7 @@ fn bad_keywords() {
::json::<::Bad>(&mut response);
}
{
- let krate = ::krate("foo");
+ let krate = ::krate("foo_bad_key_3");
let kws = vec!["?@?%".into()];
let mut req = ::new_req_with_keywords(app.clone(), krate, "1.0.0", kws);
::mock_user(&mut req, ::user("foo"));
@@ -874,7 +877,7 @@ fn bad_keywords() {
::json::<::Bad>(&mut response);
}
{
- let krate = ::krate("foo");
+ let krate = ::krate("foo_bad_key4");
let kws = vec!["áccênts".into()];
let mut req = ::new_req_with_keywords(app.clone(), krate, "1.0.0", kws);
::mock_user(&mut req, ::user("foo"));
@@ -883,6 +886,35 @@ fn bad_keywords() {
}
}
+#[test]
+fn good_categories() {
+ let (_b, app, middle) = ::app();
+ let krate = ::krate("foo_good_cat");
+ let cats = vec!["cat1".into()];
+ let mut req = ::new_req_with_categories(app, krate, "1.0.0", cats);
+ ::mock_category(&mut req, "cat1", "cat1");
+ ::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_good_cat");
+ assert_eq!(json.krate.max_version, "1.0.0");
+ assert_eq!(json.warnings.invalid_categories.len(), 0);
+}
+
+#[test]
+fn ignored_categories() {
+ let (_b, app, middle) = ::app();
+ let krate = ::krate("foo_ignored_cat");
+ let cats = vec!["bar".into()];
+ let mut req = ::new_req_with_categories(app, krate, "1.0.0", cats);
+ ::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_cat");
+ assert_eq!(json.krate.max_version, "1.0.0");
+ assert_eq!(json.warnings.invalid_categories, vec!["bar".to_string()]);
+}
+
#[test]
fn reverse_dependencies() {
let (_b, app, middle) = ::app();
@@ -920,7 +952,7 @@ fn author_license_and_description_required() {
::user("foo");
let mut req = ::req(app, Method::Put, "/api/v1/crates/new");
- let mut new_crate = new_crate("foo");
+ let mut new_crate = new_crate("foo_metadata");
new_crate.license = None;
new_crate.description = None;
new_crate.authors = Vec::new();
diff --git a/src/tests/team.rs b/src/tests/team.rs
index 3e02975b0d..de5b43f41d 100644
--- a/src/tests/team.rs
+++ b/src/tests/team.rs
@@ -29,10 +29,10 @@ fn not_github() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_not_github"));
let body = r#"{"users":["dropbox:foo:foo"]}"#;
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_not_github/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("unknown organization"),
@@ -44,10 +44,10 @@ fn weird_name() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_weird_name"));
let body = r#"{"users":["github:foo/../bar:wut"]}"#;
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_weird_name/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("organization cannot contain"),
@@ -60,10 +60,10 @@ fn one_colon() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_one_colon"));
let body = r#"{"users":["github:foo"]}"#;
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_one_colon/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("missing github team"),
@@ -75,10 +75,10 @@ fn nonexistent_team() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_nonexistent"));
let body = r#"{"users":["github:crates-test-org:this-does-not-exist"]}"#;
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_nonexistent/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("could not find the github team"),
@@ -91,10 +91,10 @@ fn add_team_as_member() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_team_member"));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_team_member/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
}
@@ -105,10 +105,10 @@ fn add_team_as_non_member() {
let (_b, app, middle) = ::app();
let mut req = ::new_req(app, "foo", "2.0.0");
::mock_user(&mut req, mock_user_on_only_x());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_team_non_member"));
let body = body_for_team_y();
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_team_non_member/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("only members"),
@@ -119,22 +119,22 @@ fn add_team_as_non_member() {
#[test]
fn remove_team_as_named_owner() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_remove_team", "1.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_remove_team"));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_remove_team/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_remove_team/owners")
.with_method(Method::Delete)
.with_body(body.as_bytes())));
::mock_user(&mut req, mock_user_on_only_x());
- let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
+ let body = ::new_req_body_version_2(::krate("foo_remove_team"));
let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/new")
.with_body(&body)
.with_method(Method::Put)));
@@ -146,25 +146,25 @@ fn remove_team_as_named_owner() {
#[test]
fn remove_team_as_team_owner() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app, "foo", "1.0.0");
+ let mut req = ::new_req(app, "foo_remove_team_owner", "1.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_remove_team_owner"));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_remove_team_owner/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
::mock_user(&mut req, mock_user_on_only_x());
let body = body_for_team_x();
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_remove_team_owner/owners")
.with_method(Method::Delete)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("don't have permission"),
"{:?}", json.errors);
- let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
+ let body = ::new_req_body_version_2(::krate("foo_remove_team_owner"));
ok_resp!(middle.call(req.with_path("/api/v1/crates/new")
.with_body(&body)
.with_method(Method::Put)));
@@ -175,17 +175,17 @@ fn remove_team_as_team_owner() {
fn publish_not_owned() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app.clone(), "foo", "1.0.0");
+ let mut req = ::new_req(app.clone(), "foo_not_owned", "1.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_not_owned"));
let body = body_for_team_y();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_not_owned/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
::mock_user(&mut req, mock_user_on_only_x());
- let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
+ let body = ::new_req_body_version_2(::krate("foo_not_owned"));
let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/new")
.with_body(&body)
.with_method(Method::Put)));
@@ -197,17 +197,17 @@ fn publish_not_owned() {
#[test]
fn publish_owned() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app.clone(), "foo", "1.0.0");
+ let mut req = ::new_req(app.clone(), "foo_team_owned", "1.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_team_owned"));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_team_owned/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
::mock_user(&mut req, mock_user_on_only_x());
- let body = ::new_req_body(::krate("foo"), "2.0.0", Vec::new(), Vec::new());
+ let body = ::new_req_body_version_2(::krate("foo_team_owned"));
ok_resp!(middle.call(req.with_path("/api/v1/crates/new")
.with_body(&body)
.with_method(Method::Put)));
@@ -217,18 +217,18 @@ fn publish_owned() {
#[test]
fn add_owners_as_team_owner() {
let (_b, app, middle) = ::app();
- let mut req = ::new_req(app.clone(), "foo", "1.0.0");
+ let mut req = ::new_req(app.clone(), "foo_add_owner", "1.0.0");
::mock_user(&mut req, mock_user_on_x_and_y());
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_add_owner"));
let body = body_for_team_x();
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_add_owner/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
::mock_user(&mut req, mock_user_on_only_x());
let body = r#"{"users":["FlashCat"]}"#; // User doesn't matter
- let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo/owners")
+ let json = bad_resp!(middle.call(req.with_path("/api/v1/crates/foo_add_owner/owners")
.with_method(Method::Put)
.with_body(body.as_bytes())));
assert!(json.errors[0].detail.contains("don't have permission"),
diff --git a/src/tests/user.rs b/src/tests/user.rs
index c1b9017d82..e10a8ddd42 100644
--- a/src/tests/user.rs
+++ b/src/tests/user.rs
@@ -91,7 +91,7 @@ fn my_packages() {
let (_b, app, middle) = ::app();
let mut req = ::req(app, Method::Get, "/api/v1/crates");
let u = ::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_my_packages"));
req.with_query(&format!("user_id={}", u.id));
let mut response = ok_resp!(middle.call(&mut req));
@@ -113,8 +113,8 @@ fn following() {
let (_b, app, middle) = ::app();
let mut req = ::req(app, Method::Get, "/");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
- ::mock_crate(&mut req, ::krate("bar"));
+ ::mock_crate(&mut req, ::krate("foo_fighters"));
+ ::mock_crate(&mut req, ::krate("bar_fighters"));
let mut response = ok_resp!(middle.call(req.with_path("/me/updates")
.with_method(Method::Get)));
@@ -122,9 +122,9 @@ fn following() {
assert_eq!(r.versions.len(), 0);
assert_eq!(r.meta.more, false);
- ok_resp!(middle.call(req.with_path("/api/v1/crates/foo/follow")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/foo_fighters/follow")
.with_method(Method::Put)));
- ok_resp!(middle.call(req.with_path("/api/v1/crates/bar/follow")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/bar_fighters/follow")
.with_method(Method::Put)));
let mut response = ok_resp!(middle.call(req.with_path("/me/updates")
@@ -140,7 +140,7 @@ fn following() {
assert_eq!(r.versions.len(), 1);
assert_eq!(r.meta.more, true);
- ok_resp!(middle.call(req.with_path("/api/v1/crates/bar/follow")
+ ok_resp!(middle.call(req.with_path("/api/v1/crates/bar_fighters/follow")
.with_method(Method::Delete)));
let mut response = ok_resp!(middle.call(req.with_path("/me/updates")
.with_method(Method::Get)
diff --git a/src/tests/version.rs b/src/tests/version.rs
index 8ceabbb440..5e4b13c7de 100644
--- a/src/tests/version.rs
+++ b/src/tests/version.rs
@@ -26,7 +26,7 @@ fn index() {
let (v1, v2) = {
::mock_user(&mut req, ::user("foo"));
- let (c, _) = ::mock_crate(&mut req, ::krate("foo"));
+ let (c, _) = ::mock_crate(&mut req, ::krate("foo_vers_index"));
let req: &mut Request = &mut req;
let tx = req.tx().unwrap();
let m = HashMap::new();
@@ -46,7 +46,7 @@ fn show() {
let mut req = ::req(app, Method::Get, "/api/v1/versions");
let v = {
::mock_user(&mut req, ::user("foo"));
- let (krate, _) = ::mock_crate(&mut req, ::krate("foo"));
+ let (krate, _) = ::mock_crate(&mut req, ::krate("foo_vers_show"));
let req: &mut Request = &mut req;
let tx = req.tx().unwrap();
Version::insert(tx, krate.id, &sv("2.0.0"), &HashMap::new(), &[]).unwrap()
@@ -60,9 +60,9 @@ fn show() {
#[test]
fn authors() {
let (_b, app, middle) = ::app();
- let mut req = ::req(app, Method::Get, "/api/v1/crates/foo/1.0.0/authors");
+ let mut req = ::req(app, Method::Get, "/api/v1/crates/foo_authors/1.0.0/authors");
::mock_user(&mut req, ::user("foo"));
- ::mock_crate(&mut req, ::krate("foo"));
+ ::mock_crate(&mut req, ::krate("foo_authors"));
let mut response = ok_resp!(middle.call(&mut req));
let mut s = String::new();
response.body.read_to_string(&mut s).unwrap();
diff --git a/src/upload.rs b/src/upload.rs
index 1759d5da11..72a8ee32ba 100644
--- a/src/upload.rs
+++ b/src/upload.rs
@@ -20,6 +20,7 @@ pub struct NewCrate {
pub documentation: Option,
pub readme: Option,
pub keywords: Option,
+ pub categories: Option,
pub license: Option,
pub license_file: Option,
pub repository: Option,
@@ -31,6 +32,8 @@ pub struct CrateVersion(pub semver::Version);
pub struct CrateVersionReq(pub semver::VersionReq);
pub struct KeywordList(pub Vec);
pub struct Keyword(pub String);
+pub struct CategoryList(pub Vec);
+pub struct Category(pub String);
pub struct Feature(pub String);
#[derive(RustcDecodable, RustcEncodable)]
@@ -64,6 +67,12 @@ impl Decodable for Keyword {
}
}
+impl Decodable for Category {
+ fn decode(d: &mut D) -> Result {
+ d.read_str().map(Category)
+ }
+}
+
impl Decodable for Feature {
fn decode(d: &mut D) -> Result {
let s = try!(d.read_str());
@@ -110,6 +119,16 @@ impl Decodable for KeywordList {
}
}
+impl Decodable for CategoryList {
+ fn decode(d: &mut D) -> Result {
+ let inner: Vec = try!(Decodable::decode(d));
+ if inner.len() > 5 {
+ return Err(d.error("a maximum of 5 categories per crate are allowed"))
+ }
+ Ok(CategoryList(inner))
+ }
+}
+
impl Decodable for DependencyKind {
fn decode(d: &mut D) -> Result {
let s: String = try!(Decodable::decode(d));
@@ -135,6 +154,12 @@ impl Encodable for Keyword {
}
}
+impl Encodable for Category {
+ fn encode(&self, d: &mut E) -> Result<(), E::Error> {
+ d.emit_str(self)
+ }
+}
+
impl Encodable for Feature {
fn encode(&self, d: &mut E) -> Result<(), E::Error> {
d.emit_str(self)
@@ -160,6 +185,13 @@ impl Encodable for KeywordList {
}
}
+impl Encodable for CategoryList {
+ fn encode(&self, d: &mut E) -> Result<(), E::Error> {
+ let CategoryList(ref inner) = *self;
+ inner.encode(d)
+ }
+}
+
impl Encodable for DependencyKind {
fn encode(&self, d: &mut E) -> Result<(), E::Error> {
match *self {
@@ -180,6 +212,11 @@ impl Deref for Keyword {
fn deref(&self) -> &str { &self.0 }
}
+impl Deref for Category {
+ type Target = str;
+ fn deref(&self) -> &str { &self.0 }
+}
+
impl Deref for Feature {
type Target = str;
fn deref(&self) -> &str { &self.0 }
@@ -203,3 +240,8 @@ impl Deref for KeywordList {
type Target = [Keyword];
fn deref(&self) -> &[Keyword] { &self.0 }
}
+
+impl Deref for CategoryList {
+ type Target = [Category];
+ fn deref(&self) -> &[Category] { &self.0 }
+}
diff --git a/src/user/mod.rs b/src/user/mod.rs
index 66d20efa08..1b3ea1853d 100644
--- a/src/user/mod.rs
+++ b/src/user/mod.rs
@@ -336,7 +336,7 @@ pub fn updates(req: &mut Request) -> CargoResult {
}
// Encode everything!
- let crates = crates.into_iter().map(|c| c.encodable(None, None)).collect();
+ let crates = crates.into_iter().map(|c| c.minimal_encodable()).collect();
let versions = versions.into_iter().map(|v| {
let id = v.crate_id;
v.encodable(&map[&id])