use std::collections::BTreeMap;
use std::sync::{Arc, RwLock};
use crate::title::Title;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateFetch {
Found(Arc<str>),
Missing,
Unknown,
}
pub trait TemplateSource: Send + Sync {
fn fetch(&self, title: &Title) -> TemplateFetch;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyTemplateSource;
impl TemplateSource for EmptyTemplateSource {
fn fetch(&self, _title: &Title) -> TemplateFetch {
TemplateFetch::Unknown
}
}
#[derive(Debug, Default)]
pub struct InMemoryTemplateStore {
pages: RwLock<BTreeMap<Title, Option<Arc<str>>>>,
}
impl InMemoryTemplateStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&self, title: Title, wikitext: impl Into<Arc<str>>) {
self.write().insert(title, Some(wikitext.into()));
}
pub fn insert_missing(&self, title: Title) {
self.write().insert(title, None);
}
pub fn extend(&self, pages: impl IntoIterator<Item = (Title, Option<Arc<str>>)>) {
self.write().extend(pages);
}
#[must_use]
pub fn len(&self) -> usize {
self.read().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.read().is_empty()
}
#[allow(
clippy::expect_used,
reason = "the lock is only poisoned if a holder panicked; propagating that panic is correct"
)]
fn read(&self) -> std::sync::RwLockReadGuard<'_, BTreeMap<Title, Option<Arc<str>>>> {
self.pages.read().expect("template store lock poisoned")
}
#[allow(
clippy::expect_used,
reason = "the lock is only poisoned if a holder panicked; propagating that panic is correct"
)]
fn write(&self) -> std::sync::RwLockWriteGuard<'_, BTreeMap<Title, Option<Arc<str>>>> {
self.pages.write().expect("template store lock poisoned")
}
}
impl TemplateSource for InMemoryTemplateStore {
fn fetch(&self, title: &Title) -> TemplateFetch {
match self.read().get(title) {
Some(Some(text)) => TemplateFetch::Found(Arc::clone(text)),
Some(None) => TemplateFetch::Missing,
None => TemplateFetch::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExistenceRecord {
pub exists: bool,
pub hidden_category: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct ExistenceMap {
records: BTreeMap<Title, ExistenceRecord>,
}
impl ExistenceMap {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, title: Title, record: ExistenceRecord) {
self.records.insert(title, record);
}
#[must_use]
pub fn get(&self, title: &Title) -> Option<ExistenceRecord> {
self.records.get(title).copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.records.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
impl FromIterator<(Title, ExistenceRecord)> for ExistenceMap {
fn from_iter<I: IntoIterator<Item = (Title, ExistenceRecord)>>(iter: I) -> Self {
Self {
records: iter.into_iter().collect(),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("existence lookup failed: {0}")]
pub struct ProviderError(pub String);
pub trait ExistenceProvider: Send + Sync {
fn lookup(&self, titles: &[Title]) -> Result<ExistenceMap, ProviderError>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{NamespaceDef, SiteConfig};
fn config() -> SiteConfig {
SiteConfig::builder("en", "Wiki")
.namespace(NamespaceDef::main())
.namespace(NamespaceDef::new(
10,
"Template",
"Template",
Vec::<String>::new(),
true,
))
.build()
.unwrap()
}
fn title(text: &str) -> Title {
Title::parse(text, &config()).unwrap()
}
#[test]
fn empty_source_answers_unknown() {
assert_eq!(
EmptyTemplateSource.fetch(&title("Template:X")),
TemplateFetch::Unknown
);
}
#[test]
fn in_memory_store_distinguishes_found_missing_unknown() {
let store = InMemoryTemplateStore::new();
assert!(store.is_empty());
store.insert(title("Template:A"), "body A");
store.insert_missing(title("Template:B"));
assert_eq!(
store.fetch(&title("Template:A")),
TemplateFetch::Found(Arc::from("body A"))
);
assert_eq!(store.fetch(&title("Template:B")), TemplateFetch::Missing);
assert_eq!(store.fetch(&title("Template:C")), TemplateFetch::Unknown);
assert_eq!(store.len(), 2);
assert!(!store.is_empty());
}
#[test]
fn extend_bulk_loads_present_and_missing() {
let store = InMemoryTemplateStore::new();
store.extend([
(title("Template:A"), Some(Arc::from("a"))),
(title("Template:B"), None),
]);
assert_eq!(
store.fetch(&title("Template:A")),
TemplateFetch::Found(Arc::from("a"))
);
assert_eq!(store.fetch(&title("Template:B")), TemplateFetch::Missing);
assert!(format!("{store:?}").contains("InMemoryTemplateStore"));
}
#[test]
fn existence_map_records_and_reads_facts() {
let mut map = ExistenceMap::new();
assert!(map.is_empty());
map.insert(
title("A"),
ExistenceRecord {
exists: true,
hidden_category: Some(false),
},
);
assert_eq!(
map.get(&title("A")),
Some(ExistenceRecord {
exists: true,
hidden_category: Some(false)
})
);
assert_eq!(map.get(&title("B")), None);
assert_eq!(map.len(), 1);
let collected: ExistenceMap = [(
title("C"),
ExistenceRecord {
exists: false,
hidden_category: None,
},
)]
.into_iter()
.collect();
assert_eq!(collected.get(&title("C")).map(|r| r.exists), Some(false));
assert!(format!("{map:?}").contains("ExistenceMap"));
}
#[test]
fn generic_inserts_and_collectors_monomorphize_over_owned_and_borrowed() {
let store = InMemoryTemplateStore::new();
store.insert(title("Template:Owned"), String::from("owned body"));
store.insert(title("Template:Arced"), Arc::<str>::from("arc body"));
assert_eq!(
store.fetch(&title("Template:Owned")),
TemplateFetch::Found(Arc::from("owned body"))
);
let pages: Vec<(Title, Option<Arc<str>>)> = vec![
(title("Template:V1"), Some(Arc::from("v1"))),
(title("Template:V2"), None),
];
store.extend(pages);
assert_eq!(store.fetch(&title("Template:V2")), TemplateFetch::Missing);
let map: ExistenceMap = vec![(
title("Template:V1"),
ExistenceRecord {
exists: true,
hidden_category: None,
},
)]
.into_iter()
.collect();
assert_eq!(map.get(&title("Template:V1")).map(|r| r.exists), Some(true));
}
#[test]
fn provider_error_renders() {
let err = ProviderError("api down".to_owned());
assert_eq!(err.to_string(), "existence lookup failed: api down");
}
#[test]
fn fetch_and_records_are_debuggable_and_comparable() {
assert_ne!(TemplateFetch::Missing, TemplateFetch::Unknown);
assert_eq!(
format!(
"{:?}",
ExistenceRecord {
exists: true,
hidden_category: None
}
),
format!(
"{:?}",
ExistenceRecord {
exists: true,
hidden_category: None
}
)
);
assert!(EmptyTemplateSource.fetch(&title("X")) == TemplateFetch::Unknown);
let _ = EmptyTemplateSource;
}
}