1use std::io;
2
3pub mod ascii;
4pub mod dot;
5pub mod html;
6pub mod impact;
7pub mod json;
8pub mod layout;
9pub mod list;
10pub mod mermaid;
11pub mod plain;
12pub mod summary;
13pub mod svg;
14
15pub(crate) fn handle_stdout_result(result: io::Result<()>) {
18 if let Err(e) = result
19 && e.kind() != io::ErrorKind::BrokenPipe
20 {
21 eprintln!("error writing output: {}", e);
22 }
23}
24
25pub(crate) fn capitalize(s: &str) -> String {
27 let mut chars = s.chars();
28 match chars.next() {
29 None => String::new(),
30 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
31 }
32}
33
34pub(crate) const NO_DIRECTORY_LABEL: &str = "(other)";
36
37pub(crate) fn directory_label(node: &crate::graph::types::NodeData) -> String {
41 match &node.file_path {
42 Some(path) => path
43 .parent()
44 .filter(|p| !p.as_os_str().is_empty())
45 .map(|p| {
46 p.components()
48 .map(|c| c.as_os_str().to_string_lossy().into_owned())
49 .collect::<Vec<String>>()
50 .join("/")
51 })
52 .unwrap_or_else(|| NO_DIRECTORY_LABEL.to_string()),
53 None => NO_DIRECTORY_LABEL.to_string(),
54 }
55}
56
57pub(crate) fn sanitize_id(s: &str) -> String {
59 s.chars()
60 .map(|c| {
61 if c.is_ascii_alphanumeric() || c == '_' {
62 c
63 } else {
64 '_'
65 }
66 })
67 .collect()
68}
69
70pub(crate) fn serde_io_error(e: serde_json::Error) -> io::Error {
73 match e.io_error_kind() {
74 Some(kind) => io::Error::new(kind, e),
75 None => io::Error::other(e),
76 }
77}
78
79#[cfg(test)]
80pub(crate) mod test_helpers {
81 use crate::graph::types::*;
82
83 pub fn make_node(unique_id: &str, label: &str, node_type: NodeType) -> NodeData {
84 NodeData {
85 unique_id: unique_id.into(),
86 label: label.into(),
87 node_type,
88 file_path: None,
89 description: None,
90 materialization: None,
91 tags: vec![],
92 columns: vec![],
93 exposure: None,
94 }
95 }
96
97 pub fn make_node_with_columns(
98 unique_id: &str,
99 label: &str,
100 node_type: NodeType,
101 columns: &[&str],
102 ) -> NodeData {
103 NodeData {
104 unique_id: unique_id.into(),
105 label: label.into(),
106 node_type,
107 file_path: None,
108 description: None,
109 materialization: None,
110 tags: vec![],
111 columns: columns.iter().map(|s| s.to_string()).collect(),
112 exposure: None,
113 }
114 }
115
116 pub fn make_node_with_path(
117 unique_id: &str,
118 label: &str,
119 node_type: NodeType,
120 path: &str,
121 ) -> NodeData {
122 NodeData {
123 unique_id: unique_id.into(),
124 label: label.into(),
125 node_type,
126 file_path: Some(path.into()),
127 description: None,
128 materialization: None,
129 tags: vec![],
130 columns: vec![],
131 exposure: None,
132 }
133 }
134
135 pub fn make_sample_lineage_graph() -> LineageGraph {
138 let mut graph = LineageGraph::new();
139 let src = graph.add_node(make_node(
140 "source.raw.orders",
141 "raw.orders",
142 NodeType::Source,
143 ));
144 let stg = graph.add_node(make_node("model.stg_orders", "stg_orders", NodeType::Model));
145 let mart = graph.add_node(make_node("model.orders", "orders", NodeType::Model));
146 let t = graph.add_node(make_node(
147 "test.orders_positive",
148 "orders_positive",
149 NodeType::Test,
150 ));
151 let exp = graph.add_node(make_node(
152 "exposure.dashboard",
153 "dashboard",
154 NodeType::Exposure,
155 ));
156
157 graph.add_edge(src, stg, EdgeData::direct(EdgeType::Source));
158 graph.add_edge(stg, mart, EdgeData::direct(EdgeType::Ref));
159 graph.add_edge(mart, t, EdgeData::direct(EdgeType::Test));
160 graph.add_edge(mart, exp, EdgeData::direct(EdgeType::Exposure));
161
162 graph
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_sanitize_id_directory_with_hyphens() {
172 assert_eq!(sanitize_id("models/my-project"), "models_my_project");
173 }
174
175 #[test]
176 fn test_sanitize_id_parentheses() {
177 assert_eq!(sanitize_id("(other)"), "_other_");
178 }
179
180 #[test]
181 fn test_sanitize_id_empty() {
182 assert_eq!(sanitize_id(""), "");
183 }
184
185 #[test]
186 fn test_sanitize_id_already_valid() {
187 assert_eq!(sanitize_id("models_staging"), "models_staging");
188 }
189
190 #[test]
191 fn test_sanitize_id_non_ascii() {
192 assert_eq!(sanitize_id("モデル/日本語"), "_______");
193 }
194
195 #[test]
196 fn test_capitalize() {
197 assert_eq!(capitalize("model"), "Model");
198 assert_eq!(capitalize(""), "");
199 }
200
201 #[test]
202 fn test_directory_label_with_path() {
203 let node = test_helpers::make_node_with_path(
204 "model.a",
205 "a",
206 crate::graph::types::NodeType::Model,
207 "models/staging/a.sql",
208 );
209 assert_eq!(directory_label(&node), "models/staging");
210 }
211
212 #[test]
213 fn test_directory_label_without_path() {
214 let node =
215 test_helpers::make_node("exposure.e", "e", crate::graph::types::NodeType::Exposure);
216 assert_eq!(directory_label(&node), NO_DIRECTORY_LABEL);
217 }
218
219 #[test]
220 fn test_directory_label_file_at_root() {
221 let node = test_helpers::make_node_with_path(
222 "model.a",
223 "a",
224 crate::graph::types::NodeType::Model,
225 "a.sql",
226 );
227 assert_eq!(directory_label(&node), NO_DIRECTORY_LABEL);
228 }
229}