dts_core/
source.rs

1use crate::{Encoding, Error, PathExt, Result};
2use std::fmt;
3use std::fs;
4use std::io::{self, BufRead, BufReader, Cursor, Read};
5use std::path::{Path, PathBuf};
6use std::str::FromStr;
7use url::Url;
8
9/// A source for data that needs to be deserialized.
10#[derive(Debug, Clone, PartialEq)]
11pub enum Source {
12    /// Stdin source.
13    Stdin,
14    /// Local file or directory source.
15    Path(PathBuf),
16    /// Remote URL source.
17    Url(Url),
18}
19
20impl Source {
21    /// Returns `Some` if the source is a local path, `None` otherwise.
22    pub fn as_path(&self) -> Option<&Path> {
23        match self {
24            Self::Path(path) => Some(path),
25            _ => None,
26        }
27    }
28
29    /// Returns `true` if the `Source` is a local path and the path exists on disk and is pointing
30    /// at a directory.
31    pub fn is_dir(&self) -> bool {
32        self.as_path().map(|path| path.is_dir()).unwrap_or(false)
33    }
34
35    /// Tries to detect the encoding of the source. Returns `None` if the encoding cannot be
36    /// detected.
37    pub fn encoding(&self) -> Option<Encoding> {
38        match self {
39            Self::Stdin => None,
40            Self::Path(path) => Encoding::from_path(path),
41            Self::Url(url) => Encoding::from_path(url.as_str()),
42        }
43    }
44
45    /// If source is a local path, this returns sources for all files matching the glob pattern.
46    ///
47    /// ## Errors
48    ///
49    /// Returns an error if the sink is not of variant `Sink::Path`, the pattern is invalid or if
50    /// there is a `io::Error` while reading the file system.
51    pub fn glob_files(&self, pattern: &str) -> Result<Vec<Source>> {
52        match self.as_path() {
53            Some(path) => Ok(path
54                .glob_files(pattern)?
55                .iter()
56                .map(|path| Self::from(path.as_path()))
57                .collect()),
58            None => Err(Error::new("not a path source")),
59        }
60    }
61
62    /// Returns a `SourceReader` to read from the source.
63    ///
64    /// ## Errors
65    ///
66    /// May return an error if the source is `Source::Path` and the file cannot be opened of if
67    /// source is `Source::Url` and there is an error requesting the remote url.
68    pub fn to_reader(&self) -> Result<SourceReader> {
69        let reader: Box<dyn io::Read> = match self {
70            Self::Stdin => Box::new(io::stdin()),
71            Self::Path(path) => Box::new(fs::File::open(path)?),
72            Self::Url(url) => Box::new(ureq::get(url.as_ref()).call()?.into_reader()),
73        };
74
75        SourceReader::new(reader, self.encoding())
76    }
77}
78
79impl From<&str> for Source {
80    fn from(s: &str) -> Self {
81        if s == "-" {
82            Self::Stdin
83        } else {
84            if let Ok(url) = Url::parse(s) {
85                if url.scheme() != "file" {
86                    return Self::Url(url);
87                }
88            }
89
90            Self::Path(PathBuf::from(s))
91        }
92    }
93}
94
95impl From<&Path> for Source {
96    fn from(path: &Path) -> Self {
97        Self::Path(path.to_path_buf())
98    }
99}
100
101impl FromStr for Source {
102    type Err = std::convert::Infallible;
103
104    fn from_str(s: &str) -> Result<Self, Self::Err> {
105        Ok(From::from(s))
106    }
107}
108
109impl fmt::Display for Source {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::Stdin => write!(f, "<stdin>"),
113            Self::Url(url) => url.fmt(f),
114            Self::Path(path) => path
115                .relative_to_cwd()
116                .unwrap_or_else(|| path.clone())
117                .display()
118                .fmt(f),
119        }
120    }
121}
122
123/// A type that can read from a `Source`. It is able to detect the `Source`'s encoding by looking
124/// at the first line of the input.
125pub struct SourceReader {
126    first_line: Cursor<Vec<u8>>,
127    remainder: BufReader<Box<dyn Read>>,
128    encoding: Option<Encoding>,
129}
130
131impl SourceReader {
132    /// Creates a new `SourceReader` for an `io::Read` implementation and an optional encoding
133    /// hint.
134    ///
135    /// Reads the first line from `reader` upon creation.
136    ///
137    /// ## Errors
138    ///
139    /// Returns an error if reading the first line from the reader fails.
140    pub fn new(reader: Box<dyn Read>, encoding: Option<Encoding>) -> Result<SourceReader> {
141        let mut remainder = BufReader::new(reader);
142        let mut buf = Vec::new();
143
144        remainder.read_until(b'\n', &mut buf)?;
145
146        let first_line = Cursor::new(buf);
147
148        Ok(SourceReader {
149            first_line,
150            remainder,
151            encoding,
152        })
153    }
154
155    /// Tries to detect the encoding of the source. If the source provides an encoding hint it is
156    /// returned as is. Otherwise the `SourceReader` attempts to detect the encoding based on the
157    /// contents of the first line of the input data.
158    ///
159    /// Returns `None` if the encoding cannot be detected.
160    pub fn encoding(&self) -> Option<Encoding> {
161        self.encoding.or_else(|| {
162            std::str::from_utf8(self.first_line.get_ref())
163                .ok()
164                .and_then(Encoding::from_first_line)
165        })
166    }
167}
168
169impl Read for SourceReader {
170    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
171        if self.first_line.position() < self.first_line.get_ref().len() as u64 {
172            self.first_line.read(buf)
173        } else {
174            self.remainder.read(buf)
175        }
176    }
177}
178
179#[cfg(test)]
180mod test {
181    use super::*;
182    use pretty_assertions::assert_eq;
183
184    #[test]
185    fn test_from_str() {
186        assert_eq!(Source::from_str("-"), Ok(Source::Stdin));
187        assert_eq!(
188            Source::from_str("foo.json"),
189            Ok(Source::Path(PathBuf::from("foo.json")))
190        );
191        assert_eq!(
192            Source::from_str("http://localhost/foo.json"),
193            Ok(Source::Url(
194                Url::from_str("http://localhost/foo.json").unwrap()
195            ))
196        );
197    }
198
199    #[test]
200    fn test_encoding() {
201        assert_eq!(Source::from("-").encoding(), None);
202        assert_eq!(Source::from("foo").encoding(), None);
203        assert_eq!(Source::from("foo.json").encoding(), Some(Encoding::Json));
204        assert_eq!(
205            Source::from("http://localhost/bar.yaml").encoding(),
206            Some(Encoding::Yaml)
207        );
208    }
209
210    #[test]
211    fn test_to_string() {
212        assert_eq!(&Source::Stdin.to_string(), "<stdin>");
213        assert_eq!(&Source::from("Cargo.toml").to_string(), "Cargo.toml");
214        assert_eq!(
215            &Source::from(std::fs::canonicalize("src/lib.rs").unwrap().as_path()).to_string(),
216            "src/lib.rs"
217        );
218        assert_eq!(
219            &Source::from("/non-existent/path").to_string(),
220            "/non-existent/path"
221        );
222        assert_eq!(
223            &Source::from("http://localhost/bar.yaml").to_string(),
224            "http://localhost/bar.yaml",
225        );
226    }
227
228    #[test]
229    fn test_glob_files() {
230        assert!(Source::from("src/")
231            .glob_files("*.rs")
232            .unwrap()
233            .contains(&Source::from("src/lib.rs")));
234        assert!(Source::from("-").glob_files("*.json").is_err());
235        assert!(Source::from("http://localhost/")
236            .glob_files("*.json")
237            .is_err(),);
238        assert!(matches!(
239            Source::from("src/").glob_files("***"),
240            Err(Error::GlobPatternError { .. })
241        ));
242    }
243
244    #[test]
245    fn test_source_reader() {
246        let input = Cursor::new("---\nfoo: bar\n");
247        let mut reader = SourceReader::new(Box::new(input), None).unwrap();
248
249        assert_eq!(reader.encoding(), Some(Encoding::Yaml));
250
251        let mut buf = String::new();
252        reader.read_to_string(&mut buf).unwrap();
253
254        assert_eq!(&buf, "---\nfoo: bar\n");
255    }
256}