Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support alternative crate registries #58

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
50 changes: 39 additions & 11 deletions src/async_client.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -124,36 +124,64 @@ impl Client {
user_agent: &str,
rate_limit: std::time::Duration,
) -> Result<Self, reqwest::header::InvalidHeaderValue> {
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<dyn std::error::Error>> {
/// 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<Self, reqwest::header::InvalidHeaderValue> {
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.
///
/// 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(),
}
}

Expand Down
138 changes: 138 additions & 0 deletions src/helper.rs
Original file line number Diff line number Diff line change
@@ -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<header::HeaderMap, header::InvalidHeaderValue> {
let mut headers = header::HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_str(user_agent)?,
);

match &registry {
Some(registry) => match &registry.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 &registry.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(())
}
}
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
};
36 changes: 28 additions & 8 deletions src/sync_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -41,18 +41,38 @@ impl SyncClient {
user_agent: &str,
rate_limit: std::time::Duration,
) -> Result<Self, reqwest::header::InvalidHeaderValue> {
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<dyn std::error::Error>> {
/// 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<Self, reqwest::header::InvalidHeaderValue> {
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),
})
Expand Down
12 changes: 11 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Token used to authenticate registry requests.
pub token: Option<String>,
}

/// Used to specify the sort behaviour of the `Client::crates()` method.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct ApiErrors {
Expand Down Expand Up @@ -432,7 +442,7 @@ pub struct User {
pub kind: Option<String>,
pub login: String,
pub name: Option<String>,
pub url: String,
pub url: Option<String>,
}

/// Additional crate author metadata.
Expand Down