Skip to main content

reinhardt_core/parsers/
parser.rs

1use crate::exception::{Error, Result};
2use async_trait::async_trait;
3use bytes::Bytes;
4use http::HeaderMap;
5use serde_json::Value;
6use std::collections::HashMap;
7
8pub type ParseError = Error;
9pub type ParseResult<T> = Result<T>;
10
11/// Media type representation
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct MediaType {
14	pub main_type: String,
15	pub sub_type: String,
16	pub parameters: HashMap<String, String>,
17}
18
19impl MediaType {
20	/// Create a new MediaType with the specified main and sub types.
21	///
22	/// # Examples
23	///
24	/// ```
25	/// use reinhardt_core::parsers::parser::MediaType;
26	///
27	/// let media_type = MediaType::new("application", "json");
28	/// assert_eq!(media_type.main_type, "application");
29	/// assert_eq!(media_type.sub_type, "json");
30	/// assert!(media_type.parameters.is_empty());
31	/// ```
32	pub fn new(main_type: impl Into<String>, sub_type: impl Into<String>) -> Self {
33		Self {
34			main_type: main_type.into(),
35			sub_type: sub_type.into(),
36			parameters: HashMap::new(),
37		}
38	}
39	/// Add a parameter to the media type (e.g., charset=utf-8).
40	///
41	/// # Examples
42	///
43	/// ```
44	/// use reinhardt_core::parsers::parser::MediaType;
45	///
46	/// let media_type = MediaType::new("text", "html")
47	///     .with_param("charset", "utf-8");
48	/// assert_eq!(media_type.parameters.get("charset"), Some(&"utf-8".to_string()));
49	/// ```
50	pub fn with_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
51		self.parameters.insert(key.into(), value.into());
52		self
53	}
54	/// Parse a content-type string into a MediaType.
55	///
56	/// # Examples
57	///
58	/// ```
59	/// use reinhardt_core::parsers::parser::MediaType;
60	///
61	/// let media_type = MediaType::parse("application/json; charset=utf-8").unwrap();
62	/// assert_eq!(media_type.main_type, "application");
63	/// assert_eq!(media_type.sub_type, "json");
64	/// assert_eq!(media_type.parameters.get("charset"), Some(&"utf-8".to_string()));
65	/// ```
66	pub fn parse(content_type: &str) -> ParseResult<Self> {
67		let parts: Vec<&str> = content_type.split(';').collect();
68		if parts.is_empty() {
69			return Err(Error::Validation(content_type.to_string()));
70		}
71
72		let type_parts: Vec<&str> = parts[0].trim().split('/').collect();
73		if type_parts.len() != 2 {
74			return Err(Error::Validation(content_type.to_string()));
75		}
76
77		let mut media_type = MediaType::new(type_parts[0], type_parts[1]);
78
79		// Parse parameters
80		for part in parts.iter().skip(1) {
81			let param_parts: Vec<&str> = part.trim().splitn(2, '=').collect();
82			if param_parts.len() == 2 {
83				media_type.parameters.insert(
84					param_parts[0].trim().to_string(),
85					param_parts[1].trim().to_string(),
86				);
87			}
88		}
89
90		Ok(media_type)
91	}
92	/// Check if this media type matches a pattern (supports wildcards).
93	///
94	/// # Examples
95	///
96	/// ```
97	/// use reinhardt_core::parsers::parser::MediaType;
98	///
99	/// let media_type = MediaType::new("application", "json");
100	/// assert!(media_type.matches("application/json"));
101	/// assert!(media_type.matches("application/*"));
102	/// assert!(media_type.matches("*/json"));
103	/// assert!(media_type.matches("*/*"));
104	/// assert!(!media_type.matches("text/html"));
105	/// ```
106	pub fn matches(&self, pattern: &str) -> bool {
107		let parts: Vec<&str> = pattern.split('/').collect();
108		if parts.len() != 2 {
109			return false;
110		}
111
112		(parts[0] == "*" || parts[0] == self.main_type)
113			&& (parts[1] == "*" || parts[1] == self.sub_type)
114	}
115}
116
117impl std::fmt::Display for MediaType {
118	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119		write!(f, "{}/{}", self.main_type, self.sub_type)?;
120		for (key, value) in &self.parameters {
121			write!(f, "; {}={}", key, value)?;
122		}
123		Ok(())
124	}
125}
126
127/// Parsed data representation
128#[derive(Debug, Clone)]
129pub enum ParsedData {
130	Json(Value),
131	Xml(Value),
132	Yaml(Value),
133	Form(HashMap<String, String>),
134	MultiPart {
135		fields: HashMap<String, String>,
136		files: Vec<UploadedFile>,
137	},
138	File(UploadedFile),
139	MessagePack(Value),
140	Protobuf(Value),
141}
142
143/// Uploaded file representation
144#[derive(Debug, Clone)]
145pub struct UploadedFile {
146	pub name: String,
147	pub filename: Option<String>,
148	pub content_type: Option<String>,
149	pub size: usize,
150	pub data: Bytes,
151}
152
153impl UploadedFile {
154	/// Create a new UploadedFile with the given name and data.
155	///
156	/// # Examples
157	///
158	/// ```
159	/// use bytes::Bytes;
160	/// use reinhardt_core::parsers::parser::UploadedFile;
161	///
162	/// let data = Bytes::from("file content");
163	/// let file = UploadedFile::new("upload".to_string(), data.clone());
164	/// assert_eq!(file.name, "upload");
165	/// assert_eq!(file.size, data.len());
166	/// assert!(file.filename.is_none());
167	/// assert!(file.content_type.is_none());
168	/// ```
169	pub fn new(name: String, data: Bytes) -> Self {
170		let size = data.len();
171		Self {
172			name,
173			filename: None,
174			content_type: None,
175			size,
176			data,
177		}
178	}
179	/// Set the original filename for this upload.
180	///
181	/// # Examples
182	///
183	/// ```
184	/// use bytes::Bytes;
185	/// use reinhardt_core::parsers::parser::UploadedFile;
186	///
187	/// let file = UploadedFile::new("upload".to_string(), Bytes::from("content"))
188	///     .with_filename("document.pdf".to_string());
189	/// assert_eq!(file.filename, Some("document.pdf".to_string()));
190	/// ```
191	pub fn with_filename(mut self, filename: String) -> Self {
192		self.filename = Some(filename);
193		self
194	}
195	/// Set the content type for this upload.
196	///
197	/// # Examples
198	///
199	/// ```
200	/// use bytes::Bytes;
201	/// use reinhardt_core::parsers::parser::UploadedFile;
202	///
203	/// let file = UploadedFile::new("upload".to_string(), Bytes::from("content"))
204	///     .with_content_type("application/pdf".to_string());
205	/// assert_eq!(file.content_type, Some("application/pdf".to_string()));
206	/// ```
207	pub fn with_content_type(mut self, content_type: String) -> Self {
208		self.content_type = Some(content_type);
209		self
210	}
211}
212
213/// Trait for request body parsers
214#[async_trait]
215pub trait Parser: Send + Sync {
216	/// Get the media types this parser can handle
217	fn media_types(&self) -> Vec<String>;
218
219	/// Parse the request body
220	async fn parse(
221		&self,
222		content_type: Option<&str>,
223		body: Bytes,
224		headers: &HeaderMap,
225	) -> ParseResult<ParsedData>;
226
227	/// Check if this parser can handle the given content type
228	fn can_parse(&self, content_type: Option<&str>) -> bool {
229		if let Some(ct) = content_type
230			&& let Ok(media_type) = MediaType::parse(ct)
231		{
232			return self.media_types().iter().any(|mt| media_type.matches(mt));
233		}
234		false
235	}
236}
237
238/// Parser registry for selecting appropriate parser
239#[derive(Default)]
240pub struct ParserRegistry {
241	parsers: Vec<Box<dyn Parser>>,
242}
243
244impl ParserRegistry {
245	/// Create a new empty parser registry.
246	///
247	/// # Examples
248	///
249	/// ```
250	/// use reinhardt_core::parsers::parser::ParserRegistry;
251	///
252	/// let registry = ParserRegistry::new();
253	/// ```
254	pub fn new() -> Self {
255		Self::default()
256	}
257	/// Register a parser to handle specific content types.
258	///
259	/// # Examples
260	///
261	/// ```
262	/// use reinhardt_core::parsers::parser::ParserRegistry;
263	/// use reinhardt_core::parsers::json::JSONParser;
264	/// use reinhardt_core::parsers::form::FormParser;
265	///
266	/// let registry = ParserRegistry::new()
267	///     .register(JSONParser::new())
268	///     .register(FormParser::new());
269	/// ```
270	pub fn register<P: Parser + 'static>(mut self, parser: P) -> Self {
271		self.parsers.push(Box::new(parser));
272		self
273	}
274	/// Parse the request body using the first matching parser.
275	///
276	/// # Examples
277	///
278	/// ```
279	/// use bytes::Bytes;
280	/// use http::HeaderMap;
281	/// use reinhardt_core::parsers::parser::{ParserRegistry, ParsedData};
282	/// use reinhardt_core::parsers::json::JSONParser;
283	///
284	/// # tokio_test::block_on(async {
285	/// let registry = ParserRegistry::new().register(JSONParser::new());
286	/// let body = Bytes::from(r#"{"key":"value"}"#);
287	/// let headers = HeaderMap::new();
288	/// let result = registry.parse(Some("application/json"), body, &headers).await.unwrap();
289	/// match result {
290	///     ParsedData::Json(_) => {},
291	///     _ => panic!("Expected JSON"),
292	/// }
293	/// # });
294	/// ```
295	pub async fn parse(
296		&self,
297		content_type: Option<&str>,
298		body: Bytes,
299		headers: &HeaderMap,
300	) -> ParseResult<ParsedData> {
301		for parser in &self.parsers {
302			if parser.can_parse(content_type) {
303				return parser.parse(content_type, body, headers).await;
304			}
305		}
306
307		Err(Error::Validation(
308			content_type.unwrap_or("none").to_string(),
309		))
310	}
311}
312
313#[cfg(test)]
314mod tests {
315	use super::*;
316
317	#[test]
318	fn test_media_type_parse() {
319		let mt = MediaType::parse("application/json").unwrap();
320		assert_eq!(mt.main_type, "application");
321		assert_eq!(mt.sub_type, "json");
322
323		let mt = MediaType::parse("text/html; charset=utf-8").unwrap();
324		assert_eq!(mt.main_type, "text");
325		assert_eq!(mt.sub_type, "html");
326		assert_eq!(mt.parameters.get("charset"), Some(&"utf-8".to_string()));
327	}
328
329	#[test]
330	fn test_media_type_matches() {
331		let mt = MediaType::new("application", "json");
332		assert!(mt.matches("application/json"));
333		assert!(mt.matches("application/*"));
334		assert!(mt.matches("*/json"));
335		assert!(mt.matches("*/*"));
336		assert!(!mt.matches("text/html"));
337	}
338}