1use std::path::Path;
28
29use serde::{Deserialize, Serialize, de::DeserializeOwned};
30
31use super::{ParsedContent, Result, SourceError};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Format {
40 #[cfg(feature = "toml")]
42 Toml,
43
44 #[cfg(feature = "json")]
46 Json,
47
48 #[cfg(feature = "yaml")]
50 Yaml,
51
52 Unknown,
54}
55
56impl Format {
57 #[must_use]
59 pub const fn all() -> &'static [Self] {
60 &[
61 #[cfg(feature = "toml")]
62 Self::Toml,
63 #[cfg(feature = "json")]
64 Self::Json,
65 #[cfg(feature = "yaml")]
66 Self::Yaml,
67 ]
68 }
69
70 #[must_use]
72 pub const fn extension(&self) -> &'static str {
73 match self {
74 #[cfg(feature = "toml")]
75 Self::Toml => "toml",
76 #[cfg(feature = "json")]
77 Self::Json => "json",
78 #[cfg(feature = "yaml")]
79 Self::Yaml => "yaml",
80 Self::Unknown => "",
81 }
82 }
83
84 #[must_use]
86 pub const fn alternate_extensions(&self) -> &'static [&'static str] {
87 match self {
88 #[cfg(feature = "yaml")]
89 Self::Yaml => &["yml"],
90 _ => &[],
91 }
92 }
93
94 #[must_use]
96 pub const fn mime_type(&self) -> &'static str {
97 match self {
98 #[cfg(feature = "toml")]
99 Self::Toml => "application/toml",
100 #[cfg(feature = "json")]
101 Self::Json => "application/json",
102 #[cfg(feature = "yaml")]
103 Self::Yaml => "application/x-yaml",
104 Self::Unknown => "application/octet-stream",
105 }
106 }
107
108 #[must_use]
110 pub const fn as_str(&self) -> &'static str {
111 match self {
112 #[cfg(feature = "toml")]
113 Self::Toml => "toml",
114 #[cfg(feature = "json")]
115 Self::Json => "json",
116 #[cfg(feature = "yaml")]
117 Self::Yaml => "yaml",
118 Self::Unknown => "unknown",
119 }
120 }
121
122 #[must_use]
136 pub fn from_path(path: &Path) -> Option<Self> {
137 let ext = path.extension()?.to_str()?.to_lowercase();
138 Self::from_extension(&ext)
139 }
140
141 #[must_use]
152 pub fn from_extension(ext: &str) -> Option<Self> {
153 let ext_lower = ext.to_lowercase();
154
155 #[cfg(feature = "toml")]
156 if ext_lower == "toml" {
157 return Some(Self::Toml);
158 }
159
160 #[cfg(feature = "json")]
161 if ext_lower == "json" {
162 return Some(Self::Json);
163 }
164
165 #[cfg(feature = "yaml")]
166 if ext_lower == "yaml" || ext_lower == "yml" {
167 return Some(Self::Yaml);
168 }
169
170 None
171 }
172
173 #[must_use]
187 pub fn from_content(content: &str) -> Option<Self> {
188 let trimmed = content.trim_start();
189
190 #[cfg(feature = "json")]
192 if trimmed.starts_with('{') || trimmed.starts_with('[') {
193 if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
196 return Some(Self::Json);
197 }
198 }
199
200 #[cfg(feature = "yaml")]
203 if trimmed.starts_with("---")
204 || trimmed.contains(": ")
205 || trimmed.lines().any(|line| line.trim().starts_with("- "))
206 {
207 return Some(Self::Yaml);
208 }
209
210 #[cfg(feature = "toml")]
212 if trimmed.contains(" = ")
213 || trimmed.starts_with('[')
214 || trimmed.lines().any(|line| {
215 let line = line.trim();
216 line.starts_with('#') || line.contains('=')
217 })
218 {
219 return Some(Self::Toml);
220 }
221
222 None
223 }
224
225 pub fn parse(&self, content: &str) -> Result<ParsedContent> {
240 match self {
241 #[cfg(feature = "toml")]
242 Self::Toml => self.parse_toml(content),
243
244 #[cfg(feature = "json")]
245 Self::Json => self.parse_json(content),
246
247 #[cfg(feature = "yaml")]
248 Self::Yaml => self.parse_yaml(content),
249
250 Self::Unknown => Err(SourceError::unsupported("cannot parse unknown format")),
251 }
252 }
253
254 pub fn parse_as<T: DeserializeOwned>(&self, content: &str) -> Result<T> {
275 match self {
276 #[cfg(feature = "toml")]
277 Self::Toml => toml::from_str(content)
278 .map_err(|e| SourceError::parse_failed("", "toml", &e.to_string())),
279
280 #[cfg(feature = "json")]
281 Self::Json => serde_json::from_str(content)
282 .map_err(|e| SourceError::parse_failed("", "json", &e.to_string())),
283
284 #[cfg(feature = "yaml")]
285 Self::Yaml => serde_yaml::from_str(content)
286 .map_err(|e| SourceError::parse_failed("", "yaml", &e.to_string())),
287
288 Self::Unknown => Err(SourceError::unsupported("cannot parse unknown format")),
289 }
290 }
291
292 #[cfg(feature = "toml")]
293 fn parse_toml(&self, content: &str) -> Result<ParsedContent> {
294 let value: toml::Value = toml::from_str(content)
295 .map_err(|e| SourceError::parse_failed("", "toml", &e.to_string()))?;
296 Ok(ParsedContent::from_toml(value))
297 }
298
299 #[cfg(feature = "json")]
300 fn parse_json(&self, content: &str) -> Result<ParsedContent> {
301 let value: serde_json::Value = serde_json::from_str(content)
302 .map_err(|e| SourceError::parse_failed("", "json", &e.to_string()))?;
303 Ok(ParsedContent::from_json(value))
304 }
305
306 #[cfg(feature = "yaml")]
307 fn parse_yaml(&self, content: &str) -> Result<ParsedContent> {
308 let value: serde_yaml::Value = serde_yaml::from_str(content)
309 .map_err(|e| SourceError::parse_failed("", "yaml", &e.to_string()))?;
310 Ok(ParsedContent::from_yaml(value))
311 }
312
313 #[must_use]
315 pub const fn is_known(&self) -> bool {
316 !matches!(self, Self::Unknown)
317 }
318
319 #[must_use]
323 pub const fn is_binary(&self) -> bool {
324 false
325 }
326}
327
328impl std::fmt::Display for Format {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 write!(f, "{}", self.as_str())
331 }
332}
333
334impl Default for Format {
335 fn default() -> Self {
336 Self::Unknown
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn test_format_extension() {
346 #[cfg(feature = "toml")]
347 assert_eq!(Format::Toml.extension(), "toml");
348
349 #[cfg(feature = "json")]
350 assert_eq!(Format::Json.extension(), "json");
351
352 #[cfg(feature = "yaml")]
353 assert_eq!(Format::Yaml.extension(), "yaml");
354
355 assert_eq!(Format::Unknown.extension(), "");
356 }
357
358 #[test]
359 fn test_format_from_extension() {
360 #[cfg(feature = "toml")]
361 assert_eq!(Format::from_extension("toml"), Some(Format::Toml));
362
363 #[cfg(feature = "json")]
364 assert_eq!(Format::from_extension("json"), Some(Format::Json));
365
366 #[cfg(feature = "yaml")]
367 {
368 assert_eq!(Format::from_extension("yaml"), Some(Format::Yaml));
369 assert_eq!(Format::from_extension("yml"), Some(Format::Yaml));
370 }
371
372 assert_eq!(Format::from_extension("unknown"), None);
373 }
374
375 #[test]
376 fn test_format_from_path() {
377 #[cfg(feature = "toml")]
378 assert_eq!(
379 Format::from_path(std::path::Path::new("config.toml")),
380 Some(Format::Toml)
381 );
382
383 #[cfg(feature = "json")]
384 assert_eq!(
385 Format::from_path(std::path::Path::new("data.json")),
386 Some(Format::Json)
387 );
388
389 assert_eq!(Format::from_path(std::path::Path::new("README")), None);
390 }
391
392 #[test]
393 fn test_format_as_str() {
394 #[cfg(feature = "toml")]
395 assert_eq!(Format::Toml.as_str(), "toml");
396
397 assert_eq!(Format::Unknown.as_str(), "unknown");
398 }
399
400 #[test]
401 fn test_format_display() {
402 #[cfg(feature = "toml")]
403 assert_eq!(format!("{}", Format::Toml), "toml");
404
405 assert_eq!(format!("{}", Format::Unknown), "unknown");
406 }
407
408 #[test]
409 fn test_format_is_known() {
410 #[cfg(feature = "toml")]
411 assert!(Format::Toml.is_known());
412
413 assert!(!Format::Unknown.is_known());
414 }
415
416 #[test]
417 #[cfg(feature = "toml")]
418 fn test_format_parse_toml() {
419 let content = r#"
420 [server]
421 host = "localhost"
422 port = 8080
423 "#;
424
425 let result = Format::Toml.parse(content);
426 assert!(result.is_ok());
427 }
428
429 #[test]
430 #[cfg(feature = "json")]
431 fn test_format_parse_json() {
432 let content = r#"{"server": {"host": "localhost", "port": 8080}}"#;
433
434 let result = Format::Json.parse(content);
435 assert!(result.is_ok());
436 }
437
438 #[test]
439 #[cfg(feature = "yaml")]
440 fn test_format_parse_yaml() {
441 let content = r#"
442 server:
443 host: localhost
444 port: 8080
445 "#;
446
447 let result = Format::Yaml.parse(content);
448 assert!(result.is_ok());
449 }
450
451 #[test]
452 fn test_format_parse_unknown() {
453 let result = Format::Unknown.parse("some content");
454 assert!(result.is_err());
455 }
456
457 #[test]
458 #[cfg(feature = "toml")]
459 fn test_format_parse_as_toml() {
460 use serde::Deserialize;
461
462 #[derive(Debug, Deserialize, PartialEq)]
463 struct Server {
464 host: String,
465 port: u16,
466 }
467
468 #[derive(Debug, Deserialize)]
469 struct Config {
470 server: Server,
471 }
472
473 let content = r#"
474 [server]
475 host = "localhost"
476 port = 8080
477 "#;
478
479 let config: Config = Format::Toml.parse_as(content).unwrap();
480 assert_eq!(config.server.host, "localhost");
481 assert_eq!(config.server.port, 8080);
482 }
483
484 #[test]
485 #[cfg(feature = "json")]
486 fn test_format_from_content_json() {
487 let content = r#"{"key": "value"}"#;
488 let format = Format::from_content(content);
489 assert_eq!(format, Some(Format::Json));
490 }
491
492 #[test]
493 fn test_format_mime_type() {
494 #[cfg(feature = "json")]
495 assert_eq!(Format::Json.mime_type(), "application/json");
496
497 #[cfg(feature = "toml")]
498 assert_eq!(Format::Toml.mime_type(), "application/toml");
499
500 assert_eq!(Format::Unknown.mime_type(), "application/octet-stream");
501 }
502
503 #[test]
504 fn test_format_serialization() {
505 #[cfg(feature = "toml")]
506 {
507 let format = Format::Toml;
508 let json = serde_json::to_string(&format).unwrap();
509 assert_eq!(json, "\"toml\"");
510 }
511 }
512}