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#[derive(Debug, Clone, PartialEq)]
11pub enum Source {
12 Stdin,
14 Path(PathBuf),
16 Url(Url),
18}
19
20impl Source {
21 pub fn as_path(&self) -> Option<&Path> {
23 match self {
24 Self::Path(path) => Some(path),
25 _ => None,
26 }
27 }
28
29 pub fn is_dir(&self) -> bool {
32 self.as_path().map(|path| path.is_dir()).unwrap_or(false)
33 }
34
35 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 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 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
123pub struct SourceReader {
126 first_line: Cursor<Vec<u8>>,
127 remainder: BufReader<Box<dyn Read>>,
128 encoding: Option<Encoding>,
129}
130
131impl SourceReader {
132 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 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}