wdl_cli/inputs/
file.rs

1//! Input files parsed in from the command line.
2
3use std::path::Path;
4use std::path::PathBuf;
5
6use thiserror::Error;
7use wdl_engine::CompoundValue;
8use wdl_engine::Object;
9use wdl_engine::Value;
10
11use crate::Inputs;
12
13/// An error related to a input file.
14#[derive(Error, Debug)]
15pub enum Error {
16    /// An error occurring in [`serde_json`].
17    #[error(transparent)]
18    Json(#[from] serde_json::Error),
19
20    /// An input file cannot be read from a directory.
21    #[error("an input file cannot be read from directory `{0}`")]
22    InvalidDir(PathBuf),
23
24    /// An I/O error.
25    #[error(transparent)]
26    Io(std::io::Error),
27
28    /// The input file did not contain a map at the root.
29    #[error("input file `{0}` did not contain a map from strings to values at the root")]
30    NonMapRoot(PathBuf),
31
32    /// Neither JSON nor YAML could be parsed from the provided path.
33    #[error(
34        "unsupported file extension `{0}`: the supported formats are JSON (`.json`) or YAML \
35         (`.yaml` and `.yml`)"
36    )]
37    UnsupportedFileExt(String),
38
39    /// An error occurring in [`serde_yaml_ng`].
40    #[error(transparent)]
41    Yaml(#[from] serde_yaml_ng::Error),
42}
43
44/// A [`Result`](std::result::Result) with an [`Error`].
45pub type Result<T> = std::result::Result<T, Error>;
46
47/// An input file containing WDL values.
48pub struct InputFile;
49
50impl InputFile {
51    /// Reads an input file.
52    ///
53    /// The file is attempted to be parsed based on its extension.
54    ///
55    /// - If the input file is successfully parsed, it's returned wrapped in
56    ///   [`Ok`].
57    /// - If a deserialization error is encountered while parsing the JSON/YAML
58    ///   file, an [`Error::Json`]/[`Error::Yaml`] is returned respectively.
59    /// - If no recognized extension is found, an [`Error::UnsupportedFileExt`]
60    ///   is returned.
61    pub fn read<P: AsRef<Path>>(path: P) -> Result<Inputs> {
62        let path = path.as_ref();
63
64        if path.is_dir() {
65            return Err(Error::InvalidDir(path.to_path_buf()));
66        }
67
68        // SAFETY: the check above ensures that the path is not a directory,
69        // which means that it can't be the root directory, which means that
70        // this call to `.parent()` cannot return `None`.
71        let parent = path.parent().unwrap();
72        let content: String = std::fs::read_to_string(path).map_err(Error::Io)?;
73
74        fn coerce_object_to_inputs(object: Object, parent: &Path) -> Result<Inputs> {
75            let mut inputs = Inputs::default();
76
77            for (key, value) in object.iter() {
78                inputs.insert(key.to_owned(), (parent.to_path_buf(), value.clone()));
79            }
80
81            Ok(inputs)
82        }
83
84        match path.extension().and_then(|ext| ext.to_str()) {
85            Some("json") => serde_json::from_str::<Value>(&content)
86                .map_err(Error::from)
87                .and_then(|value| match value {
88                    Value::Compound(CompoundValue::Object(object)) => {
89                        coerce_object_to_inputs(object, parent)
90                    }
91                    _ => Err(Error::NonMapRoot(path.to_path_buf())),
92                }),
93            Some("yml") | Some("yaml") => serde_yaml_ng::from_str::<Value>(&content)
94                .map_err(Error::from)
95                .and_then(|value| match value {
96                    Value::Compound(CompoundValue::Object(object)) => {
97                        coerce_object_to_inputs(object, parent)
98                    }
99                    _ => Err(Error::NonMapRoot(path.to_path_buf())),
100                }),
101            ext => Err(Error::UnsupportedFileExt(ext.unwrap_or("").to_owned())),
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn nonmap_root() {
112        // A JSON file that does not have a map at the root.
113        let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.json")).unwrap_err();
114        assert!(matches!(
115            err,
116            Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.json"
117        ));
118
119        // A YML file that does not have a map at the root.
120        let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.yml")).unwrap_err();
121        assert!(matches!(
122            err,
123            Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.yml"
124        ));
125    }
126}