ferinth/
lib.rs

1/*!
2# Ferinth
3
4Ferinth provides Rust API bindings for the [Modrinth API](https://docs.modrinth.com)
5
6## Missing Features
7
8- Search functionality
9- Requests that require large body data
10- Better organisation of API calls
11
12## Versioning
13
14The major version of this crate's version directly corresponds to the Modrinth API version it uses.
15If you want to use the Modrinth API version 2, which is the latest one currently, specify this crate's major version as `2`.
16
17Due to this feature, there will be breaking changes in minor version bumps too!
18*/
19
20mod 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
34/// The base URL for the Modrinth API
35pub static BASE_URL: LazyLock<Url> =
36    LazyLock::new(|| Url::parse("https://api.modrinth.com/").expect("Invalid base URL"));
37
38/// The base URL for the current version of the Modrinth API
39pub 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/**
63An instance of the API to invoke API calls on
64
65There are two methods initialise this container:
66
67Use the `Default` implementation to set the user agent based on the crate name and version.
68This container will not have authentication.
69
70```ignore
71let modrinth = ferinth::Ferinth::default();
72```
73
74Use the `new()` function to set a custom user agent and authentication token.
75
76```ignore
77let modrinth = ferinth::Ferinth::new(
78    env!("CARGO_CRATE_NAME"),
79    Some(env!("CARGO_PKG_VERSION")),
80    Some("contact@program.com"),
81    args.modrinth_token.as_ref(),
82)?;
83```
84*/
85#[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    /**
125    Instantiate the container with the provided
126    [user agent](https://docs.modrinth.com/api-spec/#section/User-Agents) details.
127
128    The program `name` is required; `version` and `contact` are optional but recommended.
129    */
130    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    /*
142    Instantiate the container with the provided
143    [user agent](https://docs.modrinth.com/api-spec/#section/User-Agents) details,
144    and authentication `token`.
145
146    The program `name` is required; `version` and `contact` are optional but recommended.
147
148    Fails if the provided `token` cannot be converted into a `HeaderValue`.
149    */
150    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}