use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoogleATPList {
pub providers: Vec<AdTechProvider>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AdTechProvider {
pub id: u16,
pub name: String,
pub domains: Vec<String>,
}
impl GoogleATPList {
#[must_use]
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
#[must_use]
pub fn with_providers(providers: Vec<AdTechProvider>) -> Self {
Self { providers }
}
#[cfg(feature = "scanner")]
pub async fn fetch() -> Result<Self, ATPError> {
use reqwest;
const ATP_LIST_URL: &str = "https://storage.googleapis.com/gac/google-atp-list.json";
let response = reqwest::get(ATP_LIST_URL)
.await
.map_err(|e| ATPError::FetchError(e.to_string()))?;
if !response.status().is_success() {
return Err(ATPError::HttpError(response.status().as_u16()));
}
let list: GoogleATPList = response
.json()
.await
.map_err(|e| ATPError::ParseError(e.to_string()))?;
Ok(list)
}
#[cfg(feature = "scanner")]
pub async fn fetch_with_timeout(timeout_secs: u64) -> Result<Self, ATPError> {
use reqwest;
use std::time::Duration;
const ATP_LIST_URL: &str = "https://storage.googleapis.com/gac/google-atp-list.json";
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(|e| ATPError::FetchError(e.to_string()))?;
let response = client
.get(ATP_LIST_URL)
.send()
.await
.map_err(|e| ATPError::FetchError(e.to_string()))?;
if !response.status().is_success() {
return Err(ATPError::HttpError(response.status().as_u16()));
}
let list: GoogleATPList = response
.json()
.await
.map_err(|e| ATPError::ParseError(e.to_string()))?;
Ok(list)
}
#[must_use = "JSON parsing failure must be handled"]
pub fn from_json(json: &str) -> Result<Self, ATPError> {
serde_json::from_str(json).map_err(|e| ATPError::ParseError(e.to_string()))
}
#[must_use]
pub fn get_provider(&self, id: u16) -> Option<&AdTechProvider> {
self.providers.iter().find(|p| p.id == id)
}
#[must_use]
pub fn find_by_domain(&self, domain: &str) -> Vec<&AdTechProvider> {
self.providers
.iter()
.filter(|p| p.domains.iter().any(|d| d == domain))
.collect()
}
#[must_use]
pub fn create_index(&self) -> HashMap<u16, &AdTechProvider> {
self.providers.iter().map(|p| (p.id, p)).collect()
}
#[must_use]
pub fn create_domain_index(&self) -> HashMap<String, Vec<&AdTechProvider>> {
let mut index: HashMap<String, Vec<&AdTechProvider>> = HashMap::new();
for provider in &self.providers {
for domain in &provider.domains {
index.entry(domain.clone()).or_default().push(provider);
}
}
index
}
#[must_use]
pub fn all_ids(&self) -> Vec<u16> {
self.providers.iter().map(|p| p.id).collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.providers.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.providers.is_empty()
}
#[must_use]
pub fn is_valid_id(&self, id: u16) -> bool {
self.get_provider(id).is_some()
}
}
impl Default for GoogleATPList {
fn default() -> Self {
Self::new()
}
}
impl AdTechProvider {
#[must_use]
pub fn new(id: u16, name: String, domains: Vec<String>) -> Self {
Self { id, name, domains }
}
#[must_use]
pub fn has_domain(&self, domain: &str) -> bool {
self.domains.iter().any(|d| d == domain)
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum ATPError {
#[error("Failed to fetch ATP list: {0}")]
FetchError(String),
#[error("HTTP error {0} when fetching ATP list")]
HttpError(u16),
#[error("Failed to parse ATP list: {0}")]
ParseError(String),
#[error("ATP ID {0} not found in the list")]
AtpNotFound(u16),
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_providers() -> Vec<AdTechProvider> {
vec![
AdTechProvider {
id: 1,
name: "Google Advertising Products".to_string(),
domains: vec!["doubleclick.net".to_string(), "google.com".to_string()],
},
AdTechProvider {
id: 35,
name: "Google Analytics".to_string(),
domains: vec!["google-analytics.com".to_string()],
},
AdTechProvider {
id: 41,
name: "YouTube".to_string(),
domains: vec!["youtube.com".to_string(), "googlevideo.com".to_string()],
},
AdTechProvider {
id: 101,
name: "DoubleClick".to_string(),
domains: vec!["doubleclick.net".to_string()],
},
]
}
fn create_test_list() -> GoogleATPList {
GoogleATPList::with_providers(create_test_providers())
}
#[test]
fn test_new_empty_list() {
let list = GoogleATPList::new();
assert!(list.providers.is_empty());
assert_eq!(list.len(), 0);
assert!(list.is_empty());
}
#[test]
fn test_with_providers() {
let providers = create_test_providers();
let list = GoogleATPList::with_providers(providers);
assert_eq!(list.len(), 4);
assert!(!list.is_empty());
}
#[test]
fn test_get_provider_found() {
let list = create_test_list();
let provider = list.get_provider(35).unwrap();
assert_eq!(provider.id, 35);
assert_eq!(provider.name, "Google Analytics");
}
#[test]
fn test_get_provider_not_found() {
let list = create_test_list();
assert!(list.get_provider(999).is_none());
}
#[test]
fn test_is_valid_id() {
let list = create_test_list();
assert!(list.is_valid_id(1));
assert!(list.is_valid_id(35));
assert!(list.is_valid_id(41));
assert!(!list.is_valid_id(999));
}
#[test]
fn test_find_by_domain_single() {
let list = create_test_list();
let found = list.find_by_domain("google-analytics.com");
assert_eq!(found.len(), 1);
assert_eq!(found[0].id, 35);
}
#[test]
fn test_find_by_domain_multiple() {
let list = create_test_list();
let found = list.find_by_domain("doubleclick.net");
assert_eq!(found.len(), 2); let ids: Vec<u16> = found.iter().map(|p| p.id).collect();
assert!(ids.contains(&1));
assert!(ids.contains(&101));
}
#[test]
fn test_find_by_domain_not_found() {
let list = create_test_list();
let found = list.find_by_domain("unknown.com");
assert!(found.is_empty());
}
#[test]
fn test_create_index() {
let list = create_test_list();
let index = list.create_index();
assert_eq!(index.len(), 4);
assert!(index.contains_key(&1));
assert!(index.contains_key(&35));
assert!(index.contains_key(&41));
assert!(index.contains_key(&101));
let provider = index.get(&35).unwrap();
assert_eq!(provider.name, "Google Analytics");
}
#[test]
fn test_create_domain_index() {
let list = create_test_list();
let index = list.create_domain_index();
assert!(index.contains_key("doubleclick.net"));
assert!(index.contains_key("google-analytics.com"));
let providers = index.get("doubleclick.net").unwrap();
assert_eq!(providers.len(), 2);
}
#[test]
fn test_all_ids() {
let list = create_test_list();
let ids = list.all_ids();
assert_eq!(ids, vec![1, 35, 41, 101]);
}
#[test]
fn test_len_and_is_empty() {
let empty_list = GoogleATPList::new();
assert_eq!(empty_list.len(), 0);
assert!(empty_list.is_empty());
let list = create_test_list();
assert_eq!(list.len(), 4);
assert!(!list.is_empty());
}
#[test]
fn test_from_json_valid() {
let json = r#"{
"providers": [
{
"id": 1,
"name": "Test Provider",
"domains": ["test.com"]
}
]
}"#;
let list = GoogleATPList::from_json(json).unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list.providers[0].id, 1);
assert_eq!(list.providers[0].name, "Test Provider");
}
#[test]
fn test_from_json_invalid() {
let json = r#"{"invalid": "json"}"#;
let result = GoogleATPList::from_json(json);
assert!(matches!(result, Err(ATPError::ParseError(_))));
}
#[test]
fn test_provider_new() {
let provider = AdTechProvider::new(1, "Test".to_string(), vec!["test.com".to_string()]);
assert_eq!(provider.id, 1);
assert_eq!(provider.name, "Test");
assert_eq!(provider.domains.len(), 1);
}
#[test]
fn test_provider_has_domain() {
let provider = AdTechProvider {
id: 1,
name: "Test".to_string(),
domains: vec!["test.com".to_string(), "example.com".to_string()],
};
assert!(provider.has_domain("test.com"));
assert!(provider.has_domain("example.com"));
assert!(!provider.has_domain("other.com"));
}
}