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    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use serde_json::json;
113
114    #[test]
115    fn test_json_data_dto_roundtrip() {
116        let original = json!({
117            "name": "test",
118            "count": 42,
119            "active": true,
120            "items": [1, 2, 3],
121            "nested": {
122                "value": null
123            }
124        });
125
126        let dto = JsonDataDto::from(original.clone());
127        let domain: JsonData = dto.clone().into();
128        let back: JsonDataDto = domain.into();
129
130        assert_eq!(dto.as_value(), back.as_value());
131    }
132
133    #[test]
134    fn test_null_conversion() {
135        let dto = JsonDataDto::from(SerdeValue::Null);
136        let domain: JsonData = dto.into();
137        assert!(matches!(domain, JsonData::Null));
138    }
139
140    #[test]
141    fn test_number_conversion() {
142        let int_dto = JsonDataDto::from(json!(42));
143        let int_domain: JsonData = int_dto.into();
144        assert!(matches!(int_domain, JsonData::Integer(42)));
145
146        let float_dto = JsonDataDto::from(json!(2.5));
147        let float_domain: JsonData = float_dto.into();
148        if let JsonData::Float(f) = float_domain {
149            assert!((f - 2.5).abs() < 0.001);
150        } else {
151            panic!("Expected Float");
152        }
153    }
154
155    #[test]
156    fn test_serde_serialization() {
157        let dto = JsonDataDto::from(json!({"key": "value"}));
158        let serialized = serde_json::to_string(&dto).unwrap();
159        let deserialized: JsonDataDto = serde_json::from_str(&serialized).unwrap();
160        assert_eq!(dto, deserialized);
161    }
162
163    #[test]
164    fn test_nan_infinity_conversion() {
165        // NaN -> Null fallback
166        let nan_domain = JsonData::Float(f64::NAN);
167        let nan_dto: JsonDataDto = nan_domain.into();
168        assert_eq!(nan_dto.as_value(), &SerdeValue::Null);
169
170        // Infinity -> Null fallback
171        let inf_domain = JsonData::Float(f64::INFINITY);
172        let inf_dto: JsonDataDto = inf_domain.into();
173        assert_eq!(inf_dto.as_value(), &SerdeValue::Null);
174
175        // NEG_INFINITY -> Null fallback
176        let neg_inf_domain = JsonData::Float(f64::NEG_INFINITY);
177        let neg_inf_dto: JsonDataDto = neg_inf_domain.into();
178        assert_eq!(neg_inf_dto.as_value(), &SerdeValue::Null);
179    }
180
181    #[test]
182    fn test_empty_collections() {
183        // Empty array
184        let empty_array_dto = JsonDataDto::from(json!([]));
185        let empty_array_domain: JsonData = empty_array_dto.clone().into();
186        let back: JsonDataDto = empty_array_domain.into();
187        assert_eq!(empty_array_dto.as_value(), back.as_value());
188        assert!(matches!(
189            JsonData::from(empty_array_dto),
190            JsonData::Array(arr) if arr.is_empty()
191        ));
192
193        // Empty object
194        let empty_obj_dto = JsonDataDto::from(json!({}));
195        let empty_obj_domain: JsonData = empty_obj_dto.clone().into();
196        let back: JsonDataDto = empty_obj_domain.into();
197        assert_eq!(empty_obj_dto.as_value(), back.as_value());
198        assert!(matches!(
199            JsonData::from(empty_obj_dto),
200            JsonData::Object(obj) if obj.is_empty()
201        ));
202    }
203
204    #[test]
205    fn test_reference_conversion() {
206        let dto = JsonDataDto::from(json!({"ref_test": "value"}));
207
208        // Test From<&JsonDataDto> for JsonData
209        let domain_from_ref: JsonData = (&dto).into();
210        let domain_from_owned: JsonData = dto.clone().into();
211
212        // Both conversions should produce equivalent JsonData
213        assert_eq!(
214            format!("{:?}", domain_from_ref),
215            format!("{:?}", domain_from_owned)
216        );
217    }
218
219    #[test]
220    fn test_number_boundaries() {
221        // i64::MAX
222        let max_i64 = i64::MAX;
223        let max_dto = JsonDataDto::from(json!(max_i64));
224        let max_domain: JsonData = max_dto.into();
225        assert!(matches!(max_domain, JsonData::Integer(i) if i == max_i64));
226
227        // i64::MIN
228        let min_i64 = i64::MIN;
229        let min_dto = JsonDataDto::from(json!(min_i64));
230        let min_domain: JsonData = min_dto.into();
231        assert!(matches!(min_domain, JsonData::Integer(i) if i == min_i64));
232
233        // Large u64 beyond i64::MAX (represented as float in JsonData)
234        let large_u64 = u64::MAX;
235        let large_dto = JsonDataDto::from(json!(large_u64));
236        let large_domain: JsonData = large_dto.into();
237        // JsonData converts unsigned integers beyond i64::MAX to Float
238        assert!(matches!(large_domain, JsonData::Float(_)));
239    }
240
241    #[test]
242    fn test_unicode_strings() {
243        // Unicode characters
244        let unicode_dto = JsonDataDto::from(json!("Hello, 世界"));
245        let unicode_domain: JsonData = unicode_dto.clone().into();
246        let back: JsonDataDto = unicode_domain.into();
247        assert_eq!(unicode_dto.as_value(), back.as_value());
248
249        // Emoji
250        let emoji_dto = JsonDataDto::from(json!("🦀 Rust"));
251        let emoji_domain: JsonData = emoji_dto.clone().into();
252        let back: JsonDataDto = emoji_domain.into();
253        assert_eq!(emoji_dto.as_value(), back.as_value());
254
255        // Special characters
256        let special_dto = JsonDataDto::from(json!("tab:\t newline:\n quote:\" backslash:\\"));
257        let special_domain: JsonData = special_dto.clone().into();
258        let back: JsonDataDto = special_domain.into();
259        assert_eq!(special_dto.as_value(), back.as_value());
260    }
261}