1use std::path::Path;
4use std::path::PathBuf;
5use std::path::absolute;
6
7use serde_json::Value as JsonValue;
8use serde_yaml_ng::Value as YamlValue;
9use thiserror::Error;
10use wdl_engine::JsonMap;
11
12use crate::Inputs;
13
14#[derive(Error, Debug)]
16pub enum Error {
17 #[error(transparent)]
19 Json(#[from] serde_json::Error),
20
21 #[error("an input file cannot be read from directory `{0}`")]
23 InvalidDir(PathBuf),
24
25 #[error(transparent)]
27 Io(#[from] std::io::Error),
28
29 #[error("input file `{0}` did not contain a map from strings to values at the root")]
31 NonMapRoot(PathBuf),
32
33 #[error(
35 "unsupported file extension `{0}`: the supported formats are JSON (`.json`) or YAML \
36 (`.yaml` and `.yml`)"
37 )]
38 UnsupportedFileExt(String),
39
40 #[error(transparent)]
42 Yaml(#[from] serde_yaml_ng::Error),
43}
44
45pub type Result<T> = std::result::Result<T, Error>;
47
48pub struct InputFile;
50
51impl InputFile {
52 pub fn read<P: AsRef<Path>>(path: P) -> Result<Inputs> {
63 fn map_to_inputs(map: JsonMap, origin: &Path) -> Inputs {
64 let mut inputs = Inputs::default();
65
66 for (key, value) in map.iter() {
67 inputs.insert(key.to_owned(), (origin.to_path_buf(), value.clone()));
68 }
69
70 inputs
71 }
72
73 let path = path.as_ref();
74
75 if path.is_dir() {
76 return Err(Error::InvalidDir(path.to_path_buf()));
77 }
78
79 let content: String = std::fs::read_to_string(path)?;
80
81 let origin = absolute(path)?;
86 let origin = origin.parent().unwrap();
87
88 match path.extension().and_then(|ext| ext.to_str()) {
89 Some("json") => serde_json::from_str::<JsonValue>(&content)
90 .map_err(Error::from)
91 .and_then(|value| match value {
92 JsonValue::Object(object) => Ok(map_to_inputs(object, origin)),
93 _ => Err(Error::NonMapRoot(path.to_path_buf())),
94 }),
95 Some("yml") | Some("yaml") => serde_yaml_ng::from_str::<YamlValue>(&content)
96 .map_err(Error::from)
97 .and_then(|value| match &value {
98 YamlValue::Mapping(_) => {
99 let value = serde_json::to_value(value).unwrap();
102 if let JsonValue::Object(map) = value {
103 return Ok(map_to_inputs(map, origin));
104 }
105
106 unreachable!(
111 "a YAML mapping must always coerce to a JSON object, found `{value}`"
112 )
113 }
114 _ => Err(Error::NonMapRoot(path.to_path_buf())),
115 }),
116 ext => Err(Error::UnsupportedFileExt(ext.unwrap_or("").to_owned())),
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn nonmap_root() {
127 let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.json")).unwrap_err();
129 assert!(matches!(
130 err,
131 Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.json"
132 ));
133
134 let err = InputFile::read(Path::new("./tests/fixtures/nonmap_inputs.yml")).unwrap_err();
136 assert!(matches!(
137 err,
138 Error::NonMapRoot(path) if path.to_str().unwrap() == "./tests/fixtures/nonmap_inputs.yml"
139 ));
140 }
141}