//! Which storyline a visitor reads (see `content/variants.rs`): one //! index for the whole site, chosen at their first request and kept in //! a cookie for a year, so every page and every mail they get tells //! the same story. A site that tells one story sets no cookie. #![cfg(feature = "ssr")] use axum::{ extract::{Request, State}, http::header::{COOKIE, SET_COOKIE}, middleware::Next, response::Response, }; pub const COOKIE_NAME: &str = "portal_storyline"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Storyline(pub usize); /// The index in a request's cookies, if it names one this site tells. fn from_cookies(header: Option<&str>, arity: usize) -> Option { header? .split(';') .map(str::trim) .find_map(|kv| kv.strip_prefix(COOKIE_NAME).and_then(|rest| rest.strip_prefix('='))) .and_then(|v| v.trim().parse::().ok()) .filter(|n| *n < arity) } /// Evenly, from nothing about the person. fn pick(arity: usize) -> usize { (uuid::Uuid::new_v4().as_u128() % arity.max(1) as u128) as usize } pub async fn assign(State(state): State, mut req: Request, next: Next) -> Response { let arity = state.questions.load().arity(); let held = from_cookies(req.headers().get(COOKIE).and_then(|v| v.to_str().ok()), arity); let n = held.unwrap_or_else(|| pick(arity)); req.extensions_mut().insert(Storyline(n)); let mut res = next.run(req).await; if held.is_none() && arity > 1 { if let Ok(value) = format!("{COOKIE_NAME}={n}; Path=/; Max-Age=31536000; SameSite=Lax").parse() { res.headers_mut().append(SET_COOKIE, value); } } res } /// The storyline of the request a server function is answering, or /// the first when there is none to ask (a call outside a request). pub async fn current() -> usize { leptos_axum::extract::>() .await .map(|axum::Extension(Storyline(n))| n) .unwrap_or(0) } #[cfg(test)] mod tests { use super::*; #[test] fn a_cookie_names_a_storyline_the_site_tells() { assert_eq!(from_cookies(Some("a=b; portal_storyline=1; c=d"), 3), Some(1)); assert_eq!(from_cookies(Some("portal_storyline=7"), 3), None); assert_eq!(from_cookies(Some("portal_storylines=1"), 3), None); assert_eq!(from_cookies(None, 3), None); assert!(pick(1) == 0); assert!((0..50).all(|_| pick(3) < 3)); } }