faucet_cli/pipeline_test/
fixtures.rs1use crate::error::{CliError, CliResult};
9use crate::pipeline_test::spec::InputSpec;
10use serde_json::Value;
11use std::path::Path;
12
13pub fn load_input(spec_dir: &Path, input: &InputSpec) -> CliResult<Vec<Value>> {
15 match input {
16 InputSpec::Inline(records) => Ok(records.clone()),
17 InputSpec::Path(rel) => load_fixture_file(&spec_dir.join(rel)),
18 }
19}
20
21fn load_fixture_file(path: &Path) -> CliResult<Vec<Value>> {
22 let text = std::fs::read_to_string(path).map_err(|source| CliError::ReadConfig {
23 path: path.to_path_buf(),
24 source,
25 })?;
26 let ext = path
27 .extension()
28 .and_then(|e| e.to_str())
29 .map(str::to_ascii_lowercase);
30 match ext.as_deref() {
31 Some("jsonl" | "ndjson") => text
32 .lines()
33 .enumerate()
34 .filter(|(_, line)| !line.trim().is_empty())
35 .map(|(i, line)| {
36 serde_json::from_str(line).map_err(|e| CliError::ParseConfig {
37 path: path.to_path_buf(),
38 message: format!("line {}: {e}", i + 1),
39 })
40 })
41 .collect(),
42 Some("json") => as_array(
43 serde_json::from_str(&text).map_err(|e| CliError::ParseConfig {
44 path: path.to_path_buf(),
45 message: e.to_string(),
46 })?,
47 path,
48 ),
49 Some("yaml" | "yml") => as_array(
50 serde_yaml::from_str(&text).map_err(|e| CliError::ParseConfig {
51 path: path.to_path_buf(),
52 message: e.to_string(),
53 })?,
54 path,
55 ),
56 _ => Err(CliError::Config(format!(
57 "fixture file '{}' must be .jsonl, .ndjson, .json, .yaml, or .yml",
58 path.display()
59 ))),
60 }
61}
62
63fn as_array(v: Value, path: &Path) -> CliResult<Vec<Value>> {
64 match v {
65 Value::Array(records) => Ok(records),
66 other => Err(CliError::Config(format!(
67 "fixture file '{}' must hold a top-level array of records, got {}",
68 path.display(),
69 type_name(&other)
70 ))),
71 }
72}
73
74fn type_name(v: &Value) -> &'static str {
75 match v {
76 Value::Null => "null",
77 Value::Bool(_) => "a boolean",
78 Value::Number(_) => "a number",
79 Value::String(_) => "a string",
80 Value::Array(_) => "an array",
81 Value::Object(_) => "an object",
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use serde_json::json;
89
90 #[test]
91 fn inline_records_pass_through() {
92 let dir = tempfile::tempdir().unwrap();
93 let records = load_input(
94 dir.path(),
95 &InputSpec::Inline(vec![json!({"a": 1}), json!({"a": 2})]),
96 )
97 .unwrap();
98 assert_eq!(records.len(), 2);
99 }
100
101 #[test]
102 fn jsonl_fixture_skips_blank_lines() {
103 let dir = tempfile::tempdir().unwrap();
104 std::fs::write(dir.path().join("f.jsonl"), "{\"a\":1}\n\n{\"a\":2}\n").unwrap();
105 let records = load_input(dir.path(), &InputSpec::Path("f.jsonl".into())).unwrap();
106 assert_eq!(records, vec![json!({"a": 1}), json!({"a": 2})]);
107 }
108
109 #[test]
110 fn jsonl_bad_line_reports_line_number() {
111 let dir = tempfile::tempdir().unwrap();
112 std::fs::write(dir.path().join("f.jsonl"), "{\"a\":1}\nnot-json\n").unwrap();
113 let err = load_input(dir.path(), &InputSpec::Path("f.jsonl".into()))
114 .unwrap_err()
115 .to_string();
116 assert!(err.contains("line 2"), "{err}");
117 }
118
119 #[test]
120 fn json_and_yaml_arrays_load() {
121 let dir = tempfile::tempdir().unwrap();
122 std::fs::write(dir.path().join("f.json"), r#"[{"a":1}]"#).unwrap();
123 std::fs::write(dir.path().join("f.yaml"), "- a: 1\n- a: 2\n").unwrap();
124 assert_eq!(
125 load_input(dir.path(), &InputSpec::Path("f.json".into())).unwrap(),
126 vec![json!({"a": 1})]
127 );
128 assert_eq!(
129 load_input(dir.path(), &InputSpec::Path("f.yaml".into()))
130 .unwrap()
131 .len(),
132 2
133 );
134 }
135
136 #[test]
137 fn non_array_json_rejected() {
138 let dir = tempfile::tempdir().unwrap();
139 std::fs::write(dir.path().join("f.json"), r#"{"a":1}"#).unwrap();
140 let err = load_input(dir.path(), &InputSpec::Path("f.json".into()))
141 .unwrap_err()
142 .to_string();
143 assert!(err.contains("top-level array"), "{err}");
144 assert!(err.contains("an object"), "{err}");
145 }
146
147 #[test]
148 fn unknown_extension_and_missing_file_rejected() {
149 let dir = tempfile::tempdir().unwrap();
150 std::fs::write(dir.path().join("f.csv"), "a\n1\n").unwrap();
151 assert!(
152 load_input(dir.path(), &InputSpec::Path("f.csv".into()))
153 .unwrap_err()
154 .to_string()
155 .contains("must be .jsonl")
156 );
157 assert!(matches!(
158 load_input(dir.path(), &InputSpec::Path("missing.jsonl".into())),
159 Err(CliError::ReadConfig { .. })
160 ));
161 }
162}