use std::marker::PhantomData;
use std::time::Duration;
pub const MAX_PREF_LIFETIME: Duration = Duration::from_secs(400 * 24 * 60 * 60);
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Purpose {
Necessary,
Preference,
}
impl Purpose {
pub fn code(self) -> &'static str {
match self {
Purpose::Necessary => "necessary",
Purpose::Preference => "preference",
}
}
}
pub struct Consent;
pub struct Choice;
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PrefKind {
Consent,
Choice,
}
pub struct ClientPref<K> {
info: PrefInfo,
kind: PhantomData<fn() -> K>,
}
impl<K> Clone for ClientPref<K> {
fn clone(&self) -> Self {
*self
}
}
impl<K> Copy for ClientPref<K> {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
pub struct PrefInfo {
pub name: &'static str,
pub kind: PrefKind,
pub purpose: Purpose,
#[serde(serialize_with = "seconds")]
pub lifetime: Duration,
}
fn seconds<S: serde::Serializer>(d: &Duration, s: S) -> Result<S::Ok, S::Error> {
s.serialize_u64(d.as_secs())
}
const fn valid_name(name: &str) -> bool {
let b = name.as_bytes();
if b.is_empty() || b.len() > 40 {
return false;
}
let mut i = 0;
while i < b.len() {
if !(b[i].is_ascii_alphanumeric() || b[i] == b'-' || b[i] == b'_') {
return false;
}
i += 1;
}
true
}
const fn info(
name: &'static str,
kind: PrefKind,
lifetime: Duration,
purpose: Purpose,
) -> PrefInfo {
assert!(
valid_name(name),
"a client preference name has 1 to 40 ASCII letters, digits, `-` or `_`, such as \"video-notice\""
);
assert!(
lifetime.as_secs() >= 1 && lifetime.as_secs() <= MAX_PREF_LIFETIME.as_secs(),
"a client preference lives 1 second to 400 days (zeyn::MAX_PREF_LIFETIME), such as Duration::from_secs(86_400)"
);
PrefInfo {
name,
kind,
purpose,
lifetime,
}
}
impl ClientPref<Consent> {
pub const fn consent(name: &'static str, lifetime: Duration, purpose: Purpose) -> Self {
ClientPref {
info: info(name, PrefKind::Consent, lifetime, purpose),
kind: PhantomData,
}
}
}
impl ClientPref<Choice> {
pub const fn choice(name: &'static str, lifetime: Duration, purpose: Purpose) -> Self {
ClientPref {
info: info(name, PrefKind::Choice, lifetime, purpose),
kind: PhantomData,
}
}
}
impl<K> ClientPref<K> {
pub fn info(&self) -> PrefInfo {
self.info
}
pub(crate) fn attrs(&self, r: &crate::render::Render) -> String {
if let Some(ctx) = r.ctx
&& !ctx
.service::<ClientPrefs>()
.is_some_and(|p| {
p.keys
.iter()
.any(|k| k.name == self.info.name)
})
{
tracing::warn!(
key = self.info.name,
"client preference key is not registered with App::client_prefs; it is not remembered"
);
return String::new();
}
format!(
r#" data-zeyn-remember="{}" data-zeyn-remember-for="{}" data-zeyn-remember-purpose="{}""#,
self.info.name,
self.info.lifetime.as_millis(),
self.info.purpose.code()
)
}
}
#[cfg(feature = "server")]
type ConsentHook = std::sync::Arc<dyn Fn(&axum::http::HeaderMap) -> bool + Send + Sync>;
#[derive(Clone, Default)]
pub struct ClientPrefs {
pub(crate) keys: Vec<PrefInfo>,
#[cfg(feature = "server")]
pub(crate) allowed: Option<ConsentHook>,
}
impl crate::ctx::Service for ClientPrefs {}
impl ClientPrefs {
pub fn key<K>(mut self, key: ClientPref<K>) -> Self {
self.keys.push(key.info);
self
}
#[cfg(feature = "server")]
pub fn allowed_when(
mut self,
hook: impl Fn(&axum::http::HeaderMap) -> bool + Send + Sync + 'static,
) -> Self {
self.allowed = Some(std::sync::Arc::new(hook));
self
}
pub(crate) fn problems(&self) -> Vec<String> {
self.keys
.iter()
.enumerate()
.filter(|(i, k)| {
self.keys[..*i]
.iter()
.any(|o| o.name == k.name)
})
.map(|(_, k)| {
format!(
"client preference `{}` is registered twice\n = help: register each key once with App::client_prefs",
k.name
)
})
.collect()
}
}