lead_oxide/
opts.rs

1//! [`Opts`][Opts] provide the ability to filter the returned proxies.
2
3use std::num::NonZeroU16;
4
5use crate::types::{Countries, LastChecked, Level, Protocol, TimeToConnect};
6
7use serde::Serialize;
8use serde_repr::Serialize_repr;
9
10// TODO: allow for multiple things being specified on the different things that accept it?
11/// A builder for setting up [`Opts`][Opts].
12///
13/// Constructed with `Opts::builder()`. By default any field that isn't specified will just return
14/// any possible value so these options just constrain the returned results.
15#[derive(Clone, Debug, Default, PartialEq)]
16pub struct OptsBuilder {
17    api_key: Option<String>,
18    level: Option<Level>,
19    protocol: Option<Protocol>,
20    countries: Option<Countries>,
21    last_checked: Option<LastChecked>,
22    port: Option<NonZeroU16>,
23    time_to_connect: Option<TimeToConnect>,
24    cookies: Option<bool>,
25    connects_to_google: Option<bool>,
26    https: Option<bool>,
27    post: Option<bool>,
28    referer: Option<bool>,
29    forwards_user_agent: Option<bool>,
30}
31
32impl OptsBuilder {
33    /// Passes an API key to the API. This removes both the rate limit and daily limit on the API.
34    pub fn api_key(mut self, api_key: String) -> Self {
35        self.api_key = Some(api_key);
36        self
37    }
38
39    /// The anonymity level of proxies returned by the API where the proxies are either Anonymous or
40    /// Elite (Transparent isn't provided).
41    pub fn level(mut self, level: Level) -> Self {
42        self.level = Some(level);
43        self
44    }
45
46    /// The protocol supported by the proxies. This can either be HTTP, SOCKS4, or SOCKS5.
47    pub fn protocol(mut self, protocol: Protocol) -> Self {
48        self.protocol = Some(protocol);
49        self
50    }
51
52    /// Either a block or allowlist of countries for where the proxies can be located.
53    pub fn countries(mut self, countries: Countries) -> Self {
54        self.countries = Some(countries);
55        self
56    }
57
58    /// Time when the proxies were last checked. Resolution down to minutes with a valid range of
59    /// 1 to 1,000 minutes.
60    pub fn last_checked(mut self, last_checked: LastChecked) -> Self {
61        self.last_checked = Some(last_checked);
62        self
63    }
64
65    /// Specifies the port that the proxy exposes.
66    pub fn port(mut self, port: NonZeroU16) -> Self {
67        self.port = Some(port);
68        self
69    }
70
71    /// Filters based on how long it took to connect to the proxy when testing. Will return values
72    /// at or below the specified time with a resolution down to seconds with a valid range of 1 to
73    /// 60 seconds.
74    pub fn time_to_connect(mut self, time_to_connect: TimeToConnect) -> Self {
75        self.time_to_connect = Some(time_to_connect);
76        self
77    }
78
79    /// If the proxy supports cookies or not.
80    pub fn cookies(mut self, cookies: bool) -> Self {
81        self.cookies = Some(cookies);
82        self
83    }
84
85    /// If the proxy was able to connect to google or not.
86    pub fn connects_to_google(mut self, connects_to_google: bool) -> Self {
87        self.connects_to_google = Some(connects_to_google);
88        self
89    }
90
91    /// If the proxy supports HTTPS requests or not.
92    pub fn https(mut self, https: bool) -> Self {
93        self.https = Some(https);
94        self
95    }
96
97    /// If the proxy supports POST requests or not.
98    pub fn post(mut self, post: bool) -> Self {
99        self.post = Some(post);
100        self
101    }
102
103    /// If the proxy supports referer requests or not.
104    pub fn referer(mut self, referer: bool) -> Self {
105        self.referer = Some(referer);
106        self
107    }
108
109    /// If the proxy supports forwarding your user agent.
110    pub fn forwards_user_agent(mut self, forwards_user_agent: bool) -> Self {
111        self.forwards_user_agent = Some(forwards_user_agent);
112        self
113    }
114
115    /// Constructs the `OptsBuilder` into the corresponding [`Opts`][Opts] value.
116    pub fn build(self) -> Opts {
117        Opts::from(self)
118    }
119}
120
121/// Internal
122#[derive(Serialize_repr, Clone, Copy, Debug, PartialEq, Eq)]
123#[repr(u8)]
124pub(crate) enum Limit {
125    Free = 5,
126    Premium = 20,
127}
128
129impl Default for Limit {
130    fn default() -> Self {
131        Self::Free
132    }
133}
134
135/// Internal
136#[derive(Serialize, Clone, Copy, Debug, PartialEq, Eq)]
137#[serde(rename_all = "lowercase")]
138enum Format {
139    // Techically txt is also allowed, but this library only uses json
140    Json,
141}
142
143impl Default for Format {
144    fn default() -> Self {
145        Self::Json
146    }
147}
148
149/// A set of options to constrain the returned proxies.
150///
151/// `Opts` represents all the filtering options that are passed on to the API by the corresponding
152/// [`Fetcher`][crate::fetcher::Fetcher]. By default no values are filtered and any kind of proxy
153/// can be returned so this list of options only serves to restrict the proxies returned. The
154/// typical way to construct `Opts` is with [`OptsBuilder`][OptsBuilder] with the entrypoint of
155/// `Opts::builder()`.
156///
157/// ```
158/// use iso_country::Country;
159/// use lead_oxide::{
160///     opts::Opts,
161///     types::{Countries, LastChecked, Level, Protocol, TimeToConnect}
162/// };
163/// use std::{convert::TryFrom, num::NonZeroU16, time::Duration};
164///
165/// let basic_opts = Opts::builder()
166///     .post(true)
167///     .cookies(true)
168///     .build();
169/// let kitchen_sink = Opts::builder()
170///     .api_key("<key>".to_string())
171///     .level(Level::Elite)
172///     .protocol(Protocol::Socks4)
173///     .countries(Countries::block().countries(&[Country::CH, Country::ES]))
174///     .last_checked(LastChecked::try_from(Duration::from_secs(60 * 10)).unwrap())
175///     .time_to_connect(TimeToConnect::try_from(Duration::from_secs(10)).unwrap())
176///     .port(NonZeroU16::new(8080).unwrap())
177///     .cookies(true)
178///     .connects_to_google(false)
179///     .https(true)
180///     .post(false)
181///     .referer(true)
182///     .forwards_user_agent(false)
183///     .build();
184/// ```
185#[derive(Serialize, Clone, Debug, Default, PartialEq)]
186pub struct Opts {
187    #[serde(rename = "api")]
188    api_key: Option<String>,
189    level: Option<Level>,
190    #[serde(rename = "type")]
191    protocol: Option<Protocol>,
192    // An empty country list is essentially `None`
193    #[serde(flatten, skip_serializing_if = "Countries::is_empty")]
194    countries: Countries,
195    #[serde(rename = "last_check")]
196    last_checked: Option<u64>,
197    // Note: using a port of 0 will return any port from the api :silly:
198    port: Option<NonZeroU16>,
199    #[serde(rename = "speed")]
200    time_to_connect: Option<u64>,
201    cookies: Option<bool>,
202    #[serde(rename = "google")]
203    connects_to_google: Option<bool>,
204    https: Option<bool>,
205    post: Option<bool>,
206    referer: Option<bool>,
207    #[serde(rename = "user_agent")]
208    forwards_user_agent: Option<bool>,
209    pub(crate) limit: Limit,
210    format: Format,
211}
212
213impl Opts {
214    /// Constructs an [`OptsBuilder`][OptsBuilder]
215    pub fn builder() -> OptsBuilder {
216        OptsBuilder::default()
217    }
218
219    /// Internal
220    pub(crate) fn is_premium(&self) -> bool {
221        self.api_key.is_some()
222    }
223}
224
225impl From<OptsBuilder> for Opts {
226    fn from(builder: OptsBuilder) -> Self {
227        Self {
228            limit: match builder.api_key {
229                Some(_) => Limit::Premium,
230                None => Limit::Free,
231            },
232            api_key: builder.api_key,
233            level: builder.level,
234            protocol: builder.protocol,
235            countries: builder.countries.unwrap_or_default(),
236            last_checked: builder
237                .last_checked
238                .map(|last_checked| last_checked.value().as_secs() / 60),
239            port: builder.port,
240            time_to_connect: builder
241                .time_to_connect
242                .map(|time_to_connect| time_to_connect.value().as_secs()),
243            cookies: builder.cookies,
244            connects_to_google: builder.connects_to_google,
245            https: builder.https,
246            post: builder.post,
247            referer: builder.referer,
248            forwards_user_agent: builder.forwards_user_agent,
249            format: Format::default(),
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    use std::{convert::TryFrom, time::Duration};
259
260    use iso_country::Country;
261
262    #[test]
263    fn url_serialization() -> Result<(), serde_urlencoded::ser::Error> {
264        let check_equivalent_params = |opts, expected: &[&str]| {
265            // Convert `opts` to a url and sort the values
266            let url = serde_urlencoded::to_string(&opts)?;
267            let mut params: Vec<_> = url.split('&').map(String::from).collect();
268            params.sort();
269
270            // Sort the `expected` values
271            let mut expected = expected.to_vec();
272            expected.sort_unstable();
273
274            assert_eq!(params, expected);
275
276            Ok(())
277        };
278
279        // Base `Opts`
280        check_equivalent_params(Opts::default(), &["format=json", "limit=5"])?;
281        // Using a key will up the limit
282        check_equivalent_params(
283            Opts::builder().api_key("<key>".to_string()).build(),
284            &["api=%3Ckey%3E", "format=json", "limit=20"],
285        )?;
286        // Empty countries list won't be included (api seems to work with an empty list, but I don't
287        // want to rely on this behavior)
288        check_equivalent_params(
289            Opts::builder().countries(Countries::default()).build(),
290            &["format=json", "limit=5"],
291        )?;
292        // Kitchen sink
293        check_equivalent_params(
294            Opts::builder()
295                .api_key("<key>".to_string())
296                .level(Level::Elite)
297                .protocol(Protocol::Socks4)
298                .countries(Countries::block().countries(&[Country::CH, Country::ES]))
299                .last_checked(LastChecked::try_from(Duration::from_secs(60 * 10)).unwrap())
300                .time_to_connect(TimeToConnect::try_from(Duration::from_secs(10)).unwrap())
301                .port(NonZeroU16::new(8080).unwrap())
302                .cookies(true)
303                .connects_to_google(false)
304                .https(true)
305                .post(false)
306                .referer(true)
307                .forwards_user_agent(false)
308                .build(),
309            &[
310                // Automatic
311                "limit=20",
312                "format=json",
313                // Key
314                "api=%3Ckey%3E",
315                // Enums
316                "level=elite",
317                "type=socks4",
318                "not_country=CH%2CES",
319                // Durations
320                "last_check=10",
321                "speed=10",
322                // NonZero
323                "port=8080",
324                // Bools
325                "cookies=true",
326                "google=false",
327                "https=true",
328                "post=false",
329                "referer=true",
330                "user_agent=false",
331            ],
332        )
333    }
334}