Skip to main content

pjson_rs/application/dto/
json_data_dto.rs

1//! JsonData Data Transfer Object for API serialization
2//!
3//! Handles conversion between serde_json::Value (external) and domain JsonData (internal)
4//! while keeping domain layer clean of serialization concerns at API boundaries.
5//!
6//! ## Security Considerations
7//!
8//! This module relies on parser-layer protections for deeply nested structures.
9//! The parser enforces depth limits to prevent stack overflow attacks.
10//! Conversions in this module are safe for all valid JsonData instances
11//! produced by the parser.
12
13use crate::domain::value_objects::JsonData;
14use serde::{Deserialize, Serialize};
15use serde_json::Value as SerdeValue;
16
17/// Serializable JSON data representation for API boundaries
18///
19/// This DTO wraps serde_json::Value for external communication and provides
20/// conversion to/from the domain JsonData type.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct JsonDataDto {
24    value: SerdeValue,
25}
26
27impl JsonDataDto {
28    /// Create from serde_json::Value
29    pub fn new(value: SerdeValue) -> Self {
30        Self { value }
31    }
32
33    /// Get inner serde_json::Value
34    pub fn into_inner(self) -> SerdeValue {
35        self.value
36    }
37
38    /// Get reference to inner value
39    pub fn as_value(&self) -> &SerdeValue {
40        &self.value
41    }
42}
43
44impl From<SerdeValue> for JsonDataDto {
45    fn from(value: SerdeValue) -> Self {
46        Self { value }
47    }
48}
49
50impl From<JsonDataDto> for SerdeValue {
51    fn from(dto: JsonDataDto) -> Self {
52        dto.value
53    }
54}
55
56impl From<JsonDataDto> for JsonData {
57    fn from(dto: JsonDataDto) -> Self {
58        // Leverages existing From<serde_json::Value> in JsonData
59        JsonData::from(dto.value)
60    }
61}
62
63impl From<&JsonDataDto> for JsonData {
64    fn from(dto: &JsonDataDto) -> Self {
65        JsonData::from(dto.value.clone())
66    }
67}
68
69impl From<JsonData> for JsonDataDto {
70    fn from(data: JsonData) -> Self {
71        Self {
72            value: convert_domain_to_serde(&data),
73        }
74    }
75}
76
77/// Convert domain JsonData to serde_json::Value (reverse direction)
78///
79/// ## Edge Case Handling
80///
81/// Special float values are converted as follows:
82/// - `NaN` → `Null` (serde_json::Number cannot represent NaN)
83/// - `Infinity` → `Null` (serde_json::Number cannot represent Infinity)
84/// - `NEG_INFINITY` → `Null` (serde_json::Number cannot represent -Infinity)
85///
86/// This is a safe fallback as JSON specification does not define these values.
87fn convert_domain_to_serde(data: &JsonData) -> SerdeValue {
88    match data {
89        JsonData::Null => SerdeValue::Null,
90        JsonData::Bool(b) => SerdeValue::Bool(*b),
91        JsonData::Integer(i) => SerdeValue::Number((*i).into()),
92        JsonData::Float(f) => serde_json::Number::from_f64(*f)
93            .map(SerdeValue::Number)
94            .unwrap_or(SerdeValue::Null),
95        JsonData::String(s) => SerdeValue::String(s.clone()),
96        JsonData::Array(arr) => {
97            SerdeValue::Array(arr.iter().map(convert_domain_to_serde).collect())
98        }
99        JsonData::Object(map) => {
100            let obj: serde_json::Map<String, SerdeValue> = map
101                .iter()
102                .map(|(k, v)| (k.clone(), convert_domain_to_serde(v)))
103                .collect();
104            SerdeValue::Object(obj)
105        }
106        _ => SerdeValue::Null,
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use serde_json::json;
114
115    #[test]
116    fn test_json_data_dto_roundtrip() {
117        let original = json!({
118            "name": "test",
119            "count": 42,
120            "active": true,
121            "items": [1, 2, 3],
122            "nested": {
123                "value": null
124            }
125        });
126
127        let dto = JsonDataDto::from(original.clone());
128        let domain: JsonData = dto.clone().into();
129        let back: JsonDataDto = domain.into();
130
131        assert_eq!(dto.as_value(), back.as_value());
132    }
133
134    #[test]
135    fn test_null_conversion() {
136        let dto = JsonDataDto::from(SerdeValue::Null);
137        let domain: JsonData = dto.into();
138        assert!(matches!(domain, JsonData::Null));
139    }
140
141    #[test]
142    fn test_number_conversion() {
143        let int_dto = JsonDataDto::from(json!(42));
144        let int_domain: JsonData = int_dto.into();
145        assert!(matches!(int_domain, JsonData::Integer(42)));
146
147        let float_dto = JsonDataDto::from(json!(2.5));
148        let float_domain: JsonData = float_dto.into();
149        if let JsonData::Float(f) = float_domain {
150            assert!((f - 2.5).abs() < 0.001);
151        } else {
152            panic!("Expected Float");
153        }
154    }
155
156    #[test]
157    fn test_serde_serialization() {
158        let dto = JsonDataDto::from(json!({"key": "value"}));
159        let serialized = serde_json::to_string(&dto).unwrap();
160        let deserialized: JsonDataDto = serde_json::from_str(&serialized).unwrap();
161        assert_eq!(dto, deserialized);
162    }
163
164    #[test]
165    fn test_nan_infinity_conversion() {
166        // NaN -> Null fallback
167        let nan_domain = JsonData::Float(f64::NAN);
168        let nan_dto: JsonDataDto = nan_domain.into();
169        assert_eq!(nan_dto.as_value(), &SerdeValue::Null);
170
171        // Infinity -> Null fallback
172        let inf_domain = JsonData::Float(f64::INFINITY);
173        let inf_dto: JsonDataDto = inf_domain.into();
174        assert_eq!(inf_dto.as_value(), &SerdeValue::Null);
175
176        // NEG_INFINITY -> Null fallback
177        let neg_inf_domain = JsonData::Float(f64::NEG_INFINITY);
178        let neg_inf_dto: JsonDataDto = neg_inf_domain.into();
179        assert_eq!(neg_inf_dto.as_value(), &SerdeValue::Null);
180    }
181
182    #[test]
183    fn test_empty_collections() {
184        // Empty array
185        let empty_array_dto = JsonDataDto::from(json!([]));
186        let empty_array_domain: JsonData = empty_array_dto.clone().into();
187        let back: JsonDataDto = empty_array_domain.into();
188        assert_eq!(empty_array_dto.as_value(), back.as_value());
189        assert!(matches!(
190            JsonData::from(empty_array_dto),
191            JsonData::Array(arr) if arr.is_empty()
192        ));
193
194        // Empty object
195        let empty_obj_dto = JsonDataDto::from(json!({}));
196        let empty_obj_domain: JsonData = empty_obj_dto.clone().into();
197        let back: JsonDataDto = empty_obj_domain.into();
198        assert_eq!(empty_obj_dto.as_value(), back.as_value());
199        assert!(matches!(
200            JsonData::from(empty_obj_dto),
201            JsonData::Object(obj) if obj.is_empty()
202        ));
203    }
204
205    #[test]
206    fn test_reference_conversion() {
207        let dto = JsonDataDto::from(json!({"ref_test": "value"}));
208
209        // Test From<&JsonDataDto> for JsonData
210        let domain_from_ref: JsonData = (&dto).into();
211        let domain_from_owned: JsonData = dto.clone().into();
212
213        // Both conversions should produce equivalent JsonData
214        assert_eq!(
215            format!("{:?}", domain_from_ref),
216            format!("{:?}", domain_from_owned)
217        );
218    }
219
220    #[test]
221    fn test_number_boundaries() {
222        // i64::MAX
223        let max_i64 = i64::MAX;
224        let max_dto = JsonDataDto::from(json!(max_i64));
225        let max_domain: JsonData = max_dto.into();
226        assert!(matches!(max_domain, JsonData::Integer(i) if i == max_i64));
227
228        // i64::MIN
229        let min_i64 = i64::MIN;
230        let min_dto = JsonDataDto::from(json!(min_i64));
231        let min_domain: JsonData = min_dto.into();
232        assert!(matches!(min_domain, JsonData::Integer(i) if i == min_i64));
233
234        // Large u64 beyond i64::MAX (represented as float in JsonData)
235        let large_u64 = u64::MAX;
236        let large_dto = JsonDataDto::from(json!(large_u64));
237        let large_domain: JsonData = large_dto.into();
238        // JsonData converts unsigned integers beyond i64::MAX to Float
239        assert!(matches!(large_domain, JsonData::Float(_)));
240    }
241
242    #[test]
243    fn test_unicode_strings() {
244        // Unicode characters
245        let unicode_dto = JsonDataDto::from(json!("Hello, 世界"));
246        let unicode_domain: JsonData = unicode_dto.clone().into();
247        let back: JsonDataDto = unicode_domain.into();
248        assert_eq!(unicode_dto.as_value(), back.as_value());
249
250        // Emoji
251        let emoji_dto = JsonDataDto::from(json!("🦀 Rust"));
252        let emoji_domain: JsonData = emoji_dto.clone().into();
253        let back: JsonDataDto = emoji_domain.into();
254        assert_eq!(emoji_dto.as_value(), back.as_value());
255
256        // Special characters
257        let special_dto = JsonDataDto::from(json!("tab:\t newline:\n quote:\" backslash:\\"));
258        let special_domain: JsonData = special_dto.clone().into();
259        let back: JsonDataDto = special_domain.into();
260        assert_eq!(special_dto.as_value(), back.as_value());
261    }
262}