diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bcc79..070369c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +### Features + +* Query registries other than crates-io + - Additional `AsyncClient::build()` and `SyncClient::build()` functions. + For building a client for an alternative registry. + +### (Breaking) Changes + +* `AsyncClient::with_http_client()` now requires the crate registry url to be specified. +* Types, make field optional: User {url} + ## 0.8.1 * Add `AsyncClient::with_http_client` constructor @@ -10,7 +21,7 @@ ## 0.8.0 - 2022-01-29 -This version has quite a few breaking changes, +This version has quite a few breaking changes, mainly to clean up and future-proof the API. ### Features @@ -95,7 +106,7 @@ mainly to clean up and future-proof the API. * Crate {recent_downloads, exact_match} * CrateResponse {versions, keywords, categories} * Version {crate_size, published_by} -* Make field optional: User {kind} +* Make field optional: User {kind} * Fix getting the reverse dependencies. * Rearrange the received data for simpler manipulation. * Add 3 new types: diff --git a/src/async_client.rs b/src/async_client.rs index 30cf273..7182b62 100644 --- a/src/async_client.rs +++ b/src/async_client.rs @@ -1,14 +1,14 @@ use futures::future::BoxFuture; use futures::prelude::*; use futures::{future::try_join_all, try_join}; -use reqwest::{header, Client as HttpClient, StatusCode, Url}; +use reqwest::{Client as HttpClient, StatusCode, Url}; use serde::de::DeserializeOwned; use std::collections::VecDeque; use super::Error; use crate::error::JsonDecodeError; -use crate::types::*; +use crate::{helper::*, types::*}; /// Asynchronous client for the crates.io API. #[derive(Clone)] @@ -124,21 +124,45 @@ impl Client { user_agent: &str, rate_limit: std::time::Duration, ) -> Result { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::USER_AGENT, - header::HeaderValue::from_str(user_agent)?, - ); + Self::build(user_agent, rate_limit, None) + } + + /// Build a new client. + /// + /// Returns an [`Error`] if the given user agent is invalid. + /// ```rust + /// use crates_io_api::{AsyncClient,Registry}; + /// # fn f() -> Result<(), Box> { + /// let client = crates_io_api::AsyncClient::build( + /// "my_bot (help@my_bot.com)", + /// std::time::Duration::from_millis(1000), + /// Some(&Registry{ + /// url: "https://crates.my-registry.com/api/v1/".to_string(), + /// name: Some("my_registry".to_string()), + /// token: None, + /// }), + /// ).unwrap(); + /// # Ok(()) + /// # } + /// ``` + pub fn build( + user_agent: &str, + rate_limit: std::time::Duration, + registry: Option<&Registry>, + ) -> Result { + let headers = setup_headers(user_agent, registry)?; let client = HttpClient::builder() .default_headers(headers) .build() .unwrap(); - Ok(Self::with_http_client(client, rate_limit)) + let base_url = base_url(registry); + + Ok(Self::with_http_client(client, rate_limit, base_url)) } - /// Instantiate a new client. + /// Instantiate a new client, for the registry sepcified by base_url. /// /// To respect the offical [Crawler Policy](https://crates.io/policies#crawlers), /// you must specify both a descriptive user agent and a rate limit interval. @@ -146,14 +170,18 @@ impl Client { /// At most one request will be executed in the specified duration. /// The guidelines suggest 1 per second or less. /// (Only one request is executed concurrenly, even if the given Duration is 0). - pub fn with_http_client(client: HttpClient, rate_limit: std::time::Duration) -> Self { + pub fn with_http_client( + client: HttpClient, + rate_limit: std::time::Duration, + base_url: &str, + ) -> Self { let limiter = std::sync::Arc::new(tokio::sync::Mutex::new(None)); Self { rate_limit, last_request_time: limiter, client, - base_url: Url::parse("https://crates.io/api/v1/").unwrap(), + base_url: Url::parse(base_url).unwrap(), } } diff --git a/src/helper.rs b/src/helper.rs new file mode 100644 index 0000000..2b78e41 --- /dev/null +++ b/src/helper.rs @@ -0,0 +1,138 @@ +//! Helper functions for querying crate registries + +use crate::types::*; +use reqwest::header; +use std::env; + +/// Setup the headers for a sync or async request +pub fn setup_headers( + user_agent: &str, + registry: Option<&Registry>, +) -> Result { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(user_agent)?, + ); + + match ®istry { + Some(registry) => match ®istry.name { + Some(name) => { + if let Ok(token) = + env::var(format!("CARGO_REGISTRIES_{}_TOKEN", name.to_uppercase())) + { + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&token)?, + ); + } + } + None => match ®istry.token { + Some(token) => { + headers.insert(header::AUTHORIZATION, header::HeaderValue::from_str(token)?); + } + None => (), + }, + }, + None => (), + } + + Ok(headers) +} + +/// Determine the url of the crate registry being queried. +pub fn base_url(registry: Option<&Registry>) -> &str { + match registry { + Some(reg) => reg.url.as_str(), + None => "https://crates.io/api/v1/", + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Error; + + #[test] + fn test_base_url_default() -> Result<(), Error> { + assert_eq!(base_url(None), "https://crates.io/api/v1/"); + Ok(()) + } + + #[test] + fn test_base_url_private() -> Result<(), Error> { + let reg = &Registry { + url: "https://crates.foobar.com/api/v1/".to_string(), + name: None, + token: None, + }; + assert_eq!(base_url(Some(reg)), "https://crates.foobar.com/api/v1/"); + Ok(()) + } + + #[test] + fn test_crates_io_headers() -> Result<(), Error> { + let reg = None; + let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)"; + let headers = setup_headers(user_agent, reg).unwrap(); + + let mut exp_headers = header::HeaderMap::new(); + exp_headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(user_agent).unwrap(), + ); + + assert_eq!(headers, exp_headers); + Ok(()) + } + + #[test] + fn test_private_registry_name_headers() -> Result<(), Error> { + let reg = &Registry { + url: "https://crates.foobar.com/api/v1/".to_string(), + name: Some("foobar".to_string()), + token: None, + }; + env::set_var("CARGO_REGISTRIES_FOOBAR_TOKEN", "baz"); + let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)"; + let headers = setup_headers(user_agent, Some(reg)).unwrap(); + + let mut exp_headers = header::HeaderMap::new(); + exp_headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(user_agent).unwrap(), + ); + exp_headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str("baz").unwrap(), + ); + + assert_eq!(headers, exp_headers); + Ok(()) + } + + #[test] + fn test_private_registry_token_headers() -> Result<(), Error> { + let reg = &Registry { + url: "https://crates.foobar.com/api/v1/".to_string(), + name: None, + token: Some("foobar".to_string()), + }; + env::set_var("CARGO_REGISTRIES_FOOBAR_TOKEN", "baz"); + let user_agent = "crates-io-api-continuous-integration (github.com/theduke/crates-io-api)"; + let headers = setup_headers(user_agent, Some(reg)).unwrap(); + + let mut exp_headers = header::HeaderMap::new(); + exp_headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(user_agent).unwrap(), + ); + exp_headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str("foobar").unwrap(), + ); + + assert_eq!(headers, exp_headers); + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index b304da1..d77eb98 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,18 +40,29 @@ //! Ok(()) //! } //! ``` +//! Instantiate a client for a private registry with environment variable authentication +//! +//! ```rust +//! use crates_io_api::{SyncClient,Registry}; +//! let client = SyncClient::new( +//! "my-user-agent (my-contact@domain.com)", +//! std::time::Duration::from_millis(1000), +//! ).unwrap(); +//! ``` #![recursion_limit = "128"] #![deny(missing_docs)] mod async_client; mod error; +mod helper; mod sync_client; mod types; pub use crate::{ async_client::Client as AsyncClient, error::{Error, NotFoundError, PermissionDeniedError}, + helper::*, sync_client::SyncClient, types::*, }; diff --git a/src/sync_client.rs b/src/sync_client.rs index 5e4535d..e4305e0 100644 --- a/src/sync_client.rs +++ b/src/sync_client.rs @@ -2,10 +2,10 @@ use super::*; use std::iter::Extend; use log::trace; -use reqwest::{blocking::Client as HttpClient, header, StatusCode, Url}; +use reqwest::{blocking::Client as HttpClient, StatusCode, Url}; use serde::de::DeserializeOwned; -use crate::{error::JsonDecodeError, types::*}; +use crate::{error::JsonDecodeError, helper::*, types::*}; /// A synchronous client for the crates.io API. pub struct SyncClient { @@ -41,18 +41,38 @@ impl SyncClient { user_agent: &str, rate_limit: std::time::Duration, ) -> Result { - let mut headers = header::HeaderMap::new(); - headers.insert( - header::USER_AGENT, - header::HeaderValue::from_str(user_agent)?, - ); + Self::build(user_agent, rate_limit, None) + } + + /// ```rust + /// use crates_io_api::{SyncClient,Registry}; + /// # fn f() -> Result<(), Box> { + /// let client = crates_io_api::SyncClient::build( + /// "my_bot (help@my_bot.com)", + /// std::time::Duration::from_millis(1000), + /// Some(&Registry{ + /// url: "https://crates.my-registry.com/api/v1/".to_string(), + /// name: Some("my_registry".to_string()), + /// token: None, + /// }), + /// ).unwrap(); + /// # Ok(()) + /// # } + /// ``` + pub fn build( + user_agent: &str, + rate_limit: std::time::Duration, + registry: Option<&Registry>, + ) -> Result { + let headers = setup_headers(user_agent, registry)?; + let base_url = base_url(registry); Ok(Self { client: HttpClient::builder() .default_headers(headers) .build() .unwrap(), - base_url: Url::parse("https://crates.io/api/v1/").unwrap(), + base_url: Url::parse(base_url).unwrap(), rate_limit, last_request_time: std::sync::Mutex::new(None), }) diff --git a/src/types.rs b/src/types.rs index 5dff9e9..297972c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,6 +4,16 @@ use chrono::{DateTime, NaiveDate, Utc}; use serde_derive::*; use std::collections::HashMap; +/// Used to specify the registry being queried by either client. +pub struct Registry { + /// Url of the registry + pub url: String, + /// Name of the registry + pub name: Option, + /// Token used to authenticate registry requests. + pub token: Option, +} + /// Used to specify the sort behaviour of the `Client::crates()` method. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct ApiErrors { @@ -432,7 +442,7 @@ pub struct User { pub kind: Option, pub login: String, pub name: Option, - pub url: String, + pub url: Option, } /// Additional crate author metadata.