lead_oxide/
types.rs

1//! [`types`][self] contains auxillary types used by [`Opts`][crate::opts::Opts].
2//!
3//! This includes NewType wrappers around parameters like [`LastChecked`][LastChecked] and
4//! [`TimeToConnect`][TimeToConnect] along with `enum`s for parameters with a limited number of
5//! options like [`Countries`][Countries], [`Level`][Level], and [`Protocol`][Protocol].
6
7use crate::errors::ParamError;
8
9use std::{convert::TryFrom, fmt, time::Duration};
10
11use iso_country::Country;
12use serde::{Deserialize, Serialize};
13use ureq::Response;
14
15#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
16struct BoundedVal<T>
17where
18    T: fmt::Debug + PartialEq + PartialOrd,
19{
20    #[serde(flatten)]
21    pub val: T,
22}
23
24impl<T> BoundedVal<T>
25where
26    T: fmt::Debug + PartialEq + PartialOrd,
27{
28    pub fn new(val: T, bounds: (T, T)) -> Result<Self, ParamError<T>> {
29        debug_assert!(bounds.0 <= bounds.1);
30
31        if val >= bounds.0 && val <= bounds.1 {
32            Ok(Self { val })
33        } else {
34            Err(ParamError::out_of_bounds(val, bounds))
35        }
36    }
37}
38
39macro_rules! bounded_val {
40    ($name:ident, $type:ty, $bounds:ident) => {
41        #[derive(Clone, Debug, PartialEq, Serialize)]
42        pub struct $name {
43            #[serde(flatten)]
44            inner: BoundedVal<$type>,
45        }
46
47        impl $name {
48            pub const BOUNDS: ($type, $type) = $bounds;
49
50            pub fn new(val: $type) -> Result<Self, ParamError<$type>> {
51                let inner = BoundedVal::new(val, Self::BOUNDS)?;
52                Ok(Self { inner })
53            }
54
55            pub fn value(&self) -> $type {
56                self.inner.val
57            }
58        }
59
60        impl TryFrom<$type> for $name {
61            type Error = ParamError<$type>;
62
63            fn try_from(val: $type) -> Result<Self, Self::Error> {
64                Self::new(val)
65            }
66        }
67    };
68}
69
70// One minute to an hour
71const LAST_CHECKED_BOUNDS: (Duration, Duration) =
72    (Duration::from_secs(60), Duration::from_secs(60 * 60));
73// One second to a minute
74const TIME_TO_CONNECT_BOUNDS: (Duration, Duration) =
75    (Duration::from_secs(1), Duration::from_secs(60));
76bounded_val! {LastChecked, Duration, LAST_CHECKED_BOUNDS}
77bounded_val! {TimeToConnect, Duration, TIME_TO_CONNECT_BOUNDS}
78
79pub(crate) struct NaiveResponse {
80    pub(crate) status: u16,
81    pub(crate) text: String,
82}
83
84impl NaiveResponse {
85    pub fn new(status: u16, text: String) -> Self {
86        Self { status, text }
87    }
88
89    pub fn ok(&self) -> bool {
90        (200..300).contains(&self.status)
91    }
92}
93
94impl From<Response> for NaiveResponse {
95    fn from(resp: Response) -> Self {
96        let status = resp.status();
97        let text = resp.into_string().unwrap_or_default();
98
99        Self::new(status, text)
100    }
101}
102
103#[derive(Serialize, Clone, Debug, PartialEq)]
104pub enum Countries {
105    #[serde(rename = "country")]
106    AllowList(String),
107    #[serde(rename = "not_country")]
108    BlockList(String),
109}
110
111impl Countries {
112    pub fn allow() -> Self {
113        Self::AllowList(String::new())
114    }
115
116    pub fn block() -> Self {
117        Self::BlockList(String::new())
118    }
119
120    pub fn is_empty(&self) -> bool {
121        match self {
122            Self::AllowList(countries) => countries.is_empty(),
123            Self::BlockList(countries) => countries.is_empty(),
124        }
125    }
126
127    pub fn countries(mut self, countries: &[Country]) -> Self {
128        for country in countries {
129            self = self.country(*country);
130        }
131
132        self
133    }
134
135    pub fn country(self, country: Country) -> Self {
136        // TODO: make sure this is documented. Mention that unknowns are automatically filtered out
137        // if any country is used in the allow or blocklist
138        if let Country::Unspecified = country {
139            // TODO: this could be returned as a `ParamError` instead of panicking
140            panic!("This library doesn't allow `Unspecified` country in the allow or blocklist");
141        }
142
143        let push_country = |list: String, new_tag: Country| {
144            let new_tag = new_tag.to_string();
145            if list.is_empty() {
146                new_tag
147            } else {
148                [list, new_tag].join(",")
149            }
150        };
151
152        match self {
153            Self::AllowList(list) => Self::AllowList(push_country(list, country)),
154            Self::BlockList(list) => Self::BlockList(push_country(list, country)),
155        }
156    }
157}
158
159impl Default for Countries {
160    fn default() -> Self {
161        // Default is to block none
162        Countries::block()
163    }
164}
165
166#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
167#[serde(rename_all = "lowercase")]
168pub enum Level {
169    Anonymous,
170    Elite,
171}
172
173#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)]
174#[serde(rename_all = "lowercase")]
175pub enum Protocol {
176    Http,
177    Socks4,
178    Socks5,
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    use std::time::Duration;
186
187    mod bounded_vals {
188        use super::*;
189
190        #[test]
191        fn bounds_checking() {
192            let zero_seconds = Duration::from_secs(0);
193            let just_over_minute = Duration::from_secs(61);
194            let just_over_hour = Duration::from_secs(60 * 60 + 1);
195
196            let bounds_err = TimeToConnect::try_from(zero_seconds).unwrap_err();
197            assert_eq!(
198                bounds_err,
199                ParamError::out_of_bounds(zero_seconds, TIME_TO_CONNECT_BOUNDS)
200            );
201
202            let bounds_err = TimeToConnect::try_from(just_over_minute).unwrap_err();
203            assert_eq!(
204                bounds_err,
205                ParamError::out_of_bounds(just_over_minute, TIME_TO_CONNECT_BOUNDS)
206            );
207
208            let bounds_err = LastChecked::try_from(zero_seconds).unwrap_err();
209            assert_eq!(
210                bounds_err,
211                ParamError::out_of_bounds(zero_seconds, LAST_CHECKED_BOUNDS)
212            );
213
214            let bounds_err = LastChecked::try_from(just_over_hour).unwrap_err();
215            assert_eq!(
216                bounds_err,
217                ParamError::out_of_bounds(just_over_hour, LAST_CHECKED_BOUNDS)
218            );
219        }
220
221        #[test]
222        fn it_works() {
223            let half_minute = Duration::from_secs(30);
224            let half_hour = Duration::from_secs(30 * 60);
225
226            let valid_time_to_connect = TimeToConnect::try_from(half_minute).unwrap();
227            assert_eq!(valid_time_to_connect.value(), half_minute);
228
229            let valid_last_checked = LastChecked::try_from(half_hour).unwrap();
230            assert_eq!(valid_last_checked.value(), half_hour);
231        }
232    }
233}