Skip to main content

reinhardt_core/parsers/
json.rs

1use async_trait::async_trait;
2use bytes::Bytes;
3use http::HeaderMap;
4use serde_json::Value;
5
6use super::parser::{ParseError, ParseResult, ParsedData, Parser};
7
8/// JSON parser for application/json content type
9///
10/// ## Strict Mode
11///
12/// The `strict` flag controls validation of parsed JSON values:
13/// - `strict = true`: Rejects non-finite float values (Infinity, -Infinity, NaN)
14/// - `strict = false`: Allows any valid JSON (but still rejects invalid JSON)
15///
16/// ## Limitations
17///
18/// Due to serde_json's adherence to JSON RFC 8259, literal `Infinity`, `-Infinity`,
19/// and `NaN` values are **not valid JSON** and will be rejected during parsing
20/// regardless of the `strict` flag. The strict validation only affects **post-parse**
21/// validation of float values.
22///
23/// In other words:
24/// - Invalid JSON input (e.g., raw `Infinity` literal) → Always rejected by serde_json
25/// - Valid JSON with non-finite values → Rejected only if `strict = true`
26#[derive(Debug, Clone)]
27pub struct JSONParser {
28	/// Whether to allow empty bodies (returns null)
29	pub allow_empty: bool,
30	/// Whether to enforce strict JSON (reject Infinity, -Infinity, NaN)
31	pub strict: bool,
32}
33
34impl Default for JSONParser {
35	fn default() -> Self {
36		Self {
37			allow_empty: false,
38			strict: true, // Default to strict mode like DRF
39		}
40	}
41}
42
43impl JSONParser {
44	/// Create a new JSONParser with default settings (strict mode, empty not allowed).
45	///
46	/// # Examples
47	///
48	/// ```
49	/// use reinhardt_core::parsers::json::JSONParser;
50	///
51	/// let parser = JSONParser::new();
52	/// assert!(!parser.allow_empty);
53	/// assert!(parser.strict);
54	/// ```
55	pub fn new() -> Self {
56		Self::default()
57	}
58	///
59	/// # Examples
60	///
61	/// ```
62	/// use reinhardt_core::parsers::json::JSONParser;
63	///
64	/// let parser = JSONParser::new().allow_empty(true);
65	/// assert!(parser.allow_empty);
66	/// ```
67	pub fn allow_empty(mut self, allow: bool) -> Self {
68		self.allow_empty = allow;
69		self
70	}
71	///
72	/// # Examples
73	///
74	/// ```
75	/// use reinhardt_core::parsers::json::JSONParser;
76	///
77	/// let parser = JSONParser::new().strict(false);
78	/// assert!(!parser.strict);
79	/// ```
80	pub fn strict(mut self, strict: bool) -> Self {
81		self.strict = strict;
82		self
83	}
84}
85
86#[async_trait]
87impl Parser for JSONParser {
88	fn media_types(&self) -> Vec<String> {
89		vec![
90			"application/json".to_string(),
91			"application/*+json".to_string(),
92		]
93	}
94
95	async fn parse(
96		&self,
97		_content_type: Option<&str>,
98		body: Bytes,
99		_headers: &HeaderMap,
100	) -> ParseResult<ParsedData> {
101		if body.is_empty() {
102			if self.allow_empty {
103				return Ok(ParsedData::Json(Value::Null));
104			} else {
105				return Err(ParseError::ParseError("Empty request body".to_string()));
106			}
107		}
108
109		match serde_json::from_slice::<Value>(&body) {
110			Ok(value) => {
111				// Check for non-finite floats in strict mode
112				if self.strict {
113					Self::validate_strict_json(&value)?;
114				}
115				Ok(ParsedData::Json(value))
116			}
117			Err(e) => Err(ParseError::ParseError(format!("Invalid JSON: {}", e))),
118		}
119	}
120}
121
122impl JSONParser {
123	/// Validate that JSON doesn't contain non-finite float values (Infinity, -Infinity, NaN)
124	///
125	/// # Note
126	///
127	/// This validation is performed **after** serde_json parsing. Due to serde_json's
128	/// strict RFC 8259 compliance, literal `Infinity`/`NaN` strings are already rejected
129	/// during parsing. This method validates the **parsed** numeric values.
130	fn validate_strict_json(value: &Value) -> ParseResult<()> {
131		match value {
132			Value::Number(n) => {
133				if let Some(f) = n.as_f64()
134					&& !f.is_finite()
135				{
136					return Err(ParseError::ParseError(
137                            "Non-finite float values (Infinity, -Infinity, NaN) are not allowed in strict mode".to_string()
138                        ));
139				}
140			}
141			Value::Array(arr) => {
142				for item in arr {
143					Self::validate_strict_json(item)?;
144				}
145			}
146			Value::Object(obj) => {
147				for value in obj.values() {
148					Self::validate_strict_json(value)?;
149				}
150			}
151			_ => {}
152		}
153		Ok(())
154	}
155}
156
157#[cfg(test)]
158mod tests {
159	use super::*;
160
161	#[tokio::test]
162	async fn test_json_parser_valid() {
163		let parser = JSONParser::new();
164		let body = Bytes::from(r#"{"name": "test", "value": 123}"#);
165		let headers = HeaderMap::new();
166
167		let result = parser
168			.parse(Some("application/json"), body, &headers)
169			.await
170			.unwrap();
171
172		match result {
173			ParsedData::Json(value) => {
174				assert_eq!(value["name"], "test");
175				assert_eq!(value["value"], 123);
176			}
177			_ => panic!("Expected JSON data"),
178		}
179	}
180
181	#[tokio::test]
182	async fn test_json_parser_invalid() {
183		let parser = JSONParser::new();
184		let body = Bytes::from(r#"{"invalid": json}"#);
185		let headers = HeaderMap::new();
186
187		let result = parser.parse(Some("application/json"), body, &headers).await;
188		assert!(result.is_err());
189	}
190
191	#[tokio::test]
192	async fn test_json_parser_empty_not_allowed() {
193		let parser = JSONParser::new();
194		let body = Bytes::new();
195		let headers = HeaderMap::new();
196
197		let result = parser.parse(Some("application/json"), body, &headers).await;
198		assert!(result.is_err());
199	}
200
201	#[tokio::test]
202	async fn test_json_parser_empty_allowed() {
203		let parser = JSONParser::new().allow_empty(true);
204		let body = Bytes::new();
205		let headers = HeaderMap::new();
206
207		let result = parser
208			.parse(Some("application/json"), body, &headers)
209			.await
210			.unwrap();
211
212		match result {
213			ParsedData::Json(Value::Null) => {}
214			_ => panic!("Expected null JSON value"),
215		}
216	}
217
218	#[test]
219	fn test_json_parser_media_types() {
220		let parser = JSONParser::new();
221		let media_types = parser.media_types();
222
223		assert!(media_types.contains(&"application/json".to_string()));
224		assert!(media_types.contains(&"application/*+json".to_string()));
225	}
226
227	// Tests from Django REST Framework
228
229	#[tokio::test]
230	async fn test_json_float_strictness() {
231		// DRF test: Test Infinity, -Infinity, NaN handling with strict mode
232		let parser = JSONParser::new(); // Default strict = true
233		let headers = HeaderMap::new();
234
235		// In strict mode, these should fail
236		// Note: These fail during serde_json parsing (not strict validation)
237		// because Infinity/NaN literals are not valid JSON per RFC 8259
238		for value in ["Infinity", "-Infinity", "NaN"] {
239			let body = Bytes::from(value);
240			let result = parser.parse(Some("application/json"), body, &headers).await;
241			assert!(
242				result.is_err(),
243				"Expected error for {} (invalid JSON literal)",
244				value
245			);
246		}
247
248		// In non-strict mode, valid JSON should still parse
249		// (strict only affects post-parse validation of float values)
250		let parser_non_strict = JSONParser::new().strict(false);
251		let valid_json = Bytes::from(r#"{"value": 1.0}"#);
252		let result = parser_non_strict
253			.parse(Some("application/json"), valid_json, &headers)
254			.await;
255		assert!(result.is_ok(), "Valid JSON should parse in non-strict mode");
256	}
257
258	#[tokio::test]
259	async fn test_json_edge_case_large_numbers() {
260		// Test extremely large numbers near floating-point limits
261		let parser = JSONParser::new();
262		let headers = HeaderMap::new();
263
264		// Maximum finite f64 value (approximately 1.7976931348623157e308)
265		let large_number = Bytes::from(r#"{"value": 1e308}"#);
266		let result = parser
267			.parse(Some("application/json"), large_number, &headers)
268			.await;
269		assert!(result.is_ok(), "Should parse very large finite numbers");
270
271		// Negative large number
272		let large_negative = Bytes::from(r#"{"value": -1e308}"#);
273		let result = parser
274			.parse(Some("application/json"), large_negative, &headers)
275			.await;
276		assert!(result.is_ok(), "Should parse very large negative numbers");
277	}
278
279	#[tokio::test]
280	async fn test_json_edge_case_small_numbers() {
281		// Test extremely small numbers near zero
282		let parser = JSONParser::new();
283		let headers = HeaderMap::new();
284
285		// Minimum positive normalized f64 value (approximately 2.2250738585072014e-308)
286		let small_number = Bytes::from(r#"{"value": 2.2250738585072014e-308}"#);
287		let result = parser
288			.parse(Some("application/json"), small_number, &headers)
289			.await;
290		assert!(result.is_ok(), "Should parse very small finite numbers");
291
292		// Very small negative number
293		let small_negative = Bytes::from(r#"{"value": -2.2250738585072014e-308}"#);
294		let result = parser
295			.parse(Some("application/json"), small_negative, &headers)
296			.await;
297		assert!(result.is_ok(), "Should parse very small negative numbers");
298	}
299
300	#[tokio::test]
301	async fn test_json_scientific_notation() {
302		// Test various scientific notation formats
303		let parser = JSONParser::new();
304		let headers = HeaderMap::new();
305
306		let test_cases = vec![
307			r#"{"value": 1.5e10}"#,      // Basic scientific notation
308			r#"{"value": 1.5E10}"#,      // Uppercase E
309			r#"{"value": 1.5e+10}"#,     // Explicit positive exponent
310			r#"{"value": 1.5e-10}"#,     // Negative exponent
311			r#"{"array": [1e5, 2e-5]}"#, // Multiple scientific notations
312		];
313
314		for test_case in test_cases {
315			let body = Bytes::from(test_case);
316			let result = parser.parse(Some("application/json"), body, &headers).await;
317			assert!(
318				result.is_ok(),
319				"Should parse scientific notation: {}",
320				test_case
321			);
322		}
323	}
324
325	#[tokio::test]
326	async fn test_json_nested_float_validation() {
327		// Test strict validation in nested structures
328		let parser_strict = JSONParser::new(); // strict = true by default
329		let headers = HeaderMap::new();
330
331		// This will be rejected by serde_json (not by strict validation)
332		// because Infinity is not valid JSON
333		let nested_infinity = Bytes::from(r#"{"outer": {"inner": Infinity}}"#);
334		let result = parser_strict
335			.parse(Some("application/json"), nested_infinity, &headers)
336			.await;
337		assert!(
338			result.is_err(),
339			"Nested Infinity literal should be rejected by serde_json"
340		);
341
342		// Valid nested structure with finite floats
343		let nested_valid = Bytes::from(r#"{"outer": {"inner": 123.456}}"#);
344		let result = parser_strict
345			.parse(Some("application/json"), nested_valid, &headers)
346			.await;
347		assert!(result.is_ok(), "Valid nested floats should be accepted");
348	}
349
350	#[tokio::test]
351	async fn test_json_array_float_validation() {
352		// Test strict validation in arrays
353		let parser_strict = JSONParser::new();
354		let parser_non_strict = JSONParser::new().strict(false);
355		let headers = HeaderMap::new();
356
357		// Valid array with finite floats
358		let valid_array = Bytes::from(r#"[1.0, 2.5, 3.14159, 100.0]"#);
359		let result = parser_strict
360			.parse(Some("application/json"), valid_array.clone(), &headers)
361			.await;
362		assert!(result.is_ok(), "Valid float arrays should be accepted");
363
364		let result = parser_non_strict
365			.parse(Some("application/json"), valid_array, &headers)
366			.await;
367		assert!(
368			result.is_ok(),
369			"Valid float arrays should be accepted in non-strict mode"
370		);
371
372		// Invalid: Array with Infinity literal (rejected by serde_json)
373		let invalid_array = Bytes::from(r#"[1.0, Infinity, 3.0]"#);
374		let result = parser_strict
375			.parse(Some("application/json"), invalid_array.clone(), &headers)
376			.await;
377		assert!(
378			result.is_err(),
379			"Infinity literal in array should be rejected"
380		);
381
382		let result = parser_non_strict
383			.parse(Some("application/json"), invalid_array, &headers)
384			.await;
385		assert!(
386			result.is_err(),
387			"Infinity literal in array should be rejected even in non-strict mode"
388		);
389	}
390}