1use std::collections::{HashMap, HashSet};
2use std::io::{self, IsTerminal, Write};
3
4use serde_json::Value;
5
6use super::json::build_node_value;
7use crate::ListOutputFormat;
8use crate::graph::types::*;
9
10pub fn resolve_list_fields(
13 json_fields: Option<&[String]>,
14 json_full: bool,
15) -> Result<HashSet<String>, String> {
16 super::json::resolve_graph_fields(json_fields, json_full)
18}
19
20pub fn render_list(
22 graph: &LineageGraph,
23 format: &ListOutputFormat,
24 fields: &HashSet<String>,
25 sql_contents: Option<&HashMap<String, String>>,
26) {
27 let mut stdout = std::io::stdout().lock();
28 let result = match format {
29 ListOutputFormat::Plain => render_list_plain(graph, &mut stdout),
30 ListOutputFormat::Json => {
31 let pretty = stdout.is_terminal();
32 render_list_json(graph, fields, sql_contents, &mut stdout, pretty)
33 }
34 };
35 super::handle_stdout_result(result);
36}
37
38pub fn render_list_plain<W: Write>(graph: &LineageGraph, w: &mut W) -> io::Result<()> {
39 let mut entries: Vec<(&str, &str)> = graph
40 .node_indices()
41 .map(|idx| {
42 let node = &graph[idx];
43 (node.node_type.label(), node.label.as_str())
44 })
45 .collect();
46 entries.sort_unstable();
47
48 for (node_type, label) in entries {
49 writeln!(w, "{}\t{}", node_type, label)?;
50 }
51 Ok(())
52}
53
54pub fn render_list_json<W: Write>(
55 graph: &LineageGraph,
56 fields: &HashSet<String>,
57 sql_contents: Option<&HashMap<String, String>>,
58 w: &mut W,
59 pretty: bool,
60) -> io::Result<()> {
61 let mut nodes: Vec<(String, String, Value)> = graph
62 .node_indices()
63 .map(|idx| {
64 let node = &graph[idx];
65 let sort_key_type = node.node_type.label().to_string();
66 let sort_key_label = node.label.clone();
67 let value = build_node_value(node, fields, sql_contents);
68 (sort_key_type, sort_key_label, value)
69 })
70 .collect();
71 nodes.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
72 let nodes: Vec<Value> = nodes.into_iter().map(|(_, _, v)| v).collect();
73
74 if pretty {
75 serde_json::to_writer_pretty(&mut *w, &nodes).map_err(super::serde_io_error)?;
76 } else {
77 serde_json::to_writer(&mut *w, &nodes).map_err(super::serde_io_error)?;
78 }
79 writeln!(w)?;
80 Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 use crate::render::test_helpers::make_node;
88
89 fn all_fields() -> HashSet<String> {
90 super::super::json::GRAPH_NODE_FIELDS
91 .iter()
92 .map(|s| (*s).to_string())
93 .collect()
94 }
95
96 fn make_test_graph() -> LineageGraph {
97 let mut graph = LineageGraph::new();
98 graph.add_node(make_node("model.orders", "orders", NodeType::Model));
99 graph.add_node(make_node(
100 "source.raw.orders",
101 "raw.orders",
102 NodeType::Source,
103 ));
104 graph.add_node(make_node("model.stg_orders", "stg_orders", NodeType::Model));
105 graph
106 }
107
108 #[test]
109 fn test_plain_sorted_output() {
110 let graph = make_test_graph();
111 let mut buf = Vec::new();
112 render_list_plain(&graph, &mut buf).unwrap();
113 let output = String::from_utf8(buf).unwrap();
114 assert_eq!(
115 output,
116 "model\torders\nmodel\tstg_orders\nsource\traw.orders\n"
117 );
118 }
119
120 #[test]
121 fn test_plain_empty_graph() {
122 let graph = LineageGraph::new();
123 let mut buf = Vec::new();
124 render_list_plain(&graph, &mut buf).unwrap();
125 let output = String::from_utf8(buf).unwrap();
126 assert!(output.is_empty());
127 }
128
129 #[test]
130 fn test_json_sorted_output() {
131 let graph = make_test_graph();
132 let mut buf = Vec::new();
133 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
134 let output = String::from_utf8(buf).unwrap();
135
136 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
137 assert_eq!(parsed.len(), 3);
138 assert_eq!(parsed[0]["node_type"], "model");
140 assert_eq!(parsed[0]["label"], "orders");
141 assert_eq!(parsed[1]["node_type"], "model");
142 assert_eq!(parsed[1]["label"], "stg_orders");
143 assert_eq!(parsed[2]["node_type"], "source");
144 assert_eq!(parsed[2]["label"], "raw.orders");
145 }
146
147 #[test]
148 fn test_json_compact_single_line() {
149 let graph = make_test_graph();
150 let mut buf = Vec::new();
151 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
152 let output = String::from_utf8(buf).unwrap();
153 let lines: Vec<&str> = output.trim_end().split('\n').collect();
154 assert_eq!(lines.len(), 1, "compact JSON should be a single line");
155 }
156
157 #[test]
158 fn test_json_pretty_multi_line() {
159 let graph = make_test_graph();
160 let mut buf = Vec::new();
161 render_list_json(&graph, &all_fields(), None, &mut buf, true).unwrap();
162 let output = String::from_utf8(buf).unwrap();
163 let lines: Vec<&str> = output.trim_end().split('\n').collect();
164 assert!(lines.len() > 1, "pretty JSON should be multi-line");
165 }
166
167 #[test]
168 fn test_json_empty_graph() {
169 let graph = LineageGraph::new();
170 let mut buf = Vec::new();
171 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
172 let output = String::from_utf8(buf).unwrap();
173 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
174 assert!(parsed.is_empty());
175 }
176
177 #[test]
178 fn test_json_has_unique_id() {
179 let mut graph = LineageGraph::new();
180 graph.add_node(make_node("model.orders", "orders", NodeType::Model));
181 let mut buf = Vec::new();
182 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
183 let output = String::from_utf8(buf).unwrap();
184 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
185 assert_eq!(parsed[0]["unique_id"], "model.orders");
186 }
187
188 #[test]
189 fn test_json_includes_file_path() {
190 let mut graph = LineageGraph::new();
191 graph.add_node(NodeData {
192 unique_id: "model.orders".into(),
193 label: "orders".into(),
194 node_type: NodeType::Model,
195 file_path: Some(std::path::PathBuf::from("models/orders.sql")),
196 description: None,
197 materialization: None,
198 tags: vec![],
199 columns: vec![],
200 exposure: None,
201 aliases: vec![],
202 });
203 let mut buf = Vec::new();
204 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
205 let output = String::from_utf8(buf).unwrap();
206 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
207 assert_eq!(parsed[0]["file_path"], "models/orders.sql");
208 }
209
210 #[test]
211 fn test_json_null_file_path() {
212 let mut graph = LineageGraph::new();
213 graph.add_node(make_node(
214 "source.raw.orders",
215 "raw.orders",
216 NodeType::Source,
217 ));
218 let mut buf = Vec::new();
219 render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
220 let output = String::from_utf8(buf).unwrap();
221 let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
222 assert!(parsed[0]["file_path"].is_null());
223 }
224
225 #[test]
226 fn test_snapshot_list_plain() {
227 let graph = crate::render::test_helpers::make_sample_lineage_graph();
228 let mut buf = Vec::new();
229 render_list_plain(&graph, &mut buf).unwrap();
230 let output = String::from_utf8(buf).unwrap();
231 insta::assert_snapshot!(output);
232 }
233
234 #[test]
235 fn test_snapshot_list_json() {
236 let graph = crate::render::test_helpers::make_sample_lineage_graph();
237 let mut buf = Vec::new();
238 render_list_json(&graph, &all_fields(), None, &mut buf, true).unwrap();
239 let output = String::from_utf8(buf).unwrap();
240 insta::assert_snapshot!(output);
241 }
242}