Skip to main content

local_store/
format_convert.rs

1//! Format conversion helpers for storage operations.
2//!
3//! This module provides pure format-conversion functions (JSON ↔ TOML) that are
4//! shared across multiple storage types. All functions are free of any IO logic.
5
6use serde_json::Value as JsonValue;
7use thiserror::Error;
8
9/// Error produced by format-conversion operations.
10///
11/// Each variant carries a human-readable message describing the failed step.
12#[derive(Debug, Error)]
13pub enum FormatConvertError {
14    /// Failed to serialize a JSON value to an intermediate string.
15    #[error("json→toml serialize: {0}")]
16    Serialize(String),
17
18    /// Failed to deserialize an intermediate string into the target TOML value.
19    #[error("json→toml deserialize: {0}")]
20    Deserialize(String),
21
22    /// Failed to parse a TOML string into a `toml::Value`.
23    ///
24    /// Reserved for conversion paths that use `toml::from_str` directly.
25    #[error("toml parse: {0}")]
26    TomlParse(String),
27}
28
29/// Convert a `serde_json::Value` to a `toml::Value`.
30///
31/// Uses a two-step round-trip through JSON string representation:
32/// `JsonValue` → JSON string → `toml::Value` via `serde_json::from_str`.
33///
34/// # Arguments
35///
36/// * `json_value` - A reference to the JSON value to convert.
37///
38/// # Returns
39///
40/// Returns `Ok(toml::Value)` on success, or a `FormatConvertError` describing
41/// which step of the conversion failed.
42///
43/// # Errors
44///
45/// - `FormatConvertError::Serialize` — when `serde_json::to_string` fails.
46/// - `FormatConvertError::Deserialize` — when `serde_json::from_str::<toml::Value>` fails.
47pub fn json_to_toml(json_value: &JsonValue) -> Result<toml::Value, FormatConvertError> {
48    let json_str = serde_json::to_string(json_value)
49        .map_err(|e| FormatConvertError::Serialize(e.to_string()))?;
50    let toml_value: toml::Value = serde_json::from_str(&json_str)
51        .map_err(|e| FormatConvertError::Deserialize(e.to_string()))?;
52    Ok(toml_value)
53}
54
55// ---------------------------------------------------------------------------
56// Tests
57// ---------------------------------------------------------------------------
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use serde_json::json;
63
64    // -----------------------------------------------------------------------
65    // T1: happy path
66    // -----------------------------------------------------------------------
67
68    #[test]
69    fn test_json_to_toml_simple_object() {
70        let json = json!({"key": "value", "count": 42});
71        let toml_val = json_to_toml(&json).expect("conversion must succeed");
72        assert_eq!(toml_val["key"].as_str(), Some("value"));
73        assert_eq!(toml_val["count"].as_integer(), Some(42));
74    }
75
76    #[test]
77    fn test_json_to_toml_empty_object() {
78        let json = json!({});
79        // T2: boundary — empty object
80        let result = json_to_toml(&json);
81        assert!(result.is_ok(), "empty object must convert successfully");
82        let toml_val = result.unwrap();
83        // An empty TOML table
84        assert!(toml_val.as_table().map(|t| t.is_empty()).unwrap_or(false));
85    }
86
87    #[test]
88    fn test_json_to_toml_nested_object() {
89        let json = json!({"outer": {"inner": true}});
90        let toml_val = json_to_toml(&json).expect("nested object must convert");
91        let outer = toml_val.get("outer").expect("outer key must exist");
92        assert_eq!(outer["inner"].as_bool(), Some(true));
93    }
94
95    // -----------------------------------------------------------------------
96    // T2: boundary / edge cases
97    // -----------------------------------------------------------------------
98
99    #[test]
100    fn test_json_to_toml_null_value_is_boundary() {
101        // JSON null has no direct TOML equivalent; serde may reject it.
102        // We document the behaviour but do not assert success or failure,
103        // because the conversion outcome is implementation-defined.
104        let json = json!(null);
105        let _ = json_to_toml(&json); // just must not panic
106    }
107
108    #[test]
109    fn test_json_to_toml_string_with_unicode() {
110        let json = json!({"emoji": "🦀", "text": "日本語"});
111        let toml_val = json_to_toml(&json).expect("unicode must convert");
112        assert_eq!(toml_val["emoji"].as_str(), Some("🦀"));
113        assert_eq!(toml_val["text"].as_str(), Some("日本語"));
114    }
115
116    // -----------------------------------------------------------------------
117    // T3: error path — FormatConvertError variants
118    // -----------------------------------------------------------------------
119
120    #[test]
121    fn test_format_convert_error_serialize_display() {
122        let err = FormatConvertError::Serialize("bad input".to_string());
123        assert!(err.to_string().contains("serialize"));
124        assert!(err.to_string().contains("bad input"));
125    }
126
127    #[test]
128    fn test_format_convert_error_deserialize_display() {
129        let err = FormatConvertError::Deserialize("unexpected char".to_string());
130        assert!(err.to_string().contains("deserialize"));
131        assert!(err.to_string().contains("unexpected char"));
132    }
133
134    #[test]
135    fn test_format_convert_error_toml_parse_display() {
136        let err = FormatConvertError::TomlParse("invalid toml".to_string());
137        assert!(err.to_string().contains("toml parse"));
138        assert!(err.to_string().contains("invalid toml"));
139    }
140
141    #[test]
142    fn test_format_convert_error_is_std_error() {
143        let err = FormatConvertError::Serialize("x".to_string());
144        let _: &dyn std::error::Error = &err;
145    }
146}