Skip to main content

shape_wire/
formatter.rs

1//! Value formatting implementation
2//!
3//! This module provides basic/fallback formatting for wire values.
4//!
5//! **Important**: Complex format implementations (Percent, Currency, Scientific, etc.)
6//! are now defined in Shape stdlib (stdlib/core/formats.shape) and executed
7//! via the Shape runtime. This Rust code provides only:
8//!
9//! 1. Basic "Default" format for each type (simple display)
10//! 2. Fallback for when Shape runtime is not available
11//! 3. Wire protocol serialization helpers
12//!
13//! For rich formatting with custom parameters, use Shape's `.format()` method:
14//! ```shape
15//! let value = 0.1234;
16//! value.format("Percent")           // "12.34%"
17//! value.format({ format: "Percent", decimals: 1 })  // "12.3%"
18//! ```
19
20use crate::error::{Result, WireError};
21use crate::value::WireValue;
22use chrono::DateTime;
23use std::collections::HashMap;
24
25/// Format a wire value to a string using the specified format
26///
27/// For basic "Default" format, this provides simple display.
28/// For named formats (Percent, Currency, etc.), this provides fallback
29/// behavior. Use Shape runtime for full format support.
30pub 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
76/// Format a number value (basic fallback)
77///
78/// For rich formatting (Percent, Currency, etc.), use Shape runtime.
79fn format_number(
80    n: f64,
81    format_name: &str,
82    params: &HashMap<String, serde_json::Value>,
83) -> Result<String> {
84    match format_name {
85        // Basic display - smart integer detection
86        "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        // Fallback implementations for common formats
94        // (Full support via Shape runtime)
95        "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        // Unknown format - use default
109        _ => Ok(format!("{}", n)),
110    }
111}
112
113/// Format an integer value (basic fallback)
114fn 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
128/// Format an unsigned integer value.
129fn 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
143/// Format a timestamp value (basic fallback - ISO8601)
144fn 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        // Default to ISO8601 for all other formats
151        _ => Ok(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()),
152    }
153}
154
155/// Format a duration value
156fn 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
173/// Format an array (basic display)
174fn 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
183/// Format an object (basic display)
184fn 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
196/// Format a series (summary display)
197fn 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
205/// Format a Result value
206fn 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
215/// Format a Range value
216fn 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
233/// Parse a string into a wire value (basic fallback)
234///
235/// For rich parsing with format-specific logic, use Shape runtime.
236pub 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    // Basic cleanup for common formats
257    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    // Adjust for percent format
270    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    // Try RFC3339/ISO8601 first
313    if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
314        return Ok(WireValue::Timestamp(dt.timestamp_millis()));
315    }
316    // Try date-only
317    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    // Try unix timestamp
324    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", &params).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", &params).unwrap();
368        assert_eq!(result, "€1234.57");
369    }
370
371    #[test]
372    fn test_format_timestamp_iso8601() {
373        // 2024-01-15T10:30:00Z in milliseconds
374        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        // Simplified formatter always uses milliseconds
384        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        // Note: Date-only format now falls back to ISO8601 in simplified formatter.
391        // Use Shape runtime for rich format support.
392        let ts = 1705314600000_i64;
393        let result = format_value(&WireValue::Timestamp(ts), "Date", &HashMap::new()).unwrap();
394        // Falls back to ISO8601
395        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}