use alloc::string::String;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex, PoisonError};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(
not(all(feature = "axum", feature = "ssr")),
allow(
dead_code,
reason = "a server without Leptos, or Leptos without `axum`, gives some"
)
)]
pub(crate) enum Kind {
NoCatalogs = 0,
Unrequested = 1,
UnknownLocale = 2,
Unmatched = 3,
RedirectOutside = 4,
}
static DONE: [AtomicBool; 5] = [
AtomicBool::new(false),
AtomicBool::new(false),
AtomicBool::new(false),
AtomicBool::new(false),
AtomicBool::new(false),
];
static SEEN: Mutex<Vec<(Kind, String)>> = Mutex::new(Vec::new());
const KEYS_PER_KIND: usize = 32;
#[cfg_attr(not(feature = "ssr"), allow(dead_code))]
pub(crate) fn pending(kind: Kind) -> bool {
!flag(kind).load(Ordering::Relaxed)
}
fn flag(kind: Kind) -> &'static AtomicBool {
match kind {
Kind::NoCatalogs => &DONE[0],
Kind::Unrequested => &DONE[1],
Kind::UnknownLocale => &DONE[2],
Kind::Unmatched => &DONE[3],
Kind::RedirectOutside => &DONE[4],
}
}
pub(crate) fn once(kind: Kind, message: impl FnOnce() -> String) {
if !flag(kind).swap(true, Ordering::Relaxed) {
emit(kind, &message());
}
}
pub(crate) fn once_for(kind: Kind, key: &str, message: impl FnOnce() -> String) {
{
let mut seen = SEEN.lock().unwrap_or_else(PoisonError::into_inner);
if seen.iter().any(|(k, s)| *k == kind && s == key)
|| seen.iter().filter(|(k, _)| *k == kind).count() >= KEYS_PER_KIND
{
return;
}
seen.push((kind, String::from(key)));
}
emit(kind, &message());
}
pub(crate) fn tag(text: &str) -> Option<&str> {
(!text.is_empty()
&& text.len() <= 35
&& text
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'))
.then_some(text)
}
fn emit(kind: Kind, line: &str) {
std::eprintln!("{line}");
#[cfg(test)]
LOG.lock()
.unwrap_or_else(PoisonError::into_inner)
.push((kind, String::from(line)));
#[cfg(not(test))]
let _ = kind;
}
#[cfg(test)]
static LOG: Mutex<Vec<(Kind, String)>> = Mutex::new(Vec::new());
#[cfg(test)]
pub(crate) fn given(kind: Kind) -> Vec<String> {
LOG.lock()
.unwrap_or_else(PoisonError::into_inner)
.iter()
.filter(|(k, _)| *k == kind)
.map(|(_, line)| line.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::{Kind, given, once_for, tag};
use alloc::string::String;
#[test]
fn a_keyed_warning_is_given_once_per_key_and_the_keys_are_bounded() {
for _ in 0..3 {
once_for(Kind::UnknownLocale, "warn-test-a", || {
String::from("warn-test-a")
});
once_for(Kind::UnknownLocale, "warn-test-b", || {
String::from("warn-test-b")
});
}
let lines = given(Kind::UnknownLocale);
assert_eq!(lines.iter().filter(|l| *l == "warn-test-a").count(), 1);
assert_eq!(lines.iter().filter(|l| *l == "warn-test-b").count(), 1);
for n in 0..100 {
let key = alloc::format!("warn-test-many-{n}");
once_for(Kind::UnknownLocale, &key, || key.clone());
}
assert!(given(Kind::UnknownLocale).len() <= super::KEYS_PER_KIND);
}
#[test]
fn only_tag_shaped_text_is_named() {
assert_eq!(tag("zh-Hant-TW"), Some("zh-Hant-TW"));
assert_eq!(tag("*"), None);
assert_eq!(tag("en\n[forged]"), None);
assert_eq!(tag(""), None);
}
}