standout-input 9.0.0

Declarative input collection for CLI applications
Documentation
use std::fmt;
use std::sync::Arc;

use crate::env::{ClipboardReader, RealClipboard, RealStdin, StdinReader};
use crate::responder::PromptResponder;

#[derive(Clone)]
pub struct InputSources {
    stdin: Arc<dyn StdinReader>,
    clipboard: Arc<dyn ClipboardReader>,
    responder: Option<Arc<dyn PromptResponder>>,
}

impl fmt::Debug for InputSources {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("InputSources")
            .field("has_responder", &self.responder.is_some())
            .finish_non_exhaustive()
    }
}

impl InputSources {
    pub fn new(
        stdin: impl StdinReader + 'static,
        clipboard: impl ClipboardReader + 'static,
        responder: Option<Arc<dyn PromptResponder>>,
    ) -> Self {
        Self {
            stdin: Arc::new(stdin),
            clipboard: Arc::new(clipboard),
            responder,
        }
    }

    pub fn from_process() -> Self {
        Self::new(RealStdin, RealClipboard, None)
    }

    pub fn with_stdin(mut self, stdin: impl StdinReader + 'static) -> Self {
        self.stdin = Arc::new(stdin);
        self
    }

    pub fn with_clipboard(mut self, clipboard: impl ClipboardReader + 'static) -> Self {
        self.clipboard = Arc::new(clipboard);
        self
    }

    pub fn with_responder(mut self, responder: Arc<dyn PromptResponder>) -> Self {
        self.responder = Some(responder);
        self
    }

    pub fn stdin(&self) -> &dyn StdinReader {
        self.stdin.as_ref()
    }

    pub fn stdin_arc(&self) -> Arc<dyn StdinReader> {
        Arc::clone(&self.stdin)
    }

    pub fn clipboard(&self) -> &dyn ClipboardReader {
        self.clipboard.as_ref()
    }

    pub fn clipboard_arc(&self) -> Arc<dyn ClipboardReader> {
        Arc::clone(&self.clipboard)
    }

    pub fn responder(&self) -> Option<&dyn PromptResponder> {
        self.responder.as_deref()
    }

    pub fn responder_arc(&self) -> Option<Arc<dyn PromptResponder>> {
        self.responder.clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::env::{MockClipboard, MockStdin};

    fn sample() -> InputSources {
        InputSources::new(MockStdin::piped("hello"), MockClipboard::empty(), None)
    }

    #[test]
    fn input_sources_constructs_from_explicit_readers() {
        let sources = sample();
        assert!(!sources.stdin().is_terminal());
        assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
        assert_eq!(sources.clipboard().read().unwrap(), None);
        assert!(sources.responder().is_none());
    }

    #[test]
    fn input_sources_is_not_copy() {
        struct Probe<U>(std::marker::PhantomData<U>);
        trait AmbiguousIfImpl<A> {
            fn check() {}
        }
        impl<U> AmbiguousIfImpl<()> for Probe<U> {}
        impl<U: Copy> AmbiguousIfImpl<u8> for Probe<U> {}
        let _ = <Probe<InputSources> as AmbiguousIfImpl<_>>::check;

        let sources = sample();
        let moved = sources;
        assert!(!moved.stdin().is_terminal());
    }

    #[test]
    fn input_sources_is_clone() {
        let sources = sample();
        let cloned = sources.clone();
        assert_eq!(cloned.stdin().read_to_string().unwrap(), "hello");
        assert_eq!(sources.stdin().read_to_string().unwrap(), "hello");
    }

    #[test]
    fn input_sources_debug_is_structural() {
        let sources = sample();
        let debug = format!("{sources:?}");
        assert!(debug.contains("InputSources"));
        assert!(debug.contains("has_responder: false"));
    }

    #[test]
    fn from_process_constructs_production_sources() {
        let sources = InputSources::from_process();
        let _ = sources.stdin().is_terminal();
        assert!(sources.responder().is_none());
    }

    #[test]
    fn builders_replace_readers() {
        let sources = InputSources::from_process()
            .with_stdin(MockStdin::piped("in"))
            .with_clipboard(MockClipboard::with_content("clip"));
        assert_eq!(sources.stdin().read_to_string().unwrap(), "in");
        assert_eq!(
            sources.clipboard().read().unwrap(),
            Some("clip".to_string())
        );
    }
}