use serde::{Deserialize, Serialize};
#[path = "units.rs"]
pub mod units;
pub use units::AGE_ADULT_FLOOR_YR;
const BLOCKED_TAGS: &[&str] = &["explicit", "sexual", "nudity", "adult"];
const AGE_PARAM_NAMES: &[&str] = &["age", "age_years", "ageyears"];
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum PolicyProfile {
Strict,
Standard,
}
#[derive(Debug, Clone)]
pub struct Policy {
pub profile: PolicyProfile,
pub allowlist: Vec<String>,
}
impl Policy {
pub fn new(profile: PolicyProfile) -> Self {
Policy {
profile,
allowlist: Vec::new(),
}
}
pub fn with_allowlist(profile: PolicyProfile, allowlist: Vec<String>) -> Self {
Policy { profile, allowlist }
}
pub fn is_target_allowed(&self, name: &str, tags: &[&str]) -> bool {
for tag in tags {
let tag_lower = tag.to_lowercase();
for blocked in BLOCKED_TAGS {
if tag_lower.contains(blocked) {
return false;
}
}
}
let name_lower = name.to_lowercase();
for blocked in BLOCKED_TAGS {
if name_lower.contains(blocked) {
return false;
}
}
match self.profile {
PolicyProfile::Standard => true,
PolicyProfile::Strict => self.allowlist.iter().any(|a| a == name),
}
}
pub fn param_floor(&self, name: &str) -> Option<f32> {
let name_lower = name.to_lowercase();
if AGE_PARAM_NAMES.iter().any(|n| *n == name_lower) {
return Some(AGE_ADULT_FLOOR_YR);
}
None
}
pub fn is_param_value_allowed(&self, name: &str, value: f32) -> bool {
match self.param_floor(name) {
Some(floor) => value >= floor,
None => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_allows_normal_targets() {
let p = Policy::new(PolicyProfile::Standard);
assert!(p.is_target_allowed("height", &["body", "shape"]));
assert!(p.is_target_allowed("weight", &[]));
}
#[test]
fn blocks_explicit_tags() {
let p = Policy::new(PolicyProfile::Standard);
assert!(!p.is_target_allowed("test", &["explicit"]));
assert!(!p.is_target_allowed("test", &["sexual-content"]));
assert!(!p.is_target_allowed("explicit-body", &[]));
}
#[test]
fn strict_blocks_unlisted() {
let p = Policy::with_allowlist(
PolicyProfile::Strict,
vec!["height".to_string(), "weight".to_string()],
);
assert!(p.is_target_allowed("height", &[]));
assert!(!p.is_target_allowed("muscle", &[]));
}
#[test]
fn age_floor_enforced_on_both_profiles() {
for profile in [PolicyProfile::Standard, PolicyProfile::Strict] {
let p = Policy::new(profile);
assert_eq!(p.param_floor("age"), Some(AGE_ADULT_FLOOR_YR));
assert_eq!(p.param_floor("Age"), Some(AGE_ADULT_FLOOR_YR));
assert_eq!(p.param_floor("age_years"), Some(AGE_ADULT_FLOOR_YR));
}
}
#[test]
fn non_age_params_have_no_floor() {
let p = Policy::new(PolicyProfile::Standard);
assert_eq!(p.param_floor("height"), None);
assert_eq!(p.param_floor("weight"), None);
assert!(p.is_param_value_allowed("height", -5.0));
}
#[test]
fn age_below_floor_rejected() {
let p = Policy::new(PolicyProfile::Strict);
assert!(!p.is_param_value_allowed("age", 17.9));
assert!(!p.is_param_value_allowed("age", 5.0));
assert!(!p.is_param_value_allowed("age", 0.0));
}
#[test]
fn age_at_or_above_floor_allowed() {
let p = Policy::new(PolicyProfile::Standard);
assert!(p.is_param_value_allowed("age", 18.0));
assert!(p.is_param_value_allowed("age", 25.0));
assert!(p.is_param_value_allowed("age", 90.0));
}
}