use std::path::Path;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use super::{ParsedContent, Result, SourceError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Format {
#[cfg(feature = "toml")]
Toml,
#[cfg(feature = "json")]
Json,
#[cfg(feature = "yaml")]
Yaml,
#[default]
Unknown,
}
impl Format {
#[must_use]
pub const fn all() -> &'static [Self] {
&[
#[cfg(feature = "toml")]
Self::Toml,
#[cfg(feature = "json")]
Self::Json,
#[cfg(feature = "yaml")]
Self::Yaml,
]
}
#[must_use]
pub const fn extension(&self) -> &'static str {
match self {
#[cfg(feature = "toml")]
Self::Toml => "toml",
#[cfg(feature = "json")]
Self::Json => "json",
#[cfg(feature = "yaml")]
Self::Yaml => "yaml",
Self::Unknown => "",
}
}
#[must_use]
pub const fn alternate_extensions(&self) -> &'static [&'static str] {
match self {
#[cfg(feature = "yaml")]
Self::Yaml => &["yml"],
_ => &[],
}
}
#[must_use]
pub const fn mime_type(&self) -> &'static str {
match self {
#[cfg(feature = "toml")]
Self::Toml => "application/toml",
#[cfg(feature = "json")]
Self::Json => "application/json",
#[cfg(feature = "yaml")]
Self::Yaml => "application/x-yaml",
Self::Unknown => "application/octet-stream",
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
#[cfg(feature = "toml")]
Self::Toml => "toml",
#[cfg(feature = "json")]
Self::Json => "json",
#[cfg(feature = "yaml")]
Self::Yaml => "yaml",
Self::Unknown => "unknown",
}
}
#[must_use]
pub fn from_path(path: &Path) -> Option<Self> {
let ext = path.extension()?.to_str()?.to_lowercase();
Self::from_extension(&ext)
}
#[must_use]
pub fn from_extension(ext: &str) -> Option<Self> {
let ext_lower = ext.to_lowercase();
#[cfg(feature = "toml")]
if ext_lower == "toml" {
return Some(Self::Toml);
}
#[cfg(feature = "json")]
if ext_lower == "json" {
return Some(Self::Json);
}
#[cfg(feature = "yaml")]
if ext_lower == "yaml" || ext_lower == "yml" {
return Some(Self::Yaml);
}
None
}
#[must_use]
pub fn from_content(content: &str) -> Option<Self> {
let trimmed = content.trim_start();
#[cfg(feature = "json")]
if trimmed.starts_with('{') || trimmed.starts_with('[') {
if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
return Some(Self::Json);
}
}
#[cfg(feature = "yaml")]
if trimmed.starts_with("---")
|| trimmed.contains(": ")
|| trimmed.lines().any(|line| line.trim().starts_with("- "))
{
return Some(Self::Yaml);
}
#[cfg(feature = "toml")]
if trimmed.contains(" = ")
|| trimmed.starts_with('[')
|| trimmed.lines().any(|line| {
let line = line.trim();
line.starts_with('#') || line.contains('=')
})
{
return Some(Self::Toml);
}
None
}
pub fn parse(&self, content: &str) -> Result<ParsedContent> {
match self {
#[cfg(feature = "toml")]
Self::Toml => Self::parse_toml(content),
#[cfg(feature = "json")]
Self::Json => Self::parse_json(content),
#[cfg(feature = "yaml")]
Self::Yaml => Self::parse_yaml(content),
Self::Unknown => Err(SourceError::unsupported("cannot parse unknown format")),
}
}
pub fn parse_as<T: DeserializeOwned>(&self, content: &str) -> Result<T> {
match self {
#[cfg(feature = "toml")]
Self::Toml => toml::from_str(content)
.map_err(|e| SourceError::parse_failed("", "toml", &e.to_string())),
#[cfg(feature = "json")]
Self::Json => serde_json::from_str(content)
.map_err(|e| SourceError::parse_failed("", "json", &e.to_string())),
#[cfg(feature = "yaml")]
Self::Yaml => serde_yaml::from_str(content)
.map_err(|e| SourceError::parse_failed("", "yaml", &e.to_string())),
Self::Unknown => Err(SourceError::unsupported("cannot parse unknown format")),
}
}
#[cfg(feature = "toml")]
fn parse_toml(content: &str) -> Result<ParsedContent> {
let value: toml::Value = toml::from_str(content)
.map_err(|e| SourceError::parse_failed("", "toml", &e.to_string()))?;
Ok(ParsedContent::from_toml(value))
}
#[cfg(feature = "json")]
fn parse_json(content: &str) -> Result<ParsedContent> {
let value: serde_json::Value = serde_json::from_str(content)
.map_err(|e| SourceError::parse_failed("", "json", &e.to_string()))?;
Ok(ParsedContent::from_json(value))
}
#[cfg(feature = "yaml")]
fn parse_yaml(content: &str) -> Result<ParsedContent> {
let value: serde_yaml::Value = serde_yaml::from_str(content)
.map_err(|e| SourceError::parse_failed("", "yaml", &e.to_string()))?;
Ok(ParsedContent::from_yaml(value))
}
#[must_use]
pub const fn is_known(&self) -> bool {
!matches!(self, Self::Unknown)
}
#[must_use]
pub const fn is_binary(&self) -> bool {
false
}
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_extension() {
#[cfg(feature = "toml")]
assert_eq!(Format::Toml.extension(), "toml");
#[cfg(feature = "json")]
assert_eq!(Format::Json.extension(), "json");
#[cfg(feature = "yaml")]
assert_eq!(Format::Yaml.extension(), "yaml");
assert_eq!(Format::Unknown.extension(), "");
}
#[test]
fn test_format_from_extension() {
#[cfg(feature = "toml")]
assert_eq!(Format::from_extension("toml"), Some(Format::Toml));
#[cfg(feature = "json")]
assert_eq!(Format::from_extension("json"), Some(Format::Json));
#[cfg(feature = "yaml")]
{
assert_eq!(Format::from_extension("yaml"), Some(Format::Yaml));
assert_eq!(Format::from_extension("yml"), Some(Format::Yaml));
}
assert_eq!(Format::from_extension("unknown"), None);
}
#[test]
fn test_format_from_path() {
#[cfg(feature = "toml")]
assert_eq!(
Format::from_path(std::path::Path::new("config.toml")),
Some(Format::Toml)
);
#[cfg(feature = "json")]
assert_eq!(
Format::from_path(std::path::Path::new("data.json")),
Some(Format::Json)
);
assert_eq!(Format::from_path(std::path::Path::new("README")), None);
}
#[test]
fn test_format_as_str() {
#[cfg(feature = "toml")]
assert_eq!(Format::Toml.as_str(), "toml");
assert_eq!(Format::Unknown.as_str(), "unknown");
}
#[test]
fn test_format_display() {
#[cfg(feature = "toml")]
assert_eq!(format!("{}", Format::Toml), "toml");
assert_eq!(format!("{}", Format::Unknown), "unknown");
}
#[test]
fn test_format_is_known() {
#[cfg(feature = "toml")]
assert!(Format::Toml.is_known());
assert!(!Format::Unknown.is_known());
}
#[test]
#[cfg(feature = "toml")]
fn test_format_parse_toml() {
let content = r#"
[server]
host = "localhost"
port = 8080
"#;
let result = Format::Toml.parse(content);
assert!(result.is_ok());
}
#[test]
#[cfg(feature = "json")]
fn test_format_parse_json() {
let content = r#"{"server": {"host": "localhost", "port": 8080}}"#;
let result = Format::Json.parse(content);
assert!(result.is_ok());
}
#[test]
#[cfg(feature = "yaml")]
fn test_format_parse_yaml() {
let content = r"
server:
host: localhost
port: 8080
";
let result = Format::Yaml.parse(content);
assert!(result.is_ok());
}
#[test]
fn test_format_parse_unknown() {
let result = Format::Unknown.parse("some content");
assert!(result.is_err());
}
#[test]
#[cfg(feature = "toml")]
fn test_format_parse_as_toml() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Server {
host: String,
port: u16,
}
#[derive(Debug, Deserialize)]
struct Config {
server: Server,
}
let content = r#"
[server]
host = "localhost"
port = 8080
"#;
let config: Config = Format::Toml.parse_as(content).unwrap();
assert_eq!(config.server.host, "localhost");
assert_eq!(config.server.port, 8080);
}
#[test]
#[cfg(feature = "json")]
fn test_format_from_content_json() {
let content = r#"{"key": "value"}"#;
let format = Format::from_content(content);
assert_eq!(format, Some(Format::Json));
}
#[test]
fn test_format_mime_type() {
#[cfg(feature = "json")]
assert_eq!(Format::Json.mime_type(), "application/json");
#[cfg(feature = "toml")]
assert_eq!(Format::Toml.mime_type(), "application/toml");
assert_eq!(Format::Unknown.mime_type(), "application/octet-stream");
}
#[test]
fn test_format_serialization() {
#[cfg(feature = "toml")]
{
let format = Format::Toml;
let json = serde_json::to_string(&format).unwrap();
assert_eq!(json, "\"toml\"");
}
}
}