Skip to main content

iris_reader/
input.rs

1use std::{
2    fs,
3    io::{self, IsTerminal, Read},
4    path::{Path, PathBuf},
5};
6
7use anyhow::{Context, Result, bail};
8
9use crate::cli::FormatArg;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InputKind {
13    Text,
14    Markdown,
15}
16
17impl InputKind {
18    pub fn label(self) -> &'static str {
19        match self {
20            Self::Text => "Text",
21            Self::Markdown => "Markdown",
22        }
23    }
24}
25
26#[derive(Debug)]
27pub struct Input {
28    pub content: String,
29    pub name: String,
30    pub kind: InputKind,
31    pub base_dir: Option<PathBuf>,
32}
33
34pub fn read_input(path: Option<&Path>, format: FormatArg) -> Result<Input> {
35    match path {
36        Some(path) if path != Path::new("-") => read_file(path, format),
37        Some(_) => read_stdin(format),
38        None if !io::stdin().is_terminal() => read_stdin(format),
39        None => bail!("no input provided (try 'iris <file>' or pipe data into iris)"),
40    }
41}
42
43fn read_file(path: &Path, format: FormatArg) -> Result<Input> {
44    if !path.exists() {
45        bail!("file not found: {}", path.display());
46    }
47    if path.is_dir() {
48        bail!("cannot read directory: {}", path.display());
49    }
50
51    let bytes = fs::read(path).with_context(|| format!("failed to read '{}'", path.display()))?;
52    let content = String::from_utf8(bytes)
53        .map_err(|_| anyhow::anyhow!("input is not valid UTF-8: {}", path.display()))?;
54    let name = path
55        .file_name()
56        .and_then(|value| value.to_str())
57        .unwrap_or_else(|| path.to_str().unwrap_or("input"))
58        .to_string();
59
60    Ok(Input {
61        content,
62        name,
63        kind: resolve_kind(Some(path), format),
64        base_dir: path.parent().map(Path::to_path_buf),
65    })
66}
67
68fn read_stdin(format: FormatArg) -> Result<Input> {
69    let mut bytes = Vec::new();
70    io::stdin()
71        .lock()
72        .read_to_end(&mut bytes)
73        .context("failed to read stdin")?;
74    let content =
75        String::from_utf8(bytes).map_err(|_| anyhow::anyhow!("stdin is not valid UTF-8"))?;
76
77    Ok(Input {
78        content,
79        name: "stdin".to_string(),
80        kind: resolve_kind(None, format),
81        base_dir: None,
82    })
83}
84
85fn resolve_kind(path: Option<&Path>, format: FormatArg) -> InputKind {
86    match format {
87        FormatArg::Text => InputKind::Text,
88        FormatArg::Markdown => InputKind::Markdown,
89        FormatArg::Auto => path.map(detect_kind).unwrap_or(InputKind::Text),
90    }
91}
92
93fn detect_kind(path: &Path) -> InputKind {
94    match path
95        .extension()
96        .and_then(|ext| ext.to_str())
97        .map(str::to_ascii_lowercase)
98        .as_deref()
99    {
100        Some("md" | "markdown" | "mdown" | "mkd" | "mkdn") => InputKind::Markdown,
101        _ => InputKind::Text,
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn detects_markdown_extensions() {
111        assert_eq!(
112            detect_kind(&std::path::PathBuf::from("README.md")),
113            InputKind::Markdown
114        );
115        assert_eq!(
116            detect_kind(&std::path::PathBuf::from("notes.txt")),
117            InputKind::Text
118        );
119    }
120}