use std::sync::{Arc, Mutex, PoisonError};
pub const MID_RUN_PREAMBLE: &str = "The user sent a message while you were working";
#[derive(Debug, Clone, Default)]
pub struct InputQueue {
items: Arc<Mutex<Vec<String>>>,
}
impl InputQueue {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&self, text: impl Into<String>) {
self.lock().push(text.into());
}
#[must_use]
pub fn pending(&self) -> Vec<String> {
self.lock().clone()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
#[must_use]
pub fn take_all(&self) -> Vec<String> {
std::mem::take(&mut *self.lock())
}
pub fn clear(&self) {
self.lock().clear();
}
#[must_use]
pub(crate) fn take_marked(&self) -> Option<String> {
let items = self.take_all();
if items.is_empty() {
return None;
}
Some(format!("{MID_RUN_PREAMBLE}:\n\n{}", items.join("\n\n")))
}
fn lock(&self) -> std::sync::MutexGuard<'_, Vec<String>> {
self.items.lock().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clones_share_one_queue() {
let a = InputQueue::new();
let b = a.clone();
a.push("from a");
assert_eq!(b.pending(), vec!["from a".to_string()]);
assert!(!b.is_empty());
}
#[test]
fn multiple_items_concatenate_under_one_marker() {
let q = InputQueue::new();
q.push("first line");
q.push("second line");
let marked = q.take_marked().expect("something queued");
assert!(marked.starts_with(MID_RUN_PREAMBLE));
assert!(marked.contains("first line"));
assert!(marked.contains("second line"));
assert_eq!(
marked.matches(MID_RUN_PREAMBLE).count(),
1,
"one marker for the whole batch, not one per item"
);
assert!(q.is_empty(), "taking consumes");
}
#[test]
fn an_empty_queue_yields_nothing_to_inject() {
assert!(InputQueue::new().take_marked().is_none());
}
#[test]
fn clear_drops_undelivered_items() {
let q = InputQueue::new();
q.push("abandoned");
q.clear();
assert!(q.is_empty());
assert!(q.take_marked().is_none());
}
}