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