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