1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
/*!
# Ferinth

Ferinth provides Rust API bindings for the [Modrinth API](https://docs.modrinth.com)

## Missing Features

- Search functionality
- Requests that require large body data
- Better organisation of API calls

## Versioning

The major version of this crate's version directly corresponds to the Modrinth API version it uses.
If you want to use the Modrinth API version 2, which is the latest one currently, specify this crate's major version as `2`.

Due to this feature, there will be breaking changes in minor version bumps too!
*/

mod api_calls;
mod request;
pub mod structures;
mod url_ext;

pub use api_calls::{check_id_slug, check_sha1_hash};

use once_cell::sync::Lazy;
use reqwest::{header, Client};
use url::Url;

/// The base URL for the Modrinth API
pub static BASE_URL: Lazy<Url> =
    Lazy::new(|| Url::parse("https://api.modrinth.com/").expect("Invalid base URL"));

/// The base URL for the current version of the Modrinth API
pub static API_BASE_URL: Lazy<Url> = Lazy::new(|| {
    BASE_URL
        .join(concat!('v', env!("CARGO_PKG_VERSION_MAJOR"), '/'))
        .expect("Invalid API base URL")
});

#[derive(thiserror::Error, Debug)]
#[error("{}", .0)]
pub enum Error {
    #[error("Invalid Modrinth ID or slug")]
    InvalidIDorSlug,
    #[error("Invalid SHA1 hash")]
    InvalidSHA1,
    #[error("You have been rate limited, please wait for {} seconds", .0)]
    RateLimitExceeded(usize),
    ReqwestError(#[from] reqwest::Error),
    JSONError(#[from] serde_json::Error),
    InvalidHeaderValue(#[from] header::InvalidHeaderValue),
}
pub type Result<T> = std::result::Result<T, Error>;

/**
An instance of the API to invoke API calls on

There are two methods initialise this container:

Use the `Default` implementation to set the user agent based on the crate name and version.
This container will not have authentication.

```ignore
let modrinth = ferinth::Ferinth::default();
```

Use the `new()` function to set a custom user agent and authentication token.

```ignore
let modrinth = ferinth::Ferinth::new(
    env!("CARGO_CRATE_NAME"),
    Some(env!("CARGO_PKG_VERSION")),
    Some("contact@program.com"),
    args.modrinth_token.as_ref(),
)?;
```
*/
#[derive(Debug, Clone)]
pub struct Ferinth { client: Client }

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"),
        }
    }
}

impl Ferinth {
    /**
    Instantiate the container with the provided [user agent](https://docs.modrinth.com/api-spec/#section/User-Agents) details,
    and an optional GitHub token for `authorisation`.

    The program `name` is required, while `version` and `contact` are optional but recommended.

    This function fails if the GitHub `authorisation` token provided is invalid header data.
    */
    pub fn new(
        name: &str,
        version: Option<&str>,
        contact: Option<&str>,
        authorisation: Option<&str>,
    ) -> Result<Self> {
        use header::{HeaderMap, HeaderValue};

        Ok(Self {
            client: Client::builder()
                .user_agent(format!(
                    "{}{}{}",
                    name,
                    version.map_or("".into(), |version| format!("/{}", version)),
                    contact.map_or("".into(), |contact| format!(" ({})", contact))
                ))
                .default_headers(if let Some(authorisation) = authorisation {
                    HeaderMap::from_iter(vec![(
                        header::AUTHORIZATION,
                        HeaderValue::from_str(authorisation)?,
                    )])
                } else {
                    HeaderMap::new()
                })
                .build()
                .expect("Failed to initialise TLS backend"),
        })
    }
}