1mod api_calls;
21mod request;
22pub mod structures;
23mod url_ext;
24
25pub use api_calls::{check_id_slug, check_sha1_hash};
26
27use reqwest::{
28 header::{HeaderMap, HeaderValue, InvalidHeaderValue},
29 Client,
30};
31use std::{marker::PhantomData, sync::LazyLock};
32use url::Url;
33
34pub static BASE_URL: LazyLock<Url> =
36 LazyLock::new(|| Url::parse("https://api.modrinth.com/").expect("Invalid base URL"));
37
38pub static API_BASE_URL: LazyLock<Url> = LazyLock::new(|| {
40 BASE_URL
41 .join(concat!('v', env!("CARGO_PKG_VERSION_MAJOR"), '/'))
42 .expect("Invalid API base URL")
43});
44
45#[derive(thiserror::Error, Debug)]
46#[error(transparent)]
47pub enum Error {
48 #[error("Invalid Modrinth ID or slug")]
49 InvalidIDorSlug,
50 #[error("Invalid SHA1 hash")]
51 InvalidSHA1,
52 #[error("You have been rate limited, please wait for {0} seconds")]
53 RateLimitExceeded(usize),
54 #[error("The API at {} is deprecated", *API_BASE_URL)]
55 ApiDeprecated,
56 ReqwestError(#[from] reqwest::Error),
57 JSONError(#[from] serde_json::Error),
58 InvalidHeaderValue(#[from] InvalidHeaderValue),
59}
60pub type Result<T> = std::result::Result<T, Error>;
61
62#[derive(Debug, Clone)]
86pub struct Ferinth<Auth> {
87 client: Client,
88 auth: PhantomData<Auth>,
89}
90pub struct Authenticated;
91
92impl Default for Ferinth<()> {
93 fn default() -> Self {
94 Self {
95 client: Client::builder()
96 .user_agent(concat!(
97 env!("CARGO_CRATE_NAME"),
98 "/",
99 env!("CARGO_PKG_VERSION")
100 ))
101 .build()
102 .expect("Failed to initialise TLS backend"),
103 auth: PhantomData,
104 }
105 }
106}
107
108impl<T> Ferinth<T> {
109 fn client_builder(
110 name: &str,
111 version: Option<&str>,
112 contact: Option<&str>,
113 ) -> reqwest::ClientBuilder {
114 Client::builder().user_agent(format!(
115 "{}{}{}",
116 name,
117 version.map_or("".into(), |version| format!("/{}", version)),
118 contact.map_or("".into(), |contact| format!(" ({})", contact))
119 ))
120 }
121}
122
123impl Ferinth<()> {
124 pub fn new(name: &str, version: Option<&str>, contact: Option<&str>) -> Self {
131 Self {
132 auth: PhantomData,
133 client: Self::client_builder(name, version, contact)
134 .build()
135 .expect("Failed to initialise TLS backend"),
136 }
137 }
138}
139
140impl Ferinth<Authenticated> {
141 pub fn new<V>(
151 name: &str,
152 version: Option<&str>,
153 contact: Option<&str>,
154 token: V,
155 ) -> Result<Self>
156 where
157 V: TryInto<HeaderValue>,
158 Error: From<V::Error>,
159 {
160 Ok(Self {
161 auth: PhantomData,
162 client: Self::client_builder(name, version, contact)
163 .default_headers(HeaderMap::from_iter([(
164 reqwest::header::AUTHORIZATION,
165 token.try_into()?,
166 )]))
167 .build()
168 .expect("Failed to initialise TLS backend"),
169 })
170 }
171}