71 lines
2.4 KiB
Rust
71 lines
2.4 KiB
Rust
//! 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<usize> {
|
||
|
|
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::<usize>().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<super::AppState>, 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::<axum::Extension<Storyline>>()
|
||
|
|
.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));
|
||
|
|
}
|
||
|
|
}
|