use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::{BitField, Error, Result, VendorSet};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TCModel {
pub core_string: CoreString,
pub disclosed_vendors: Option<DisclosedVendors>,
pub allowed_vendors: Option<AllowedVendors>,
pub publisher_tc: Option<PublisherTC>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CoreString {
pub version: u8,
pub created: DateTime<Utc>,
pub last_updated: DateTime<Utc>,
pub cmp_id: u16,
pub cmp_version: u16,
pub consent_screen: u8,
pub consent_language: String,
pub vendor_list_version: u16,
pub tcf_policy_version: u8,
pub is_service_specific: bool,
pub use_non_standard_texts: bool,
pub special_feature_opt_ins: BitField,
pub purposes_consent: BitField,
pub purposes_li_transparency: BitField,
pub purpose_one_treatment: bool,
pub publisher_cc: String,
pub vendor_consents: VendorSet,
pub vendor_legitimate_interests: VendorSet,
pub publisher_restrictions: PublisherRestrictions,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PublisherRestrictions {
pub num_restrictions: u16,
pub restrictions: Vec<PublisherRestriction>,
}
impl PublisherRestrictions {
#[must_use]
pub fn new() -> Self {
Self {
num_restrictions: 0,
restrictions: Vec::new(),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
num_restrictions: 0,
restrictions: Vec::with_capacity(capacity),
}
}
pub fn add_restriction(&mut self, restriction: PublisherRestriction) {
self.restrictions.push(restriction);
#[allow(clippy::cast_possible_truncation)]
let num = self.restrictions.len() as u16;
self.num_restrictions = num;
}
#[must_use]
pub fn get_restrictions_for_purpose(&self, purpose_id: u8) -> Vec<&PublisherRestriction> {
self.restrictions
.iter()
.filter(|r| r.purpose_id == purpose_id)
.collect()
}
#[must_use]
pub fn is_vendor_restricted(&self, purpose_id: u8, vendor_id: u16) -> Option<RestrictionType> {
self.restrictions
.iter()
.find(|r| r.purpose_id == purpose_id && r.vendors.contains(vendor_id))
.map(|r| r.restriction_type)
}
#[must_use]
pub fn len(&self) -> usize {
self.restrictions.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.restrictions.is_empty()
}
}
impl Default for PublisherRestrictions {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PublisherRestriction {
pub purpose_id: u8,
pub restriction_type: RestrictionType,
pub vendors: VendorSet,
}
impl PublisherRestriction {
pub fn new(
purpose_id: u8,
restriction_type: RestrictionType,
vendors: VendorSet,
) -> Result<Self> {
if purpose_id == 0 || purpose_id > 24 {
return Err(Error::InvalidPurposeId(purpose_id));
}
Ok(Self {
purpose_id,
restriction_type,
vendors,
})
}
#[must_use]
pub fn applies_to_vendor(&self, vendor_id: u16) -> bool {
self.vendors.contains(vendor_id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
pub enum RestrictionType {
NotAllowed = 0,
RequireConsent = 1,
RequireLegitimateInterest = 2,
}
impl RestrictionType {
pub fn from_u8(value: u8) -> Result<Self> {
match value {
0 => Ok(RestrictionType::NotAllowed),
1 => Ok(RestrictionType::RequireConsent),
2 => Ok(RestrictionType::RequireLegitimateInterest),
_ => Err(Error::InvalidRestrictionType(value)),
}
}
#[must_use]
pub fn to_u8(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DisclosedVendors {
pub segment_type: u8,
pub vendors: VendorSet,
}
impl DisclosedVendors {
#[must_use]
pub fn new(vendors: VendorSet) -> Self {
Self {
segment_type: 1,
vendors,
}
}
#[must_use]
pub fn is_vendor_disclosed(&self, vendor_id: u16) -> bool {
self.vendors.contains(vendor_id)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AllowedVendors {
pub segment_type: u8,
pub vendors: VendorSet,
}
impl AllowedVendors {
#[must_use]
pub fn new(vendors: VendorSet) -> Self {
Self {
segment_type: 2,
vendors,
}
}
#[must_use]
pub fn is_vendor_allowed(&self, vendor_id: u16) -> bool {
self.vendors.contains(vendor_id)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PublisherTC {
pub segment_type: u8,
pub pub_purposes_consent: BitField,
pub pub_purposes_li_transparency: BitField,
pub num_custom_purposes: u8,
pub custom_purposes_consent: BitField,
pub custom_purposes_li_transparency: BitField,
}
impl PublisherTC {
#[must_use]
pub fn new(pub_purposes_consent: BitField, pub_purposes_li_transparency: BitField) -> Self {
Self {
segment_type: 3,
pub_purposes_consent,
pub_purposes_li_transparency,
num_custom_purposes: 0,
custom_purposes_consent: BitField::new(0),
custom_purposes_li_transparency: BitField::new(0),
}
}
#[must_use]
pub fn with_custom_purposes(
pub_purposes_consent: BitField,
pub_purposes_li_transparency: BitField,
custom_purposes_consent: BitField,
custom_purposes_li_transparency: BitField,
) -> Self {
let num_custom = custom_purposes_consent.len();
#[allow(clippy::cast_possible_truncation)]
let num_custom_purposes = num_custom as u8;
Self {
segment_type: 3,
pub_purposes_consent,
pub_purposes_li_transparency,
num_custom_purposes,
custom_purposes_consent,
custom_purposes_li_transparency,
}
}
#[must_use]
pub fn has_purpose_consent(&self, purpose_id: u8) -> bool {
if purpose_id == 0 || purpose_id > 24 {
return false;
}
self.pub_purposes_consent.is_set((purpose_id - 1) as usize)
}
#[must_use]
pub fn has_custom_purpose_consent(&self, custom_purpose_id: u8) -> bool {
if custom_purpose_id as usize >= self.num_custom_purposes as usize {
return false;
}
self.custom_purposes_consent
.is_set(custom_purpose_id as usize)
}
}
impl CoreString {
#[must_use]
pub fn new() -> Self {
let now = Utc::now();
Self {
version: 2,
created: now,
last_updated: now,
cmp_id: 0,
cmp_version: 0,
consent_screen: 0,
consent_language: "EN".to_string(),
vendor_list_version: 0,
tcf_policy_version: 2,
is_service_specific: false,
use_non_standard_texts: false,
special_feature_opt_ins: BitField::new(12),
purposes_consent: BitField::new(24),
purposes_li_transparency: BitField::new(24),
purpose_one_treatment: false,
publisher_cc: "AA".to_string(),
vendor_consents: VendorSet::new_bitfield(),
vendor_legitimate_interests: VendorSet::new_bitfield(),
publisher_restrictions: PublisherRestrictions::new(),
}
}
#[must_use]
pub fn has_purpose_consent(&self, purpose_id: u8) -> bool {
if purpose_id == 0 || purpose_id > 24 {
return false;
}
self.purposes_consent.is_set((purpose_id - 1) as usize)
}
#[must_use]
pub fn has_purpose_li_transparency(&self, purpose_id: u8) -> bool {
if purpose_id == 0 || purpose_id > 24 {
return false;
}
self.purposes_li_transparency
.is_set((purpose_id - 1) as usize)
}
#[must_use]
pub fn has_special_feature_opt_in(&self, feature_id: u8) -> bool {
if feature_id == 0 || feature_id > 12 {
return false;
}
self.special_feature_opt_ins
.is_set((feature_id - 1) as usize)
}
#[must_use]
pub fn has_vendor_consent(&self, vendor_id: u16) -> bool {
self.vendor_consents.contains(vendor_id)
}
#[must_use]
pub fn has_vendor_li_transparency(&self, vendor_id: u16) -> bool {
self.vendor_legitimate_interests.contains(vendor_id)
}
}
impl Default for CoreString {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn test_core_string_creation() {
let core = CoreString::new();
assert_eq!(core.version, 2);
assert_eq!(core.tcf_policy_version, 2);
assert_eq!(core.consent_language, "EN");
assert!(!core.is_service_specific);
}
#[test]
fn test_purpose_consent() {
let mut core = CoreString::new();
core.purposes_consent.set(0, true); core.purposes_consent.set(1, true);
assert!(core.has_purpose_consent(1));
assert!(core.has_purpose_consent(2));
assert!(!core.has_purpose_consent(3));
assert!(!core.has_purpose_consent(0));
assert!(!core.has_purpose_consent(25));
}
#[test]
fn test_special_feature_opt_in() {
let mut core = CoreString::new();
core.special_feature_opt_ins.set(0, true); core.special_feature_opt_ins.set(4, true);
assert!(core.has_special_feature_opt_in(1));
assert!(core.has_special_feature_opt_in(5));
assert!(!core.has_special_feature_opt_in(2));
assert!(!core.has_special_feature_opt_in(0));
assert!(!core.has_special_feature_opt_in(13));
}
#[test]
fn test_vendor_consent() {
let mut core = CoreString::new();
let mut vendors = HashSet::new();
vendors.insert(1);
vendors.insert(35);
vendors.insert(100);
core.vendor_consents = VendorSet::BitField(vendors);
assert!(core.has_vendor_consent(1));
assert!(core.has_vendor_consent(35));
assert!(core.has_vendor_consent(100));
assert!(!core.has_vendor_consent(2));
assert!(!core.has_vendor_consent(999));
}
#[test]
fn test_publisher_restrictions_creation() {
let restrictions = PublisherRestrictions::new();
assert_eq!(restrictions.num_restrictions, 0);
assert!(restrictions.is_empty());
}
#[test]
fn test_publisher_restriction_add() -> Result<()> {
let mut restrictions = PublisherRestrictions::new();
let mut vendors = HashSet::new();
vendors.insert(1);
vendors.insert(2);
let vendor_set = VendorSet::BitField(vendors);
let restriction =
PublisherRestriction::new(1, RestrictionType::RequireConsent, vendor_set)?;
restrictions.add_restriction(restriction);
assert_eq!(restrictions.len(), 1);
assert_eq!(restrictions.num_restrictions, 1);
Ok(())
}
#[test]
fn test_publisher_restriction_query() -> Result<()> {
let mut restrictions = PublisherRestrictions::new();
let mut vendors = HashSet::new();
vendors.insert(1);
vendors.insert(2);
let vendor_set = VendorSet::BitField(vendors);
let restriction = PublisherRestriction::new(1, RestrictionType::NotAllowed, vendor_set)?;
restrictions.add_restriction(restriction);
assert_eq!(
restrictions.is_vendor_restricted(1, 1),
Some(RestrictionType::NotAllowed)
);
assert_eq!(
restrictions.is_vendor_restricted(1, 2),
Some(RestrictionType::NotAllowed)
);
assert_eq!(restrictions.is_vendor_restricted(1, 3), None);
assert_eq!(restrictions.is_vendor_restricted(2, 1), None);
Ok(())
}
#[test]
fn test_restriction_type_conversion() -> Result<()> {
assert_eq!(RestrictionType::from_u8(0)?, RestrictionType::NotAllowed);
assert_eq!(
RestrictionType::from_u8(1)?,
RestrictionType::RequireConsent
);
assert_eq!(
RestrictionType::from_u8(2)?,
RestrictionType::RequireLegitimateInterest
);
assert!(RestrictionType::from_u8(3).is_err());
Ok(())
}
#[test]
fn test_disclosed_vendors() {
let mut vendors = HashSet::new();
vendors.insert(1);
vendors.insert(35);
let vendor_set = VendorSet::BitField(vendors);
let disclosed = DisclosedVendors::new(vendor_set);
assert_eq!(disclosed.segment_type, 1);
assert!(disclosed.is_vendor_disclosed(1));
assert!(disclosed.is_vendor_disclosed(35));
assert!(!disclosed.is_vendor_disclosed(100));
}
#[test]
fn test_allowed_vendors() {
let mut vendors = HashSet::new();
vendors.insert(1);
vendors.insert(2);
let vendor_set = VendorSet::BitField(vendors);
let allowed = AllowedVendors::new(vendor_set);
assert_eq!(allowed.segment_type, 2);
assert!(allowed.is_vendor_allowed(1));
assert!(allowed.is_vendor_allowed(2));
assert!(!allowed.is_vendor_allowed(3));
}
#[test]
fn test_publisher_tc_standard_purposes() {
let mut consent = BitField::new(24);
consent.set(0, true); consent.set(2, true);
let mut li_transparency = BitField::new(24);
li_transparency.set(1, true);
let pub_tc = PublisherTC::new(consent, li_transparency);
assert_eq!(pub_tc.segment_type, 3);
assert_eq!(pub_tc.num_custom_purposes, 0);
assert!(pub_tc.has_purpose_consent(1));
assert!(pub_tc.has_purpose_consent(3));
assert!(!pub_tc.has_purpose_consent(2));
}
#[test]
fn test_publisher_tc_custom_purposes() {
let pub_consent = BitField::new(24);
let pub_li = BitField::new(24);
let mut custom_consent = BitField::new(5);
custom_consent.set(0, true); custom_consent.set(2, true);
let custom_li = BitField::new(5);
let pub_tc =
PublisherTC::with_custom_purposes(pub_consent, pub_li, custom_consent, custom_li);
assert_eq!(pub_tc.num_custom_purposes, 5);
assert!(pub_tc.has_custom_purpose_consent(0));
assert!(pub_tc.has_custom_purpose_consent(2));
assert!(!pub_tc.has_custom_purpose_consent(1));
assert!(!pub_tc.has_custom_purpose_consent(5));
}
#[test]
fn test_invalid_purpose_id() {
let vendor_set = VendorSet::new_bitfield();
let result = PublisherRestriction::new(0, RestrictionType::NotAllowed, vendor_set.clone());
assert!(result.is_err());
let result = PublisherRestriction::new(25, RestrictionType::NotAllowed, vendor_set);
assert!(result.is_err());
}
}