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    }
73}
74
75/// Format a number value (basic fallback)
76///
77/// For rich formatting (Percent, Currency, etc.), use Shape runtime.
78fn format_number(
79    n: f64,
80    format_name: &str,
81    params: &HashMap<String, serde_json::Value>,
82) -> Result<String> {
83    match format_name {
84        // Basic display - smart integer detection
85        "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        // Fallback implementations for common formats
93        // (Full support via Shape runtime)
94        "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        // Unknown format - use default
108        _ => Ok(format!("{}", n)),
109    }
110}
111
112/// Format an integer value (basic fallback)
113fn 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
127/// Format an unsigned integer value.
128fn 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
142/// Format a timestamp value (basic fallback - ISO8601)
143fn 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        // Default to ISO8601 for all other formats
150        _ => Ok(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()),
151    }
152}
153
154/// Format a duration value
155fn 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
172/// Format an array (basic display)
173fn 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
182/// Format an object (basic display)
183fn 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
195/// Format a series (summary display)
196fn 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
204/// Format a Result value
205fn 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
214/// Format a Range value
215fn 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
232/// Parse a string into a wire value (basic fallback)
233///
234/// For rich parsing with format-specific logic, use Shape runtime.
235pub 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    // Basic cleanup for common formats
256    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    // Adjust for percent format
269    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    // Try RFC3339/ISO8601 first
312    if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
313        return Ok(WireValue::Timestamp(dt.timestamp_millis()));
314    }
315    // Try date-only
316    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    // Try unix timestamp
323    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", &params).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", &params).unwrap();
367        assert_eq!(result, "€1234.57");
368    }
369
370    #[test]
371    fn test_format_timestamp_iso8601() {
372        // 2024-01-15T10:30:00Z in milliseconds
373        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        // Simplified formatter always uses milliseconds
383        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        // Note: Date-only format now falls back to ISO8601 in simplified formatter.
390        // Use Shape runtime for rich format support.
391        let ts = 1705314600000_i64;
392        let result = format_value(&WireValue::Timestamp(ts), "Date", &HashMap::new()).unwrap();
393        // Falls back to ISO8601
394        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}