use hashbrown::HashSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LegalContext {
pub data_transfer_allowed: bool,
pub gdpr_region: bool,
pub ccpa_applies: bool,
pub lgpd_applies: bool,
pub required_regions: Option<HashSet<String>>,
pub allowed_regions: Option<HashSet<String>>,
pub blocked_regions: HashSet<String>,
pub required_certifications: HashSet<String>,
pub data_classification: DataClassification,
pub purpose_flags: HashSet<String>,
pub user_consent: bool,
pub audit_required: bool,
pub retention_days: u32,
}
impl LegalContext {
#[must_use]
pub fn new() -> Self {
Self {
data_transfer_allowed: true,
gdpr_region: false,
ccpa_applies: false,
lgpd_applies: false,
required_regions: None,
allowed_regions: None,
blocked_regions: HashSet::new(),
required_certifications: HashSet::new(),
data_classification: DataClassification::Public,
purpose_flags: HashSet::new(),
user_consent: true,
audit_required: false,
retention_days: 0,
}
}
#[must_use]
pub fn gdpr() -> Self {
let mut allowed = HashSet::new();
for code in [
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE",
"IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS",
"LI", "NO", "CH", "GB", "JP", "KR", "CA", "NZ", "AR", "IL",
] {
allowed.insert(code.to_string());
}
Self {
data_transfer_allowed: true,
gdpr_region: true,
ccpa_applies: false,
lgpd_applies: false,
required_regions: None,
allowed_regions: Some(allowed),
blocked_regions: HashSet::new(),
required_certifications: HashSet::new(),
data_classification: DataClassification::Internal,
purpose_flags: HashSet::new(),
user_consent: false, audit_required: true,
retention_days: 0,
}
}
#[must_use]
pub fn ccpa() -> Self {
Self {
data_transfer_allowed: true,
gdpr_region: false,
ccpa_applies: true,
lgpd_applies: false,
required_regions: None,
allowed_regions: None,
blocked_regions: HashSet::new(),
required_certifications: HashSet::new(),
data_classification: DataClassification::Internal,
purpose_flags: HashSet::new(),
user_consent: true, audit_required: true,
retention_days: 0,
}
}
pub fn block_region(&mut self, region: impl Into<String>) {
self.blocked_regions.insert(region.into());
}
pub fn require_region(&mut self, region: impl Into<String>) {
let region = region.into();
if self.required_regions.is_none() {
self.required_regions = Some(HashSet::new());
}
if let Some(ref mut regions) = self.required_regions {
regions.insert(region);
}
}
#[must_use]
pub fn is_region_allowed(&self, region: &str) -> bool {
if self.blocked_regions.contains(region) {
return false;
}
if let Some(ref required) = self.required_regions {
if !required.contains(region) {
return false;
}
}
if let Some(ref allowed) = self.allowed_regions {
if !allowed.contains(region) {
return false;
}
}
true
}
#[must_use]
pub fn can_transfer_to(&self, region: &str) -> bool {
if !self.data_transfer_allowed {
return false;
}
self.is_region_allowed(region)
}
#[must_use]
pub fn dominant_regulation(&self) -> Regulation {
if self.gdpr_region {
Regulation::Gdpr
} else if self.lgpd_applies {
Regulation::Lgpd
} else if self.ccpa_applies {
Regulation::Ccpa
} else {
Regulation::None
}
}
#[must_use]
pub fn is_purpose_allowed(&self, purpose: &str) -> bool {
if self.purpose_flags.is_empty() {
return true;
}
self.purpose_flags.contains(purpose)
}
pub fn allow_purpose(&mut self, purpose: impl Into<String>) {
self.purpose_flags.insert(purpose.into());
}
#[must_use]
pub fn compliance_score(&self) -> f32 {
let mut score = 1.0;
if !self.user_consent {
score *= 0.1;
}
if !self.required_certifications.is_empty() {
score *= 0.5; }
match self.data_classification {
DataClassification::Public => {}
DataClassification::Internal => score *= 0.9,
DataClassification::Confidential => score *= 0.7,
DataClassification::Restricted => score *= 0.3,
}
score
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DataClassification {
#[default]
Public,
Internal,
Confidential,
Restricted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Regulation {
None,
Gdpr,
Ccpa,
Lgpd,
Pipl,
Appi,
}
#[cfg(feature = "legal")]
impl LegalContext {
pub fn from_legalis_jurisdiction(jurisdiction: &str) -> Self {
match jurisdiction.to_uppercase().as_str() {
"EU" | "EEA" => Self::gdpr(),
"US-CA" | "CALIFORNIA" => Self::ccpa(),
"BR" | "BRAZIL" => {
let mut ctx = Self::new();
ctx.lgpd_applies = true;
ctx.audit_required = true;
ctx
}
_ => Self::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_legal_context() {
let ctx = LegalContext::gdpr();
assert!(ctx.gdpr_region);
assert!(ctx.audit_required);
assert!(ctx.is_region_allowed("DE"));
assert!(ctx.is_region_allowed("JP")); }
#[test]
fn test_blocked_regions() {
let mut ctx = LegalContext::new();
ctx.block_region("XX");
assert!(ctx.is_region_allowed("US"));
assert!(!ctx.is_region_allowed("XX"));
}
#[test]
fn test_required_regions() {
let mut ctx = LegalContext::new();
ctx.require_region("EU");
ctx.require_region("US");
assert!(ctx.is_region_allowed("EU"));
assert!(ctx.is_region_allowed("US"));
assert!(!ctx.is_region_allowed("JP"));
}
#[test]
fn test_data_transfer() {
let gdpr = LegalContext::gdpr();
assert!(gdpr.can_transfer_to("DE"));
assert!(gdpr.can_transfer_to("JP"));
assert!(!gdpr.can_transfer_to("XX"));
let mut blocked = LegalContext::new();
blocked.data_transfer_allowed = false;
assert!(!blocked.can_transfer_to("US"));
}
#[test]
fn test_dominant_regulation() {
assert_eq!(LegalContext::gdpr().dominant_regulation(), Regulation::Gdpr);
assert_eq!(LegalContext::ccpa().dominant_regulation(), Regulation::Ccpa);
assert_eq!(LegalContext::new().dominant_regulation(), Regulation::None);
}
#[test]
fn test_compliance_score() {
let full_consent = LegalContext::new();
assert!(full_consent.compliance_score() > 0.9);
let mut no_consent = LegalContext::new();
no_consent.user_consent = false;
assert!(no_consent.compliance_score() < 0.2);
}
}