mod api_calls;
mod request;
pub mod structures;
mod url_ext;
pub use api_calls::{check_id_slug, check_sha1_hash};
use reqwest::{
header::{HeaderMap, HeaderValue, InvalidHeaderValue},
Client,
};
use std::{marker::PhantomData, sync::LazyLock};
use url::Url;
pub static BASE_URL: LazyLock<Url> =
LazyLock::new(|| Url::parse("https://api.modrinth.com/").expect("Invalid base URL"));
pub static API_BASE_URL: LazyLock<Url> = LazyLock::new(|| {
BASE_URL
.join(concat!('v', env!("CARGO_PKG_VERSION_MAJOR"), '/'))
.expect("Invalid API base URL")
});
#[derive(thiserror::Error, Debug)]
#[error(transparent)]
pub enum Error {
#[error("Invalid Modrinth ID or slug")]
InvalidIDorSlug,
#[error("Invalid SHA1 hash")]
InvalidSHA1,
#[error("You have been rate limited, please wait for {0} seconds")]
RateLimitExceeded(usize),
#[error("The API at {} is deprecated", *API_BASE_URL)]
ApiDeprecated,
ReqwestError(#[from] reqwest::Error),
JSONError(#[from] serde_json::Error),
InvalidHeaderValue(#[from] InvalidHeaderValue),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub struct Ferinth<Auth> {
client: Client,
auth: PhantomData<Auth>,
}
pub struct Authenticated;
impl Default for Ferinth<()> {
fn default() -> Self {
Self {
client: Client::builder()
.user_agent(concat!(
env!("CARGO_CRATE_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()
.expect("Failed to initialise TLS backend"),
auth: PhantomData,
}
}
}
impl<T> Ferinth<T> {
fn client_builder(
name: &str,
version: Option<&str>,
contact: Option<&str>,
) -> reqwest::ClientBuilder {
Client::builder().user_agent(format!(
"{}{}{}",
name,
version.map_or("".into(), |version| format!("/{}", version)),
contact.map_or("".into(), |contact| format!(" ({})", contact))
))
}
}
impl Ferinth<()> {
pub fn new(name: &str, version: Option<&str>, contact: Option<&str>) -> Self {
Self {
auth: PhantomData,
client: Self::client_builder(name, version, contact)
.build()
.expect("Failed to initialise TLS backend"),
}
}
}
impl Ferinth<Authenticated> {
pub fn new<V>(
name: &str,
version: Option<&str>,
contact: Option<&str>,
token: V,
) -> Result<Self>
where
V: TryInto<HeaderValue>,
Error: From<V::Error>,
{
Ok(Self {
auth: PhantomData,
client: Self::client_builder(name, version, contact)
.default_headers(HeaderMap::from_iter([(
reqwest::header::AUTHORIZATION,
token.try_into()?,
)]))
.build()
.expect("Failed to initialise TLS backend"),
})
}
}