diff --git a/src/cargo/core/manifest.rs b/src/cargo/core/manifest.rs index 204d55a1ea6..d03cf688b93 100644 --- a/src/cargo/core/manifest.rs +++ b/src/cargo/core/manifest.rs @@ -47,6 +47,7 @@ pub struct VirtualManifest { pub struct ManifestMetadata { pub authors: Vec, pub keywords: Vec, + pub categories: Vec, pub license: Option, pub license_file: Option, pub description: Option, // not markdown diff --git a/src/cargo/ops/registry.rs b/src/cargo/ops/registry.rs index 58b2894cd8b..2397f8204a9 100644 --- a/src/cargo/ops/registry.rs +++ b/src/cargo/ops/registry.rs @@ -113,6 +113,7 @@ fn transmit(config: &Config, let ManifestMetadata { ref authors, ref description, ref homepage, ref documentation, ref keywords, ref readme, ref repository, ref license, ref license_file, + ref categories, } = *manifest.metadata(); let readme = match *readme { Some(ref readme) => Some(paths::read(&pkg.root().join(readme))?), @@ -133,7 +134,7 @@ fn transmit(config: &Config, return Ok(()); } - registry.publish(&NewCrate { + let publish = registry.publish(&NewCrate { name: pkg.name().to_string(), vers: pkg.version().to_string(), deps: deps, @@ -143,13 +144,27 @@ fn transmit(config: &Config, homepage: homepage.clone(), documentation: documentation.clone(), keywords: keywords.clone(), + categories: categories.clone(), readme: readme, repository: repository.clone(), license: license.clone(), license_file: license_file.clone(), - }, tarball).map_err(|e| { - human(e.to_string()) - }) + }, tarball); + + match publish { + Ok(warnings) => { + if !warnings.invalid_categories.is_empty() { + let msg = format!("\ + the following are not valid category slugs and were \ + ignored: {}. Please see https://crates.io/category_slugs \ + for the list of all category slugs. \ + ", warnings.invalid_categories.join(", ")); + config.shell().warn(&msg)?; + } + Ok(()) + }, + Err(e) => Err(human(e.to_string())), + } } pub fn registry_configuration(config: &Config) -> CargoResult { diff --git a/src/cargo/util/toml.rs b/src/cargo/util/toml.rs index 3bcbe7c9550..8abbaa2f183 100644 --- a/src/cargo/util/toml.rs +++ b/src/cargo/util/toml.rs @@ -317,6 +317,7 @@ pub struct TomlProject { documentation: Option, readme: Option, keywords: Option>, + categories: Option>, license: Option, license_file: Option, repository: Option, @@ -654,6 +655,7 @@ impl TomlManifest { license_file: project.license_file.clone(), repository: project.repository.clone(), keywords: project.keywords.clone().unwrap_or(Vec::new()), + categories: project.categories.clone().unwrap_or(Vec::new()), }; let workspace_config = match (self.workspace.as_ref(), diff --git a/src/crates-io/lib.rs b/src/crates-io/lib.rs index 884460df290..00637be3dc2 100644 --- a/src/crates-io/lib.rs +++ b/src/crates-io/lib.rs @@ -10,7 +10,7 @@ use std::io::{self, Cursor}; use std::result; use curl::easy::{Easy, List}; -use rustc_serialize::json; +use rustc_serialize::json::{self, Json}; use url::percent_encoding::{percent_encode, QUERY_ENCODE_SET}; @@ -39,6 +39,7 @@ pub enum Error { NotFound, JsonEncodeError(json::EncoderError), JsonDecodeError(json::DecoderError), + JsonParseError(json::ParserError), } impl From for Error { @@ -53,6 +54,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: json::ParserError) -> Error { + Error::JsonParseError(err) + } +} + impl From for Error { fn from(err: curl::Error) -> Error { Error::Curl(err) @@ -78,6 +85,7 @@ pub struct NewCrate { pub homepage: Option, pub readme: Option, pub keywords: Vec, + pub categories: Vec, pub license: Option, pub license_file: Option, pub repository: Option, @@ -103,6 +111,10 @@ pub struct User { pub name: Option, } +pub struct Warnings { + pub invalid_categories: Vec, +} + #[derive(RustcDecodable)] struct R { ok: bool } #[derive(RustcDecodable)] struct ApiErrorList { errors: Vec } #[derive(RustcDecodable)] struct ApiError { detail: String } @@ -110,7 +122,6 @@ pub struct User { #[derive(RustcDecodable)] struct Users { users: Vec } #[derive(RustcDecodable)] struct TotalCrates { total: u32 } #[derive(RustcDecodable)] struct Crates { crates: Vec, meta: TotalCrates } - impl Registry { pub fn new(host: String, token: Option) -> Registry { Registry::new_handle(host, token, Easy::new()) @@ -147,7 +158,8 @@ impl Registry { Ok(json::decode::(&body)?.users) } - pub fn publish(&mut self, krate: &NewCrate, tarball: &File) -> Result<()> { + pub fn publish(&mut self, krate: &NewCrate, tarball: &File) + -> Result { let json = json::encode(krate)?; // Prepare the body. The format of the upload request is: // @@ -190,10 +202,24 @@ impl Registry { headers.append(&format!("Authorization: {}", token))?; self.handle.http_headers(headers)?; - let _body = handle(&mut self.handle, &mut |buf| { + let body = handle(&mut self.handle, &mut |buf| { body.read(buf).unwrap_or(0) })?; - Ok(()) + // Can't derive RustcDecodable because JSON has a key named "crate" :( + let response = if body.len() > 0 { + Json::from_str(&body)? + } else { + Json::from_str("{}")? + }; + let invalid_categories: Vec = + response + .find_path(&["warnings", "invalid_categories"]) + .and_then(Json::as_array) + .map(|x| { + x.iter().flat_map(Json::as_string).map(Into::into).collect() + }) + .unwrap_or_else(Vec::new); + Ok(Warnings { invalid_categories: invalid_categories }) } pub fn search(&mut self, query: &str, limit: u8) -> Result<(Vec, u32)> { @@ -328,6 +354,7 @@ impl fmt::Display for Error { Error::NotFound => write!(f, "cannot find crate"), Error::JsonEncodeError(ref e) => write!(f, "json encode error: {}", e), Error::JsonDecodeError(ref e) => write!(f, "json decode error: {}", e), + Error::JsonParseError(ref e) => write!(f, "json parse error: {}", e), } } } diff --git a/src/doc/manifest.md b/src/doc/manifest.md index ccdc6c3cc67..09fa5e04e9f 100644 --- a/src/doc/manifest.md +++ b/src/doc/manifest.md @@ -118,10 +118,16 @@ repository = "..." # contents of this file are stored and indexed in the registry. readme = "..." -# This is a small list of keywords used to categorize and search for this -# package. +# This is a list of up to five keywords that describe this crate. Keywords +# are searchable on crates.io, and you may choose any words that would +# help someone find this crate. keywords = ["...", "..."] +# This is a list of up to five categories where this crate would fit. +# Categories are a fixed list available at crates.io/categories, and +# they must match exactly. +categories = ["...", "..."] + # This is a string description of the license for this package. Currently # crates.io will validate the license provided against a whitelist of known # license identifiers from http://spdx.org/licenses/. Multiple licenses can be