sfs_core/
input.rs

1//! Input for creating spectra.
2
3use std::{
4    env,
5    fs::File,
6    io::{self, IsTerminal as _},
7    path::{Path, PathBuf},
8};
9
10pub mod genotype;
11pub use genotype::Genotype;
12
13pub mod sample;
14pub use sample::Sample;
15
16pub mod site;
17pub use site::Site;
18
19/// A status when trying to read an element from a reader.
20#[derive(Debug)]
21pub enum ReadStatus<T> {
22    /// Element was succesfully read.
23    Read(T),
24    /// An error was encountered.
25    Error(io::Error),
26    /// The reader has finished.
27    Done,
28}
29
30impl<T> ReadStatus<T> {
31    fn map<U, F>(self, op: F) -> ReadStatus<U>
32    where
33        F: FnOnce(T) -> U,
34    {
35        match self {
36            ReadStatus::Read(t) => ReadStatus::Read(op(t)),
37            ReadStatus::Error(e) => ReadStatus::Error(e),
38            ReadStatus::Done => ReadStatus::Done,
39        }
40    }
41}
42
43/// An input source for reading.
44#[derive(Debug)]
45pub enum Input {
46    /// A path from which to read a file.
47    Path(PathBuf),
48    /// Stdin.
49    Stdin,
50}
51
52impl Input {
53    /// By default, reading an `Input` checks that either a path is provided, or that input is
54    /// available via stdin, instead of hanging.
55    ///
56    /// In some contexts, e.g. testing, this can cause issues, and so it may be disabled by setting
57    /// this environment variable, or by using [`Input::new_unchecked`].
58    pub const ENV_KEY_DISABLE_CHECK: &'static str = "SFS_ALLOW_STDIN";
59
60    /// Creates a new input source.
61    pub fn new(input: Option<PathBuf>) -> io::Result<Self> {
62        let check = env::var(Self::ENV_KEY_DISABLE_CHECK).is_err();
63
64        if input.is_some() && !io::stdin().is_terminal() && check {
65            Err(io::Error::new(
66                io::ErrorKind::Other,
67                "received input both via file and stdin",
68            ))
69        } else if input.is_none() && std::io::stdin().is_terminal() && check {
70            Err(io::Error::new(
71                io::ErrorKind::Other,
72                "received no input via file or stdin",
73            ))
74        } else {
75            Ok(Self::new_unchecked(input))
76        }
77    }
78
79    /// Creates a new input source without checking that any data is available.
80    pub fn new_unchecked(input: Option<PathBuf>) -> Self {
81        if let Some(path) = input {
82            Self::Path(path)
83        } else {
84            Self::Stdin
85        }
86    }
87
88    /// Open the input for reading.
89    pub fn open(&self) -> io::Result<Reader> {
90        match self {
91            Input::Path(path) => File::open(path).map(io::BufReader::new).map(Reader::File),
92            Input::Stdin => Ok(Reader::Stdin(io::stdin().lock())),
93        }
94    }
95
96    /// Returns the provided path if provided, otherwise `None`.
97    pub fn as_path(&self) -> Option<&Path> {
98        match self {
99            Input::Path(path) => Some(path.as_ref()),
100            Input::Stdin => None,
101        }
102    }
103}
104
105impl From<Input> for Option<PathBuf> {
106    fn from(input: Input) -> Self {
107        match input {
108            Input::Path(path) => Some(path),
109            Input::Stdin => None,
110        }
111    }
112}
113
114/// A reader from either a file or stdin.
115#[derive(Debug)]
116pub enum Reader {
117    /// A reader from a file.
118    File(io::BufReader<File>),
119    /// A reader stdin.
120    Stdin(io::StdinLock<'static>),
121}