1use std::cell::RefCell;
19
20use polydat::ast::Value;
21
22thread_local! {
23 static ROWS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum EmitFormat {
30 Map,
32 Csv,
35 Jsonl,
37 Text,
40}
41
42impl EmitFormat {
43 pub fn parse(s: &str) -> Option<Self> {
45 match s.trim().to_ascii_lowercase().as_str() {
46 "map" => Some(Self::Map),
47 "csv" => Some(Self::Csv),
48 "json" | "jsonl" => Some(Self::Jsonl),
49 "text" => Some(Self::Text),
50 _ => None,
51 }
52 }
53}
54
55pub fn header(format: EmitFormat, names: &[&str]) -> Option<String> {
57 match format {
58 EmitFormat::Csv => Some(names.join(",")),
59 EmitFormat::Map | EmitFormat::Jsonl | EmitFormat::Text => None,
60 }
61}
62
63pub fn render_row(format: EmitFormat, names: &[&str], values: &[Value]) -> String {
65 match format {
66 EmitFormat::Text => values
67 .iter()
68 .map(|v| v.to_display_string())
69 .collect::<Vec<_>>()
70 .join("\n"),
71 EmitFormat::Map => names
72 .iter()
73 .zip(values)
74 .map(|(n, v)| format!("{n}={}", v.to_display_string()))
75 .collect::<Vec<_>>()
76 .join(" "),
77 EmitFormat::Csv => values
78 .iter()
79 .map(|v| csv_cell(&v.to_display_string()))
80 .collect::<Vec<_>>()
81 .join(","),
82 EmitFormat::Jsonl => {
83 let mut map = serde_json::Map::with_capacity(names.len());
84 for (n, v) in names.iter().zip(values) {
85 map.insert((*n).to_string(), v.to_json_value());
86 }
87 serde_json::Value::Object(map).to_string()
88 }
89 }
90}
91
92fn csv_cell(s: &str) -> String {
93 if s.contains([',', '"', '\n']) {
94 format!("\"{}\"", s.replace('"', "\"\""))
95 } else {
96 s.to_string()
97 }
98}
99
100pub fn take_rows() -> Vec<String> {
102 ROWS.with(|r| std::mem::take(&mut *r.borrow_mut()))
103}
104
105#[polydat::polydat_node(category = Diagnostic, purity = SideChannel(Other), variadic_min = 0)]
110fn emit_row(format: Const<&str>, names: Const<&str>, values: &[Value]) -> u64 {
111 let fmt = EmitFormat::parse(&format).unwrap_or(EmitFormat::Map);
112 let names: Vec<&str> = names
113 .split(',')
114 .map(str::trim)
115 .filter(|s| !s.is_empty())
116 .collect();
117 let row = render_row(fmt, &names, values);
118 ROWS.with(|r| r.borrow_mut().push(row));
119 values.len() as u64
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use polydat::ast::PolydatNode;
126
127 #[test]
128 fn csv_quotes_only_when_needed() {
129 let vals = [
130 Value::U64(1),
131 Value::Str("a,b".into()),
132 Value::Str("plain".into()),
133 ];
134 let row = render_row(EmitFormat::Csv, &["x", "y", "z"], &vals);
135 assert_eq!(row, "1,\"a,b\",plain");
136 }
137
138 #[test]
139 fn map_and_jsonl_shapes() {
140 let vals = [Value::U64(7), Value::F64(1.5)];
141 assert_eq!(render_row(EmitFormat::Map, &["a", "b"], &vals), "a=7 b=1.5");
142 assert_eq!(
143 render_row(EmitFormat::Jsonl, &["a", "b"], &vals),
144 r#"{"a":7,"b":1.5}"#
145 );
146 }
147
148 #[test]
149 fn rows_accumulate_per_thread_and_drain() {
150 let node = EmitRow::new("csv".to_string(), "a".to_string(), 1);
151 let mut out = [Value::None];
152 node.eval(&[Value::U64(3)], &mut out);
153 node.eval(&[Value::U64(4)], &mut out);
154 assert_eq!(out[0].as_u64(), 1);
155 assert_eq!(take_rows(), vec!["3".to_string(), "4".to_string()]);
156 assert!(take_rows().is_empty());
157 }
158}