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