use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GlobalVendorList {
pub gvl_specification_version: u8,
pub vendor_list_version: u16,
pub tcf_policy_version: u8,
pub last_updated: String,
pub purposes: HashMap<u8, Purpose>,
pub special_purposes: HashMap<u8, SpecialPurpose>,
pub features: HashMap<u8, Feature>,
pub special_features: HashMap<u8, SpecialFeature>,
pub vendors: HashMap<u16, Vendor>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Purpose {
pub id: u8,
pub name: String,
pub description: String,
pub description_legal: String,
pub consentable: bool,
pub right_to_object: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpecialPurpose {
pub id: u8,
pub name: String,
pub description: String,
pub description_legal: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Feature {
pub id: u8,
pub name: String,
pub description: String,
pub description_legal: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpecialFeature {
pub id: u8,
pub name: String,
pub description: String,
pub description_legal: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vendor {
pub id: u16,
pub name: String,
pub purposes: Vec<u8>,
pub special_purposes: Vec<u8>,
pub leg_int_purposes: Vec<u8>,
pub flexible_purposes: Vec<u8>,
pub features: Vec<u8>,
pub special_features: Vec<u8>,
pub policy_url: String,
pub deleted_date: Option<String>,
#[serde(default)]
pub overflow: Option<Overflow>,
#[serde(default)]
pub cookie_max_age_seconds: Option<u64>,
#[serde(default)]
pub cookie_refresh: Option<bool>,
pub uses_cookies: bool,
pub uses_non_cookie_access: bool,
pub data_declaration: Vec<u8>,
#[serde(default)]
pub data_retention: Option<DataRetention>,
#[serde(default)]
pub std_retention: Option<u16>,
pub urls: Vec<VendorUrl>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Overflow {
pub http_get_limit: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataRetention {
pub purposes: HashMap<u8, u16>,
pub special_purposes: HashMap<u8, u16>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VendorUrl {
pub lang_id: String,
pub privacy: String,
#[serde(default)]
pub leg_int_claim: Option<String>,
}
impl GlobalVendorList {
const BASE_URL: &'static str = "https://vendor-list.consensu.org/v2";
pub async fn fetch_latest() -> Result<Self> {
let url = format!("{}/vendor-list.json", Self::BASE_URL);
Self::fetch_from_url(&url).await
}
pub async fn fetch_version(version: u16) -> Result<Self> {
let url = format!("{}/archives/vendor-list-v{}.json", Self::BASE_URL, version);
Self::fetch_from_url(&url).await
}
async fn fetch_from_url(url: &str) -> Result<Self> {
let response = reqwest::get(url)
.await
.map_err(|e| Error::NetworkError(e.to_string()))?;
if !response.status().is_success() {
return Err(Error::GVLFetchError(response.status().as_u16()));
}
let gvl: GlobalVendorList = response
.json()
.await
.map_err(|e| Error::GVLParseError(e.to_string()))?;
Ok(gvl)
}
pub fn from_json(json: &str) -> Result<Self> {
serde_json::from_str(json).map_err(|e| Error::GVLParseError(e.to_string()))
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(|e| Error::SerializationError(e.to_string()))
}
pub fn to_json_pretty(&self) -> Result<String> {
serde_json::to_string_pretty(self).map_err(|e| Error::SerializationError(e.to_string()))
}
#[must_use]
pub fn get_vendor(&self, vendor_id: u16) -> Option<&Vendor> {
self.vendors.get(&vendor_id)
}
#[must_use]
pub fn has_vendor(&self, vendor_id: u16) -> bool {
self.vendors.contains_key(&vendor_id)
}
#[must_use]
pub fn get_active_vendors(&self) -> Vec<&Vendor> {
self.vendors
.values()
.filter(|v| v.deleted_date.is_none())
.collect()
}
#[must_use]
pub fn get_vendor_ids(&self) -> Vec<u16> {
self.vendors.keys().copied().collect()
}
#[must_use]
pub fn get_purpose(&self, purpose_id: u8) -> Option<&Purpose> {
self.purposes.get(&purpose_id)
}
#[must_use]
pub fn get_special_purpose(&self, purpose_id: u8) -> Option<&SpecialPurpose> {
self.special_purposes.get(&purpose_id)
}
#[must_use]
pub fn get_feature(&self, feature_id: u8) -> Option<&Feature> {
self.features.get(&feature_id)
}
#[must_use]
pub fn get_special_feature(&self, feature_id: u8) -> Option<&SpecialFeature> {
self.special_features.get(&feature_id)
}
#[must_use]
pub fn validate_vendor_ids(&self, vendor_ids: &[u16]) -> Vec<u16> {
vendor_ids
.iter()
.filter(|&&id| !self.has_vendor(id))
.copied()
.collect()
}
}
impl Vendor {
#[must_use]
pub fn requires_consent(&self, purpose_id: u8) -> bool {
self.purposes.contains(&purpose_id)
}
#[must_use]
pub fn uses_legitimate_interest(&self, purpose_id: u8) -> bool {
self.leg_int_purposes.contains(&purpose_id)
}
#[must_use]
pub fn uses_special_purpose(&self, purpose_id: u8) -> bool {
self.special_purposes.contains(&purpose_id)
}
#[must_use]
pub fn is_flexible_purpose(&self, purpose_id: u8) -> bool {
self.flexible_purposes.contains(&purpose_id)
}
#[must_use]
pub fn is_deleted(&self) -> bool {
self.deleted_date.is_some()
}
#[must_use]
pub fn get_privacy_url(&self, lang: &str) -> Option<&str> {
self.urls
.iter()
.find(|u| u.lang_id.eq_ignore_ascii_case(lang))
.map(|u| u.privacy.as_str())
.or(Some(self.policy_url.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_gvl() -> GlobalVendorList {
let mut vendors = HashMap::new();
vendors.insert(
1,
Vendor {
id: 1,
name: "Test Vendor 1".to_string(),
purposes: vec![1, 2, 3],
special_purposes: vec![1],
leg_int_purposes: vec![7, 8],
flexible_purposes: vec![2],
features: vec![1],
special_features: vec![],
policy_url: "https://example.com/privacy".to_string(),
deleted_date: None,
overflow: None,
cookie_max_age_seconds: Some(31_536_000),
cookie_refresh: Some(true),
uses_cookies: true,
uses_non_cookie_access: false,
data_declaration: vec![1, 2, 3],
data_retention: None,
std_retention: Some(180),
urls: vec![VendorUrl {
lang_id: "en".to_string(),
privacy: "https://example.com/en/privacy".to_string(),
leg_int_claim: None,
}],
},
);
GlobalVendorList {
gvl_specification_version: 2,
vendor_list_version: 1,
tcf_policy_version: 2,
last_updated: "2024-01-01T00:00:00Z".to_string(),
purposes: HashMap::new(),
special_purposes: HashMap::new(),
features: HashMap::new(),
special_features: HashMap::new(),
vendors,
}
}
#[test]
fn test_gvl_creation() {
let gvl = create_test_gvl();
assert_eq!(gvl.gvl_specification_version, 2);
assert_eq!(gvl.vendor_list_version, 1);
assert_eq!(gvl.vendors.len(), 1);
}
#[test]
fn test_get_vendor() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1);
assert!(vendor.is_some());
assert_eq!(vendor.unwrap().name, "Test Vendor 1");
let vendor = gvl.get_vendor(999);
assert!(vendor.is_none());
}
#[test]
fn test_has_vendor() {
let gvl = create_test_gvl();
assert!(gvl.has_vendor(1));
assert!(!gvl.has_vendor(999));
}
#[test]
fn test_get_active_vendors() {
let gvl = create_test_gvl();
let active = gvl.get_active_vendors();
assert_eq!(active.len(), 1);
}
#[test]
fn test_validate_vendor_ids() {
let gvl = create_test_gvl();
let valid = vec![1];
let unknown = gvl.validate_vendor_ids(&valid);
assert!(unknown.is_empty());
let invalid = vec![1, 999, 1000];
let unknown = gvl.validate_vendor_ids(&invalid);
assert_eq!(unknown.len(), 2);
assert!(unknown.contains(&999));
assert!(unknown.contains(&1000));
}
#[test]
fn test_vendor_requires_consent() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1).unwrap();
assert!(vendor.requires_consent(1));
assert!(vendor.requires_consent(2));
assert!(!vendor.requires_consent(4));
}
#[test]
fn test_vendor_uses_legitimate_interest() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1).unwrap();
assert!(vendor.uses_legitimate_interest(7));
assert!(vendor.uses_legitimate_interest(8));
assert!(!vendor.uses_legitimate_interest(1));
}
#[test]
fn test_vendor_special_purpose() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1).unwrap();
assert!(vendor.uses_special_purpose(1));
assert!(!vendor.uses_special_purpose(2));
}
#[test]
fn test_vendor_flexible_purpose() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1).unwrap();
assert!(vendor.is_flexible_purpose(2));
assert!(!vendor.is_flexible_purpose(1));
}
#[test]
fn test_vendor_privacy_url() {
let gvl = create_test_gvl();
let vendor = gvl.get_vendor(1).unwrap();
let url = vendor.get_privacy_url("en");
assert!(url.is_some());
assert_eq!(url.unwrap(), "https://example.com/en/privacy");
let url = vendor.get_privacy_url("fr");
assert!(url.is_some());
assert_eq!(url.unwrap(), "https://example.com/privacy"); }
#[test]
fn test_json_serialization() -> Result<()> {
let gvl = create_test_gvl();
let json = gvl.to_json()?;
let parsed = GlobalVendorList::from_json(&json)?;
assert_eq!(gvl.vendor_list_version, parsed.vendor_list_version);
assert_eq!(gvl.vendors.len(), parsed.vendors.len());
Ok(())
}
}