use std::sync::LazyLock;
const TIPS_TOML: &str = include_str!("../tips.toml");
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
struct TipsFile {
#[serde(default)]
categories: Vec<Category>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
struct Category {
name: String,
#[serde(default)]
tips: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tip {
pub category: String,
pub text: String,
}
static TIPS: LazyLock<Vec<Tip>> = LazyLock::new(|| parse(TIPS_TOML).unwrap_or_default());
#[must_use]
pub fn all() -> &'static [Tip] {
&TIPS
}
pub fn parse(text: &str) -> std::result::Result<Vec<Tip>, String> {
let file: TipsFile = toml::from_str(text).map_err(|e| e.to_string())?;
let mut tips = Vec::new();
for category in file.categories {
for text in category.tips {
tips.push(Tip {
category: category.name.clone(),
text,
});
}
}
Ok(tips)
}
#[must_use]
pub fn random_index(len: usize, seed: u128) -> usize {
if len == 0 {
return 0;
}
let mut x = seed as u64;
x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
x ^= x >> 31;
(x % len as u64) as usize
}
#[must_use]
pub fn random_start(len: usize) -> usize {
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos());
random_index(len, seed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_tips_are_well_formed_and_plentiful() {
let tips = parse(TIPS_TOML).expect("tips.toml must parse");
assert!(
tips.len() >= 100,
"tix #1632 asks for at least 100 tips, found {}",
tips.len()
);
let categories: std::collections::BTreeSet<&str> =
tips.iter().map(|t| t.category.as_str()).collect();
assert!(
categories.len() >= 10,
"the tips must be categorized, found {} categories",
categories.len()
);
for tip in &tips {
assert!(!tip.text.trim().is_empty(), "a tip is blank");
assert!(
!tip.category.trim().is_empty(),
"a tip (\"{}\") has no category",
tip.text
);
}
let unique: std::collections::BTreeSet<&str> =
tips.iter().map(|t| t.text.as_str()).collect();
assert_eq!(unique.len(), tips.len(), "a tip is duplicated");
}
#[test]
fn all_is_populated_from_the_embedded_file() {
assert_eq!(all().len(), parse(TIPS_TOML).unwrap().len());
}
#[test]
fn parse_rejects_a_malformed_file() {
assert!(parse("categories = 3").is_err());
}
#[test]
fn an_empty_category_list_yields_no_tips() {
assert_eq!(parse("").unwrap(), [] as [crate::tips::Tip; 0]);
}
#[test]
fn random_index_is_in_range_and_total_at_zero() {
assert_eq!(random_index(0, 12345), 0);
for seed in 0..1000u128 {
let index = random_index(7, seed);
assert!(index < 7, "index {index} out of range");
}
assert!(random_start(10) < 10);
}
#[test]
fn random_index_spreads_its_seeds() {
let bucket: std::collections::BTreeSet<usize> =
(0..64u128).map(|seed| random_index(16, seed)).collect();
assert!(
bucket.len() > 4,
"the seed mix is not spreading: {bucket:?}"
);
}
}