1use crate::error::{Result, WireError};
21use crate::value::WireValue;
22use chrono::DateTime;
23use std::collections::HashMap;
24
25pub fn format_value(
31 value: &WireValue,
32 format_name: &str,
33 params: &HashMap<String, serde_json::Value>,
34) -> Result<String> {
35 match value {
36 WireValue::Null => Ok("null".to_string()),
37 WireValue::Bool(b) => Ok(b.to_string()),
38 WireValue::Number(n) => format_number(*n, format_name, params),
39 WireValue::Integer(i) => format_integer(*i, format_name, params),
40 WireValue::I8(v) => format_integer(*v as i64, format_name, params),
41 WireValue::U8(v) => format_unsigned(*v as u64, format_name, params),
42 WireValue::I16(v) => format_integer(*v as i64, format_name, params),
43 WireValue::U16(v) => format_unsigned(*v as u64, format_name, params),
44 WireValue::I32(v) => format_integer(*v as i64, format_name, params),
45 WireValue::U32(v) => format_unsigned(*v as u64, format_name, params),
46 WireValue::I64(v) => format_integer(*v, format_name, params),
47 WireValue::U64(v) => format_unsigned(*v, format_name, params),
48 WireValue::Isize(v) => format_integer(*v, format_name, params),
49 WireValue::Usize(v) => format_unsigned(*v, format_name, params),
50 WireValue::Ptr(v) => {
51 if matches!(format_name, "Default" | "") {
52 Ok(format!("0x{v:x}"))
53 } else {
54 format_unsigned(*v, format_name, params)
55 }
56 }
57 WireValue::F32(v) => format_number(*v as f64, format_name, params),
58 WireValue::String(s) => Ok(s.clone()),
59 WireValue::Timestamp(ts) => format_timestamp(*ts, format_name),
60 WireValue::Duration { value, unit } => format_duration(*value, unit),
61 WireValue::Array(arr) => format_array(arr),
62 WireValue::Object(obj) => format_object(obj),
63 WireValue::Table(series) => format_table(series),
64 WireValue::Result { ok, value } => format_result(*ok, value),
65 WireValue::Range {
66 start,
67 end,
68 inclusive,
69 } => format_range(start, end, *inclusive),
70 WireValue::FunctionRef { name } => Ok(format!("<function {}>", name)),
71 WireValue::PrintResult(result) => Ok(result.rendered.clone()),
72 WireValue::Content(node) => Ok(format!("{}", node)),
73 }
74}
75
76fn format_number(
80 n: f64,
81 format_name: &str,
82 params: &HashMap<String, serde_json::Value>,
83) -> Result<String> {
84 match format_name {
85 "Default" | "" => {
87 if n.fract() == 0.0 && n.abs() < 1e15 {
88 Ok(format!("{}", n as i64))
89 } else {
90 Ok(format!("{}", n))
91 }
92 }
93 "Fixed" => {
96 let decimals = params.get("decimals").and_then(|v| v.as_i64()).unwrap_or(2) as usize;
97 Ok(format!("{:.1$}", n, decimals))
98 }
99 "Percent" => {
100 let decimals = params.get("decimals").and_then(|v| v.as_i64()).unwrap_or(2) as usize;
101 Ok(format!("{:.1$}%", n * 100.0, decimals))
102 }
103 "Currency" => {
104 let symbol = params.get("symbol").and_then(|v| v.as_str()).unwrap_or("$");
105 let decimals = params.get("decimals").and_then(|v| v.as_i64()).unwrap_or(2) as usize;
106 Ok(format!("{}{:.*}", symbol, decimals, n))
107 }
108 _ => Ok(format!("{}", n)),
110 }
111}
112
113fn format_integer(
115 i: i64,
116 format_name: &str,
117 params: &HashMap<String, serde_json::Value>,
118) -> Result<String> {
119 match format_name {
120 "Default" | "" => Ok(format!("{}", i)),
121 "Hex" => Ok(format!("0x{:x}", i)),
122 "Binary" => Ok(format!("0b{:b}", i)),
123 "Octal" => Ok(format!("0o{:o}", i)),
124 _ => format_number(i as f64, format_name, params),
125 }
126}
127
128fn format_unsigned(
130 i: u64,
131 format_name: &str,
132 params: &HashMap<String, serde_json::Value>,
133) -> Result<String> {
134 match format_name {
135 "Default" | "" => Ok(format!("{i}")),
136 "Hex" => Ok(format!("0x{i:x}")),
137 "Binary" => Ok(format!("0b{i:b}")),
138 "Octal" => Ok(format!("0o{i:o}")),
139 _ => format_number(i as f64, format_name, params),
140 }
141}
142
143fn format_timestamp(ts_millis: i64, format_name: &str) -> Result<String> {
145 let dt = DateTime::from_timestamp_millis(ts_millis)
146 .ok_or_else(|| WireError::InvalidValue(format!("Invalid timestamp: {}", ts_millis)))?;
147
148 match format_name {
149 "Unix" => Ok(format!("{}", ts_millis)),
150 _ => Ok(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()),
152 }
153}
154
155fn format_duration(value: f64, unit: &crate::value::DurationUnit) -> Result<String> {
157 use crate::value::DurationUnit;
158
159 let unit_str = match unit {
160 DurationUnit::Nanoseconds => "ns",
161 DurationUnit::Microseconds => "µs",
162 DurationUnit::Milliseconds => "ms",
163 DurationUnit::Seconds => "s",
164 DurationUnit::Minutes => "min",
165 DurationUnit::Hours => "h",
166 DurationUnit::Days => "d",
167 DurationUnit::Weeks => "w",
168 };
169
170 Ok(format!("{}{}", value, unit_str))
171}
172
173fn format_array(arr: &[WireValue]) -> Result<String> {
175 let formatted: Result<Vec<String>> = arr
176 .iter()
177 .map(|v| format_value(v, "Default", &HashMap::new()))
178 .collect();
179
180 Ok(format!("[{}]", formatted?.join(", ")))
181}
182
183fn format_object(obj: &std::collections::BTreeMap<String, WireValue>) -> Result<String> {
185 let formatted: Result<Vec<String>> = obj
186 .iter()
187 .map(|(k, v)| {
188 let formatted_val = format_value(v, "Default", &HashMap::new())?;
189 Ok(format!("{}: {}", k, formatted_val))
190 })
191 .collect();
192
193 Ok(format!("{{ {} }}", formatted?.join(", ")))
194}
195
196fn format_table(series: &crate::value::WireTable) -> Result<String> {
198 let type_name = series.type_name.as_deref().unwrap_or("Table");
199 Ok(format!(
200 "<{} ({} rows, {} columns)>",
201 type_name, series.row_count, series.column_count
202 ))
203}
204
205fn format_result(ok: bool, value: &WireValue) -> Result<String> {
207 let formatted_value = format_value(value, "Default", &HashMap::new())?;
208 if ok {
209 Ok(format!("Ok({})", formatted_value))
210 } else {
211 Ok(format!("Err({})", formatted_value))
212 }
213}
214
215fn format_range(
217 start: &Option<Box<WireValue>>,
218 end: &Option<Box<WireValue>>,
219 inclusive: bool,
220) -> Result<String> {
221 let start_str = match start {
222 Some(v) => format_value(v, "Default", &HashMap::new())?,
223 None => "".to_string(),
224 };
225 let end_str = match end {
226 Some(v) => format_value(v, "Default", &HashMap::new())?,
227 None => "".to_string(),
228 };
229 let op = if inclusive { "..=" } else { ".." };
230 Ok(format!("{}{}{}", start_str, op, end_str))
231}
232
233pub fn parse_value(
237 text: &str,
238 target_type: &str,
239 format_name: &str,
240 _params: &HashMap<String, serde_json::Value>,
241) -> Result<WireValue> {
242 match target_type {
243 "Number" => parse_number(text, format_name),
244 "Integer" => parse_integer(text, format_name),
245 "Bool" => parse_bool(text),
246 "Timestamp" => parse_timestamp(text),
247 "String" => Ok(WireValue::String(text.to_string())),
248 _ => Err(WireError::TypeMismatch {
249 expected: target_type.to_string(),
250 actual: "String".to_string(),
251 }),
252 }
253}
254
255fn parse_number(text: &str, format_name: &str) -> Result<WireValue> {
256 let cleaned = match format_name {
258 "Percent" => text.trim_end_matches('%'),
259 "Currency" => {
260 text.trim_start_matches(|c: char| !c.is_ascii_digit() && c != '-' && c != '.')
261 }
262 _ => text,
263 };
264
265 let n: f64 = cleaned
266 .parse()
267 .map_err(|_| WireError::InvalidValue(format!("Cannot parse '{}' as number", text)))?;
268
269 let n = if format_name == "Percent" {
271 n / 100.0
272 } else {
273 n
274 };
275
276 Ok(WireValue::Number(n))
277}
278
279fn parse_integer(text: &str, format_name: &str) -> Result<WireValue> {
280 let i = match format_name {
281 "Hex" => {
282 let cleaned = text.trim_start_matches("0x").trim_start_matches("0X");
283 i64::from_str_radix(cleaned, 16)
284 }
285 "Binary" => {
286 let cleaned = text.trim_start_matches("0b").trim_start_matches("0B");
287 i64::from_str_radix(cleaned, 2)
288 }
289 "Octal" => {
290 let cleaned = text.trim_start_matches("0o").trim_start_matches("0O");
291 i64::from_str_radix(cleaned, 8)
292 }
293 _ => text.parse(),
294 }
295 .map_err(|_| WireError::InvalidValue(format!("Cannot parse '{}' as integer", text)))?;
296
297 Ok(WireValue::Integer(i))
298}
299
300fn parse_bool(text: &str) -> Result<WireValue> {
301 match text.to_lowercase().as_str() {
302 "true" | "yes" | "1" => Ok(WireValue::Bool(true)),
303 "false" | "no" | "0" => Ok(WireValue::Bool(false)),
304 _ => Err(WireError::InvalidValue(format!(
305 "Cannot parse '{}' as boolean",
306 text
307 ))),
308 }
309}
310
311fn parse_timestamp(text: &str) -> Result<WireValue> {
312 if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
314 return Ok(WireValue::Timestamp(dt.timestamp_millis()));
315 }
316 if let Ok(nd) = chrono::NaiveDate::parse_from_str(text, "%Y-%m-%d") {
318 let dt = nd
319 .and_hms_opt(0, 0, 0)
320 .ok_or_else(|| WireError::InvalidValue("Invalid date".to_string()))?;
321 return Ok(WireValue::Timestamp(dt.and_utc().timestamp_millis()));
322 }
323 if let Ok(n) = text.parse::<i64>() {
325 return Ok(WireValue::Timestamp(n));
326 }
327 Err(WireError::InvalidValue(format!(
328 "Cannot parse '{}' as timestamp",
329 text
330 )))
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_format_number_default() {
339 let result = format_value(&WireValue::Number(42.5), "Default", &HashMap::new()).unwrap();
340 assert_eq!(result, "42.5");
341
342 let result = format_value(&WireValue::Number(42.0), "Default", &HashMap::new()).unwrap();
343 assert_eq!(result, "42");
344 }
345
346 #[test]
347 fn test_format_number_fixed() {
348 let mut params = HashMap::new();
349 params.insert("decimals".to_string(), serde_json::json!(4));
350
351 let result = format_value(&WireValue::Number(3.14159), "Fixed", ¶ms).unwrap();
352 assert_eq!(result, "3.1416");
353 }
354
355 #[test]
356 fn test_format_number_percent() {
357 let result = format_value(&WireValue::Number(0.1234), "Percent", &HashMap::new()).unwrap();
358 assert_eq!(result, "12.34%");
359 }
360
361 #[test]
362 fn test_format_number_currency() {
363 let mut params = HashMap::new();
364 params.insert("symbol".to_string(), serde_json::json!("€"));
365 params.insert("decimals".to_string(), serde_json::json!(2));
366
367 let result = format_value(&WireValue::Number(1234.567), "Currency", ¶ms).unwrap();
368 assert_eq!(result, "€1234.57");
369 }
370
371 #[test]
372 fn test_format_timestamp_iso8601() {
373 let ts = 1705314600000_i64;
375 let result = format_value(&WireValue::Timestamp(ts), "ISO8601", &HashMap::new()).unwrap();
376 assert_eq!(result, "2024-01-15T10:30:00Z");
377 }
378
379 #[test]
380 fn test_format_timestamp_unix() {
381 let ts = 1705314600000_i64;
382
383 let result = format_value(&WireValue::Timestamp(ts), "Unix", &HashMap::new()).unwrap();
385 assert_eq!(result, "1705314600000");
386 }
387
388 #[test]
389 fn test_format_timestamp_date_only() {
390 let ts = 1705314600000_i64;
393 let result = format_value(&WireValue::Timestamp(ts), "Date", &HashMap::new()).unwrap();
394 assert_eq!(result, "2024-01-15T10:30:00Z");
396 }
397
398 #[test]
399 fn test_parse_timestamp_iso8601() {
400 let result = parse_value(
401 "2024-01-15T10:30:00Z",
402 "Timestamp",
403 "ISO8601",
404 &HashMap::new(),
405 )
406 .unwrap();
407 assert_eq!(result, WireValue::Timestamp(1705314600000));
408 }
409
410 #[test]
411 fn test_parse_number_percent() {
412 let result = parse_value("12.34%", "Number", "Percent", &HashMap::new()).unwrap();
413 if let WireValue::Number(n) = result {
414 assert!((n - 0.1234).abs() < 0.0001);
415 } else {
416 panic!("Expected Number");
417 }
418 }
419
420 #[test]
421 fn test_format_array() {
422 let arr = WireValue::Array(vec![
423 WireValue::Number(1.0),
424 WireValue::Number(2.0),
425 WireValue::Number(3.0),
426 ]);
427 let result = format_value(&arr, "Default", &HashMap::new()).unwrap();
428 assert_eq!(result, "[1, 2, 3]");
429 }
430
431 #[test]
432 fn test_format_integer_hex() {
433 let result = format_value(&WireValue::Integer(255), "Hex", &HashMap::new()).unwrap();
434 assert_eq!(result, "0xff");
435 }
436
437 #[test]
438 fn test_parse_integer_hex() {
439 let result = parse_value("0xff", "Integer", "Hex", &HashMap::new()).unwrap();
440 assert_eq!(result, WireValue::Integer(255));
441 }
442}