use crate::proxy::ProxyOutcome;
use std::borrow::Cow;
use std::fmt;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResidentialProviderKind {
None,
BrightData,
Oxylabs,
IPRoyal,
}
impl ResidentialProviderKind {
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::BrightData => "brightdata",
Self::Oxylabs => "oxylabs",
Self::IPRoyal => "iproyal",
}
}
}
impl fmt::Display for ResidentialProviderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for ResidentialProviderKind {
type Err = ResidentialError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"none" | "" => Ok(Self::None),
"brightdata" | "bright-data" | "luminati" => Ok(Self::BrightData),
"oxylabs" => Ok(Self::Oxylabs),
"iproyal" | "ip-royal" => Ok(Self::IPRoyal),
other => Err(ResidentialError::UnknownProvider(other.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub enum ResidentialError {
ProviderNotConfigured(&'static str),
UnknownProvider(String),
Upstream(Cow<'static, str>),
}
impl fmt::Display for ResidentialError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ProviderNotConfigured(p) => {
write!(f, "residential provider `{p}` selected but not configured")
}
Self::UnknownProvider(p) => write!(f, "unknown residential provider `{p}`"),
Self::Upstream(m) => write!(f, "residential provider upstream error: {m}"),
}
}
}
impl std::error::Error for ResidentialError {}
pub trait ResidentialProvider: Send + Sync {
fn name(&self) -> &'static str;
fn rotate(&self, host: &str) -> Result<Url, ResidentialError>;
fn report_outcome(&self, _proxy: &Url, _outcome: ProxyOutcome) {}
}
#[derive(Debug, Default)]
pub struct BrightDataStub;
impl ResidentialProvider for BrightDataStub {
fn name(&self) -> &'static str {
"brightdata-stub"
}
fn rotate(&self, _host: &str) -> Result<Url, ResidentialError> {
Err(ResidentialError::ProviderNotConfigured("brightdata"))
}
}
#[derive(Debug, Default)]
pub struct OxylabsStub;
impl ResidentialProvider for OxylabsStub {
fn name(&self) -> &'static str {
"oxylabs-stub"
}
fn rotate(&self, _host: &str) -> Result<Url, ResidentialError> {
Err(ResidentialError::ProviderNotConfigured("oxylabs"))
}
}
#[derive(Debug, Default)]
pub struct IPRoyalStub;
impl ResidentialProvider for IPRoyalStub {
fn name(&self) -> &'static str {
"iproyal-stub"
}
fn rotate(&self, _host: &str) -> Result<Url, ResidentialError> {
Err(ResidentialError::ProviderNotConfigured("iproyal"))
}
}
pub fn build_provider(kind: ResidentialProviderKind) -> Option<Box<dyn ResidentialProvider>> {
match kind {
ResidentialProviderKind::None => None,
ResidentialProviderKind::BrightData => Some(Box::new(BrightDataStub)),
ResidentialProviderKind::Oxylabs => Some(Box::new(OxylabsStub)),
ResidentialProviderKind::IPRoyal => Some(Box::new(IPRoyalStub)),
}
}
pub mod env {
pub const CRAWLEX_RES_PROVIDER: &str = "CRAWLEX_RES_PROVIDER";
pub const CRAWLEX_RES_PROXY_BRIGHTDATA_USER: &str = "CRAWLEX_RES_PROXY_BRIGHTDATA_USER";
pub const CRAWLEX_RES_PROXY_BRIGHTDATA_PASS: &str = "CRAWLEX_RES_PROXY_BRIGHTDATA_PASS";
pub const CRAWLEX_RES_PROXY_BRIGHTDATA_ZONE: &str = "CRAWLEX_RES_PROXY_BRIGHTDATA_ZONE";
pub const CRAWLEX_RES_PROXY_OXYLABS_USER: &str = "CRAWLEX_RES_PROXY_OXYLABS_USER";
pub const CRAWLEX_RES_PROXY_OXYLABS_PASS: &str = "CRAWLEX_RES_PROXY_OXYLABS_PASS";
pub const CRAWLEX_RES_PROXY_IPROYAL_USER: &str = "CRAWLEX_RES_PROXY_IPROYAL_USER";
pub const CRAWLEX_RES_PROXY_IPROYAL_PASS: &str = "CRAWLEX_RES_PROXY_IPROYAL_PASS";
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn parses_known_providers() {
assert_eq!(
ResidentialProviderKind::from_str("brightdata").unwrap(),
ResidentialProviderKind::BrightData
);
assert_eq!(
ResidentialProviderKind::from_str("oxylabs").unwrap(),
ResidentialProviderKind::Oxylabs
);
assert_eq!(
ResidentialProviderKind::from_str("iproyal").unwrap(),
ResidentialProviderKind::IPRoyal
);
assert_eq!(
ResidentialProviderKind::from_str("none").unwrap(),
ResidentialProviderKind::None
);
}
#[test]
fn rejects_unknown_provider() {
assert!(ResidentialProviderKind::from_str("bogus").is_err());
}
#[test]
fn stubs_return_not_configured() {
let p = BrightDataStub;
let err = p.rotate("example.com").unwrap_err();
assert!(matches!(err, ResidentialError::ProviderNotConfigured(_)));
}
#[test]
fn build_provider_none_yields_none() {
assert!(build_provider(ResidentialProviderKind::None).is_none());
assert!(build_provider(ResidentialProviderKind::BrightData).is_some());
}
}