Skip to main content

stygian_proxy/
types.rs

1//! Core domain types for proxy management.
2
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::{Duration, Instant};
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// The protocol variant of a proxy endpoint.
10///
11/// # Example
12/// ```
13/// use stygian_proxy::types::ProxyType;
14/// assert_eq!(ProxyType::Http, ProxyType::Http);
15/// ```
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ProxyType {
19    /// Plain HTTP proxy (CONNECT / forwarding).
20    Http,
21    /// HTTPS proxy over TLS.
22    Https,
23    #[cfg(feature = "socks")]
24    /// SOCKS4 proxy (requires the `socks` feature).
25    Socks4,
26    #[cfg(feature = "socks")]
27    /// SOCKS5 proxy (requires the `socks` feature).
28    Socks5,
29}
30
31/// A proxy endpoint with optional authentication credentials.
32///
33/// `Debug` output masks `password` to prevent accidental credential logging.
34///
35/// # Example
36/// ```
37/// use stygian_proxy::types::{Proxy, ProxyType};
38/// let p = Proxy {
39///     url: "http://proxy.example.com:8080".into(),
40///     proxy_type: ProxyType::Http,
41///     username: Some("alice".into()),
42///     password: Some("secret".into()),
43///     weight: 1,
44///     tags: vec!["prod".into()],
45/// };
46/// let debug = format!("{p:?}");
47/// assert!(debug.contains("***"), "password must be masked in Debug output");
48/// ```
49#[derive(Clone, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub struct Proxy {
52    /// The proxy URL, e.g. `http://proxy.example.com:8080`.
53    pub url: String,
54    pub proxy_type: ProxyType,
55    pub username: Option<String>,
56    pub password: Option<String>,
57    /// Relative selection weight for weighted rotation (default: `1`).
58    pub weight: u32,
59    /// User-defined tags for filtering and grouping.
60    pub tags: Vec<String>,
61}
62
63impl std::fmt::Debug for Proxy {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        f.debug_struct("Proxy")
66            .field("url", &self.url)
67            .field("proxy_type", &self.proxy_type)
68            .field("username", &self.username)
69            .field("password", &self.password.as_deref().map(|_| "***"))
70            .field("weight", &self.weight)
71            .field("tags", &self.tags)
72            .finish()
73    }
74}
75
76/// A [`Proxy`] with a stable identity and insertion timestamp.
77///
78/// # Example
79/// ```
80/// use stygian_proxy::types::{Proxy, ProxyType, ProxyRecord};
81/// let proxy = Proxy {
82///     url: "http://proxy.example.com:8080".into(),
83///     proxy_type: ProxyType::Http,
84///     username: None,
85///     password: None,
86///     weight: 1,
87///     tags: vec![],
88/// };
89/// let record = ProxyRecord::new(proxy);
90/// assert!(!record.id.is_nil());
91/// ```
92#[derive(Debug, Clone, Serialize, Deserialize)]
93#[serde(rename_all = "snake_case")]
94pub struct ProxyRecord {
95    pub id: Uuid,
96    pub proxy: Proxy,
97    /// Wall-clock time the proxy was added. Not serialized — `Instant` is
98    /// not meaningfully portable; defaults to `Instant::now()` on deserialization.
99    #[serde(skip, default = "Instant::now")]
100    pub added_at: Instant,
101}
102
103impl ProxyRecord {
104    /// Create a new [`ProxyRecord`] wrapping `proxy` with a freshly generated UUID.
105    pub fn new(proxy: Proxy) -> Self {
106        Self {
107            id: Uuid::new_v4(),
108            proxy,
109            added_at: Instant::now(),
110        }
111    }
112}
113
114/// Per-proxy runtime metrics using lock-free atomic counters.
115///
116/// Intended to be shared via `Arc<ProxyMetrics>`.
117///
118/// # Example
119/// ```
120/// use stygian_proxy::types::ProxyMetrics;
121/// let m = ProxyMetrics::default();
122/// assert_eq!(m.success_rate(), 0.0);
123/// assert_eq!(m.avg_latency_ms(), 0.0);
124/// ```
125#[derive(Debug, Default)]
126pub struct ProxyMetrics {
127    pub requests_total: AtomicU64,
128    pub successes: AtomicU64,
129    pub failures: AtomicU64,
130    pub total_latency_ms: AtomicU64,
131}
132
133impl ProxyMetrics {
134    /// Returns the fraction of requests that succeeded, in `[0.0, 1.0]`.
135    ///
136    /// Returns `0.0` when no requests have been recorded.
137    ///
138    /// # Example
139    /// ```
140    /// use stygian_proxy::types::ProxyMetrics;
141    /// use std::sync::atomic::Ordering;
142    /// let m = ProxyMetrics::default();
143    /// m.requests_total.store(10, Ordering::Relaxed);
144    /// m.successes.store(8, Ordering::Relaxed);
145    /// assert!((m.success_rate() - 0.8).abs() < f64::EPSILON);
146    /// ```
147    pub fn success_rate(&self) -> f64 {
148        let total = self.requests_total.load(Ordering::Relaxed);
149        if total == 0 {
150            return 0.0;
151        }
152        self.successes.load(Ordering::Relaxed) as f64 / total as f64
153    }
154
155    /// Returns the average request latency in milliseconds.
156    ///
157    /// Returns `0.0` when no requests have been recorded.
158    ///
159    /// # Example
160    /// ```
161    /// use stygian_proxy::types::ProxyMetrics;
162    /// use std::sync::atomic::Ordering;
163    /// let m = ProxyMetrics::default();
164    /// m.requests_total.store(4, Ordering::Relaxed);
165    /// m.total_latency_ms.store(400, Ordering::Relaxed);
166    /// assert!((m.avg_latency_ms() - 100.0).abs() < f64::EPSILON);
167    /// ```
168    pub fn avg_latency_ms(&self) -> f64 {
169        let total = self.requests_total.load(Ordering::Relaxed);
170        if total == 0 {
171            return 0.0;
172        }
173        self.total_latency_ms.load(Ordering::Relaxed) as f64 / total as f64
174    }
175}
176
177mod serde_duration_secs {
178    use serde::{Deserialize, Deserializer, Serialize, Serializer};
179    use std::time::Duration;
180
181    pub fn serialize<S: Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
182        d.as_secs().serialize(s)
183    }
184
185    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
186        Ok(Duration::from_secs(u64::deserialize(d)?))
187    }
188}
189
190/// Configuration governing health checking and circuit-breaker behaviour.
191///
192/// Duration fields serialize as integer seconds for TOML/JSON compatibility.
193///
194/// # Example
195/// ```
196/// use stygian_proxy::types::ProxyConfig;
197/// use std::time::Duration;
198/// let cfg = ProxyConfig::default();
199/// assert_eq!(cfg.health_check_url, "https://httpbin.org/ip");
200/// assert_eq!(cfg.health_check_interval, Duration::from_secs(60));
201/// assert_eq!(cfg.health_check_timeout, Duration::from_secs(5));
202/// assert_eq!(cfg.circuit_open_threshold, 5);
203/// assert_eq!(cfg.circuit_half_open_after, Duration::from_secs(30));
204/// ```
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub struct ProxyConfig {
208    /// URL called during health checks to verify proxy liveness.
209    pub health_check_url: String,
210    /// How often to run health checks (seconds).
211    #[serde(with = "serde_duration_secs")]
212    pub health_check_interval: Duration,
213    /// Per-probe HTTP timeout (seconds).
214    #[serde(with = "serde_duration_secs")]
215    pub health_check_timeout: Duration,
216    /// Consecutive failures before the circuit trips to OPEN.
217    pub circuit_open_threshold: u32,
218    /// How long to wait in OPEN before transitioning to HALF-OPEN (seconds).
219    #[serde(with = "serde_duration_secs")]
220    pub circuit_half_open_after: Duration,
221}
222
223impl Default for ProxyConfig {
224    fn default() -> Self {
225        Self {
226            health_check_url: "https://httpbin.org/ip".into(),
227            health_check_interval: Duration::from_secs(60),
228            health_check_timeout: Duration::from_secs(5),
229            circuit_open_threshold: 5,
230            circuit_half_open_after: Duration::from_secs(30),
231        }
232    }
233}