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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
//! A low level api wrapper for Rolimons.com.
//!
//! This crate is a low level wrapper due to the fact that allowed
//! requests to the api are limited. To maintain flexibiliy while also
//! using the api endpoints responsibly, the user is expected to maintain
//! their own caching.
//!
//! All endpoints are accessed from a [`Client`].
//!
//! # API Coverage Checklist
//! - [x] Items API
//!     - [`Client::deals_activity`]
//! - [x] Deals API
//!     - [`Client::all_item_details`]
//! - [x] Trade Ad API (Partial)
//!    - [`Client::create_trade_ad`]
//!
//! # Quick Start
//!
//! This code snippet allows you to get a list of all limited items
//! on Rolimon's, which includes information you would see on an item's page.
//!
//! ```no_run
//! #[tokio::main]
//! async fn main() {
//!     let client = roli::ClientBuilder::new().build();
//!     let all_item_details = client.all_item_details().await.unwrap();
//!     println!("Item Amount: {}", all_item_details.len());
//! }
//! ```

#![warn(missing_docs)]

use serde::{Deserialize, Serialize};

/// A module that contains all the endpoints associated with the deals page.
pub mod deals;
/// A module that contains all the endpoints associated with getting item details.
pub mod items;
/// A module that contains all the endpoints associated with the trade ads page.
pub mod trade_ads;

// Re-export reqwest so people can use the correct version.
pub use reqwest;

const USER_AGENT: &str =
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0";

/// The universal error used in this crate.
#[derive(thiserror::Error, Debug, Default)]
pub enum RoliError {
    /// Used when an endpoint returns `success: false`.
    #[error("Request Returned Unsuccessful")]
    RequestReturnedUnsuccessful,
    /// Used when an endpoint returns status code 429.
    #[default]
    #[error("Too Many Requests")]
    TooManyRequests,
    /// Used when an endpoint returns status code 500.
    #[error("Internal Server Error")]
    InternalServerError,
    /// Used when the response from an API endpoint is malformed.
    #[error("Malformed Response")]
    MalformedResponse,
    /// Used when roli_verification contains ASCII characters outside of the range 32-127.
    #[error("Roli Verification Contains Invalid Characters")]
    RoliVerificationContainsInvalidCharacters,
    /// Used when roli_verification is invalid or expired.
    #[error("Roli Verification Invalid Or Expired")]
    RoliVerificationInvalidOrExpired,
    /// Used when roli_verification is not set.
    #[error("Roli Verification Not Set")]
    RoliVerificationNotSet,
    /// Used when a cooldown for something, such as making a trade ad, has not expired.
    #[error("Cooldown Not Expired")]
    CooldownNotExpired,
    /// Used for any status codes that do not fit any enum variants of this error.
    /// If you encounter this enum variant, please submit an issue so a variant can be
    /// made or the crate can be fixed.
    #[error("Unidentified Status Code {0}")]
    UnidentifiedStatusCode(u16),
    /// Used for any reqwest error that occurs.
    #[error("RequestError {0}")]
    ReqwestError(reqwest::Error),
}

/// Used for holding either an integer or a string in [`AllItemDetailsResponse`].
/// This is necessary as (for some reason) numbers are represented as strings
/// in the api response.
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum Code {
    Integer(i64),
    String(String),
}

/// Used to interact with the rest of the Rolimons.com api wrapper.
///
/// Contains any necessary authentication and the reqwest client. All
/// [`Client`] methods make exactly one api call.
#[derive(Clone, Debug, Default)]
pub struct Client {
    roli_verification: Option<String>,
    reqwest_client: reqwest::Client,
}

/// Used to build a [`Client`].
///
/// Creates its own reqwest client if one is not provided to the builder.
#[derive(Clone, Debug, Default)]
pub struct ClientBuilder {
    roli_verification: Option<String>,
    reqwest_client: Option<reqwest::Client>,
}

impl Code {
    /// Returns an i64 inside if the operation was successful, otherwise returns a [`RoliError::MalformedResponse`]
    /// (as [`Code`] is only used to parse responses).
    fn to_i64(&self) -> Result<i64, RoliError> {
        match self {
            Self::Integer(x) => Ok(*x),
            Self::String(x) => x.parse().map_err(|_| RoliError::MalformedResponse),
        }
    }
}

impl std::fmt::Display for Code {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Integer(x) => write!(f, "{}", x),
            Self::String(x) => write!(f, "{}", x),
        }
    }
}

impl Client {
    /// Constructs a client without providing a roli verification token or custom
    /// reqwest client.
    ///
    /// Use [`ClientBuilder`] to add these parameters to a new [`Client`].
    pub fn new() -> Self {
        Self::default()
    }

    /// Constructs a new [`Client`] with a roli verification token.
    pub fn with_roli_verification(roli_verification: String) -> Self {
        Self {
            roli_verification: Some(roli_verification),
            ..Default::default()
        }
    }

    /// Sets the value for the optional `roli_verification` field.
    pub fn set_roli_verification(&mut self, roli_verification: String) {
        self.roli_verification = Some(roli_verification);
    }

    /// Returns whether the client has `self.roliverification`
    /// set to `Some(_)`. Does not check to see if the token is valid.
    pub fn contains_roli_verification(&self) -> bool {
        self.roli_verification.is_some()
    }
}

impl ClientBuilder {
    /// Constructs a new instance of the builder with no values set for its fields.
    pub fn new() -> Self {
        Self {
            roli_verification: None,
            reqwest_client: None,
        }
    }

    /// Builds the `Client` struct using the values set in this builder. Uses default values for any unset fields.
    pub fn build(self) -> Client {
        let reqwest_client = self.reqwest_client.unwrap_or_default();

        Client {
            roli_verification: self.roli_verification,
            reqwest_client,
        }
    }

    /// Sets the value for the optional `roli_verification` field.
    ///
    /// # Examples
    ///
    /// ```
    /// # use roli::{ClientBuilder, Client};
    /// let builder = ClientBuilder::new();
    /// let client = builder.set_roli_verification("apikey".to_string()).build();
    /// assert!(client.contains_roli_verification())
    /// ```
    pub fn set_roli_verification(mut self, roli_verification: String) -> Self {
        self.roli_verification = Some(roli_verification);
        self
    }

    /// Sets the value for the optional `reqwest_client` field.
    ///
    /// # Examples
    ///
    /// ```
    /// # use roli::{ClientBuilder, Client};
    /// let builder = ClientBuilder::new();
    /// let reqwest_client = reqwest::Client::new();
    /// let client = builder.set_reqwest_client(reqwest_client).build();
    /// ```
    pub fn set_reqwest_client(mut self, reqwest_client: reqwest::Client) -> Self {
        self.reqwest_client = Some(reqwest_client);
        self
    }
}