wdl_cli/analysis/
source.rs

1//! Sources for a WDL documents used in analysis.
2
3use std::path::Path;
4use std::path::PathBuf;
5
6use anyhow::Result;
7use anyhow::anyhow;
8use anyhow::bail;
9use path_clean::clean;
10use url::Url;
11use wdl_analysis::Analyzer;
12use wdl_engine::path::parse_url;
13
14/// A source for an analysis.
15#[derive(Clone, Debug)]
16pub enum Source {
17    /// A remote URL.
18    Remote(Url),
19
20    /// A local file.
21    File(Url),
22
23    /// A local directory.
24    Directory(PathBuf),
25}
26
27impl Source {
28    /// Attempts to reference the source as a URL.
29    pub fn as_url(&self) -> Option<&Url> {
30        match self {
31            Source::Remote(url) | Source::File(url) => Some(url),
32            Source::Directory(_) => None,
33        }
34    }
35
36    /// Registers the source within an [`Analyzer`].
37    pub async fn register<T: Send + Clone + 'static>(
38        self,
39        analyzer: &mut Analyzer<T>,
40    ) -> Result<()> {
41        match self {
42            Source::Remote(url) | Source::File(url) => analyzer.add_document(url).await,
43            Source::Directory(path) => analyzer.add_directory(path).await,
44        }
45    }
46}
47
48impl std::fmt::Display for Source {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Source::Remote(url) => write!(f, "{url}"),
52            Source::File(url) => write!(f, "{url}"),
53            Source::Directory(path) => write!(f, "{path}", path = path.display()),
54        }
55    }
56}
57
58impl std::str::FromStr for Source {
59    type Err = anyhow::Error;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        if let Some(url) = parse_url(s) {
63            return Ok(Self::Remote(url));
64        }
65
66        let path = Path::new(s);
67
68        let path = std::path::absolute(path)
69            .map_err(|_| anyhow!("failed to convert `{path}` to a URI", path = path.display()))
70            .map(|path| clean(&path))?;
71
72        if !path.exists() {
73            bail!("source file `{s}` does not exist");
74        }
75
76        if path.is_dir() {
77            return Ok(Source::Directory(path));
78        } else if path.is_file() {
79            if let Ok(url) = Url::from_file_path(&path) {
80                return Ok(Source::File(url));
81            }
82        }
83
84        bail!("failed to convert `{s}` to a URI")
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn file() {
94        let file = tempfile::NamedTempFile::new().unwrap();
95        let path = std::path::absolute(file.path()).unwrap();
96
97        let source = path.to_str().unwrap().parse::<Source>().unwrap();
98        assert!(matches!(source, Source::File(_)));
99        let url = source.as_url().unwrap();
100        assert_eq!(url.scheme(), "file");
101        assert_eq!(url.to_file_path().unwrap(), path);
102    }
103
104    #[test]
105    fn directory() {
106        let dir = tempfile::TempDir::new().unwrap();
107        let name = dir.path().as_os_str().to_str().unwrap();
108
109        assert!(matches!(name.parse().unwrap(),
110            Source::Directory(path)
111            if path.as_os_str().to_str().unwrap() == name));
112    }
113
114    #[test]
115    fn url() {
116        const EXAMPLE: &str = "https://example.com/";
117        assert!(matches!(EXAMPLE.parse().unwrap(),
118            Source::Remote(url)
119            if url.as_str()
120                == EXAMPLE
121        ));
122    }
123
124    #[test]
125    fn missing_file() {
126        let err = "a-random-file-that-doesnt-exist.txt"
127            .parse::<Source>()
128            .unwrap_err();
129
130        assert_eq!(
131            err.to_string(),
132            "source file `a-random-file-that-doesnt-exist.txt` does not exist"
133        );
134    }
135
136    #[test]
137    fn invalid_source() {
138        let err = "".parse::<Source>().unwrap_err();
139
140        assert_eq!(err.to_string(), "failed to convert `` to a URI");
141    }
142}