Skip to main content

dlin_core/render/
list.rs

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
10/// Resolve which fields to emit for list JSON, and validate field names.
11/// Uses the same field set as graph JSON output.
12pub fn resolve_list_fields(
13    json_fields: Option<&[String]>,
14    json_full: bool,
15) -> Result<HashSet<String>, String> {
16    // Delegate to the same logic as graph, sharing the field set
17    super::json::resolve_graph_fields(json_fields, json_full)
18}
19
20/// Render node list to stdout.
21pub 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        // Sorted by type then label
139        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        });
202        let mut buf = Vec::new();
203        render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
204        let output = String::from_utf8(buf).unwrap();
205        let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
206        assert_eq!(parsed[0]["file_path"], "models/orders.sql");
207    }
208
209    #[test]
210    fn test_json_null_file_path() {
211        let mut graph = LineageGraph::new();
212        graph.add_node(make_node(
213            "source.raw.orders",
214            "raw.orders",
215            NodeType::Source,
216        ));
217        let mut buf = Vec::new();
218        render_list_json(&graph, &all_fields(), None, &mut buf, false).unwrap();
219        let output = String::from_utf8(buf).unwrap();
220        let parsed: Vec<serde_json::Value> = serde_json::from_str(&output).unwrap();
221        assert!(parsed[0]["file_path"].is_null());
222    }
223
224    #[test]
225    fn test_snapshot_list_plain() {
226        let graph = crate::render::test_helpers::make_sample_lineage_graph();
227        let mut buf = Vec::new();
228        render_list_plain(&graph, &mut buf).unwrap();
229        let output = String::from_utf8(buf).unwrap();
230        insta::assert_snapshot!(output);
231    }
232
233    #[test]
234    fn test_snapshot_list_json() {
235        let graph = crate::render::test_helpers::make_sample_lineage_graph();
236        let mut buf = Vec::new();
237        render_list_json(&graph, &all_fields(), None, &mut buf, true).unwrap();
238        let output = String::from_utf8(buf).unwrap();
239        insta::assert_snapshot!(output);
240    }
241}