use anyhow::{anyhow, Result};
use std::path::Path;
pub const CANONICAL_FILENAME: &str = ".metarepo";
pub const LEGACY_FILENAME: &str = ".meta";
pub const KNOWN_FILENAMES: &[&str] = &[
CANONICAL_FILENAME,
LEGACY_FILENAME,
".metarepo.json",
".metarepo.yaml",
".metarepo.yml",
".metarepo.toml",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConfigFormat {
Json,
Yaml,
Toml,
}
impl ConfigFormat {
pub fn from_path(path: &Path) -> Option<Self> {
let name = path.file_name()?.to_str()?;
if name == CANONICAL_FILENAME || name == LEGACY_FILENAME {
return Some(ConfigFormat::Json);
}
let (stem, ext) = name.rsplit_once('.')?;
if stem != ".metarepo" && stem != "metarepo" && stem != ".meta" && stem != "meta" {
return None;
}
match ext.to_ascii_lowercase().as_str() {
"json" => Some(ConfigFormat::Json),
"yaml" | "yml" => Some(ConfigFormat::Yaml),
"toml" => Some(ConfigFormat::Toml),
_ => None,
}
}
pub fn parse(name: &str) -> Result<Self> {
match name.to_ascii_lowercase().as_str() {
"json" => Ok(ConfigFormat::Json),
"yaml" | "yml" => Ok(ConfigFormat::Yaml),
"toml" => Ok(ConfigFormat::Toml),
other => Err(anyhow!(
"Unknown config format '{}'. Expected json, yaml, or toml.",
other
)),
}
}
pub fn canonical_filename(self) -> &'static str {
match self {
ConfigFormat::Json => CANONICAL_FILENAME,
ConfigFormat::Yaml => ".metarepo.yaml",
ConfigFormat::Toml => ".metarepo.toml",
}
}
pub fn label(self) -> &'static str {
match self {
ConfigFormat::Json => "json",
ConfigFormat::Yaml => "yaml",
ConfigFormat::Toml => "toml",
}
}
}
pub fn serialize_to_string<T: serde::Serialize>(value: &T, format: ConfigFormat) -> Result<String> {
match format {
ConfigFormat::Json => Ok(serde_json::to_string_pretty(value)?),
ConfigFormat::Yaml => Ok(serde_yaml::to_string(value)?),
ConfigFormat::Toml => Ok(toml::to_string_pretty(value)?),
}
}
pub fn deserialize_from_str<T: serde::de::DeserializeOwned>(
content: &str,
format: ConfigFormat,
) -> Result<T> {
match format {
ConfigFormat::Json => Ok(serde_json::from_str(content)?),
ConfigFormat::Yaml => Ok(serde_yaml::from_str(content)?),
ConfigFormat::Toml => Ok(toml::from_str(content)?),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn detects_canonical_and_legacy_as_json() {
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".metarepo")),
Some(ConfigFormat::Json)
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".meta")),
Some(ConfigFormat::Json)
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from("/x/y/.metarepo")),
Some(ConfigFormat::Json)
);
}
#[test]
fn detects_extension_variants() {
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".metarepo.json")),
Some(ConfigFormat::Json)
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".metarepo.yaml")),
Some(ConfigFormat::Yaml)
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".metarepo.yml")),
Some(ConfigFormat::Yaml)
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".metarepo.toml")),
Some(ConfigFormat::Toml)
);
}
#[test]
fn rejects_unrelated_paths() {
assert_eq!(
ConfigFormat::from_path(&PathBuf::from("package.json")),
None
);
assert_eq!(
ConfigFormat::from_path(&PathBuf::from(".meta.yaml")),
Some(ConfigFormat::Yaml)
);
assert_eq!(ConfigFormat::from_path(&PathBuf::from(".rando")), None);
}
#[test]
fn parses_format_names() {
assert_eq!(ConfigFormat::parse("json").unwrap(), ConfigFormat::Json);
assert_eq!(ConfigFormat::parse("YAML").unwrap(), ConfigFormat::Yaml);
assert_eq!(ConfigFormat::parse("yml").unwrap(), ConfigFormat::Yaml);
assert_eq!(ConfigFormat::parse("toml").unwrap(), ConfigFormat::Toml);
assert!(ConfigFormat::parse("xml").is_err());
}
#[test]
fn canonical_filenames_are_what_we_advertise() {
assert_eq!(ConfigFormat::Json.canonical_filename(), ".metarepo");
assert_eq!(ConfigFormat::Yaml.canonical_filename(), ".metarepo.yaml");
assert_eq!(ConfigFormat::Toml.canonical_filename(), ".metarepo.toml");
}
}