1use std::path::Path;
4use std::path::PathBuf;
5
6use serde_json::Value as JsonValue;
7use serde_yaml_ng::Value as YamlValue;
8use thiserror::Error;
9use wdl_engine::JsonMap;
10
11use crate::Inputs;
12
13#[derive(Error, Debug)]
15pub enum Error {
16 #[error(transparent)]
18 Json(#[from] serde_json::Error),
19
20 #[error("an input file cannot be read from directory `{0}`")]
22 InvalidDir(PathBuf),
23
24 #[error(transparent)]
26 Io(std::io::Error),
27
28 #[error("input file `{0}` did not contain a map from strings to values at the root")]
30 NonMapRoot(PathBuf),
31
32 #[error(
34 "unsupported file extension `{0}`: the supported formats are JSON (`.json`) or YAML \
35 (`.yaml` and `.yml`)"
36 )]
37 UnsupportedFileExt(String),
38
39 #[error(transparent)]
41 Yaml(#[from] serde_yaml_ng::Error),
42}
43
44pub type Result<T> = std::result::Result<T, Error>;
46
47pub struct InputFile;
49
50impl InputFile {
51 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 let parent = path.parent().unwrap();
72 let content: String = std::fs::read_to_string(path).map_err(Error::Io)?;
73
74 fn map_to_inputs(map: JsonMap, parent: &Path) -> Result<Inputs> {
75 let mut inputs = Inputs::default();
76
77 for (key, value) in map.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::<JsonValue>(&content)
86 .map_err(Error::from)
87 .and_then(|value| match value {
88 JsonValue::Object(object) => map_to_inputs(object, parent),
89 _ => Err(Error::NonMapRoot(path.to_path_buf())),
90 }),
91 Some("yml") | Some("yaml") => serde_yaml_ng::from_str::<YamlValue>(&content)
92 .map_err(Error::from)
93 .and_then(|value| match &value {
94 YamlValue::Mapping(_) => {
95 let value = serde_json::to_value(value).unwrap();
98
99 if let JsonValue::Object(map) = value {
100 return map_to_inputs(map, parent);
101 }
102
103 unreachable!(
108 "a YAML mapping must always coerce to a JSON object, found `{value}`"
109 )
110 }
111 _ => Err(Error::NonMapRoot(path.to_path_buf())),
112 }),
113 ext => Err(Error::UnsupportedFileExt(ext.unwrap_or("").to_owned())),
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn nonmap_root() {
124 let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.json")).unwrap_err();
126 assert!(matches!(
127 err,
128 Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.json"
129 ));
130
131 let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.yml")).unwrap_err();
133 assert!(matches!(
134 err,
135 Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.yml"
136 ));
137 }
138}