faucet_cli/
lineage_glue.rs1use crate::config::TransformSpec;
5use faucet_core::FaucetError;
6use faucet_lineage::{ColumnOp, LineageConfig, LineageEmitter};
7use std::sync::Arc;
8
9pub fn build_emitter(
12 cfg: Option<&LineageConfig>,
13) -> Result<Option<Arc<LineageEmitter>>, FaucetError> {
14 match cfg {
15 Some(c) => Ok(Some(LineageEmitter::new(c.clone())?)),
16 None => Ok(None),
17 }
18}
19
20pub async fn check_transport(cfg: &LineageConfig) -> Result<String, String> {
23 match &cfg.transport {
24 faucet_lineage::Transport::File { path } => {
25 let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
26 if parent.exists() || tokio::fs::create_dir_all(parent).await.is_ok() {
27 Ok(format!("file path writable: {}", path.display()))
28 } else {
29 Err(format!("cannot create parent dir for {}", path.display()))
30 }
31 }
32 faucet_lineage::Transport::Http { url, .. } => {
33 match reqwest::Client::new().head(url).send().await {
34 Ok(_) => Ok(format!("http endpoint reachable: {url}")),
35 Err(e) => Err(format!("http endpoint unreachable: {e}")),
36 }
37 }
38 #[cfg(feature = "lineage-kafka")]
39 faucet_lineage::Transport::Kafka { brokers, .. } => {
40 Ok(format!("kafka brokers configured: {brokers} (not probed)"))
41 }
42 }
43}
44
45pub fn column_ops(specs: &[TransformSpec], has_masking: bool) -> Vec<ColumnOp> {
56 let mut ops: Vec<ColumnOp> = specs.iter().map(map_one).collect();
57 if has_masking {
58 ops.push(ColumnOp::Identity);
59 }
60 ops
61}
62
63fn map_one(s: &TransformSpec) -> ColumnOp {
64 match s.kind.as_str() {
65 "cast" | "redact" | "value_case" | "spell_symbols" => ColumnOp::Identity,
66 "select" => ColumnOp::Select(string_array(&s.config, "fields")),
67 "drop" => ColumnOp::Drop(string_array(&s.config, "fields")),
68 "set" => ColumnOp::Set(object_keys(&s.config, "values")),
69 "rename_field" => ColumnOp::Rename(string_pairs(&s.config, "fields")),
70 _ => ColumnOp::Opaque,
72 }
73}
74
75fn string_array(config: &serde_json::Value, key: &str) -> Vec<String> {
76 config
77 .get(key)
78 .and_then(|v| v.as_array())
79 .map(|a| {
80 a.iter()
81 .filter_map(|x| x.as_str().map(String::from))
82 .collect()
83 })
84 .unwrap_or_default()
85}
86
87fn object_keys(config: &serde_json::Value, key: &str) -> Vec<String> {
88 config
89 .get(key)
90 .and_then(|v| v.as_object())
91 .map(|m| m.keys().cloned().collect())
92 .unwrap_or_default()
93}
94
95fn string_pairs(config: &serde_json::Value, key: &str) -> Vec<(String, String)> {
96 config
97 .get(key)
98 .and_then(|v| v.as_object())
99 .map(|m| {
100 m.iter()
101 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
102 .collect()
103 })
104 .unwrap_or_default()
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::config::TransformSpec;
111 use serde_json::json;
112
113 fn spec(kind: &str, config: serde_json::Value) -> TransformSpec {
114 TransformSpec {
115 kind: kind.into(),
116 config,
117 }
118 }
119
120 #[test]
121 fn maps_explicit_mapping_transforms() {
122 let specs = vec![
123 spec("rename_field", json!({"fields": {"a": "b"}})),
124 spec("select", json!({"fields": ["b", "c"]})),
125 spec("cast", json!({"fields": {"b": "integer"}})),
126 ];
127 let ops = column_ops(&specs, false);
128 assert!(matches!(ops[0], faucet_lineage::ColumnOp::Rename(_)));
129 assert!(matches!(ops[1], faucet_lineage::ColumnOp::Select(_)));
130 assert!(matches!(ops[2], faucet_lineage::ColumnOp::Identity));
131 }
132
133 #[test]
134 fn masking_appends_trailing_identity_op() {
135 let specs = vec![spec("select", json!({"fields": ["a"]}))];
136 let ops = column_ops(&specs, true);
137 assert_eq!(ops.len(), 2);
138 assert!(matches!(ops[0], faucet_lineage::ColumnOp::Select(_)));
139 assert!(
140 matches!(ops[1], faucet_lineage::ColumnOp::Identity),
141 "masking is value-only + key-preserving → Identity"
142 );
143 assert_eq!(column_ops(&specs, false).len(), 1);
145 }
146
147 #[test]
148 fn maps_structure_changing_to_opaque() {
149 for k in [
150 "flatten",
151 "explode",
152 "keys_case",
153 "rename_keys",
154 "weird_custom",
155 ] {
156 let ops = column_ops(&[spec(k, json!({}))], false);
157 assert!(matches!(ops[0], faucet_lineage::ColumnOp::Opaque), "{k}");
158 }
159 }
160}