#[allow(unused_imports)]
use crate::document::{DocumentError, DocumentResult, Value};
use std::path::Path;
#[cfg(feature = "dotenv")]
pub mod dotenv;
pub mod frontmatter;
#[cfg(feature = "ini")]
pub mod ini;
pub mod json;
#[cfg(feature = "markdown")]
pub mod markdown;
#[cfg(feature = "toml")]
pub mod toml;
#[cfg(feature = "yaml")]
pub mod yaml;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
#[cfg(feature = "toml")]
Toml,
#[cfg(feature = "yaml")]
Yaml,
#[cfg(feature = "dotenv")]
Dotenv,
#[cfg(feature = "ini")]
Ini,
#[cfg(feature = "toml")]
TomlFrontmatter,
#[cfg(feature = "yaml")]
YamlFrontmatter,
#[cfg(feature = "markdown")]
Markdown,
}
impl Format {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Json => "JSON",
#[cfg(feature = "toml")]
Self::Toml => "TOML",
#[cfg(feature = "yaml")]
Self::Yaml => "YAML",
#[cfg(feature = "dotenv")]
Self::Dotenv => "dotenv",
#[cfg(feature = "ini")]
Self::Ini => "INI",
#[cfg(feature = "toml")]
Self::TomlFrontmatter => "TOML frontmatter",
#[cfg(feature = "yaml")]
Self::YamlFrontmatter => "YAML frontmatter",
#[cfg(feature = "markdown")]
Self::Markdown => "Markdown",
}
}
#[must_use]
pub const fn is_read_only(self) -> bool {
match self {
#[cfg(feature = "markdown")]
Self::Markdown => true,
_ => false,
}
}
#[must_use]
pub const fn array_rule(self) -> Option<crate::document::ArrayRule<'static>> {
match self {
#[cfg(feature = "markdown")]
Self::Markdown => Some(crate::document::ArrayRule {
field: "text",
match_kind: crate::document::MatchKind::Contains,
}),
_ => None,
}
}
pub(crate) fn read_only_error(self, operation: &str) -> DocumentError {
DocumentError::UnsupportedOperation {
format: self.name().to_string(),
operation: operation.to_string(),
detail: format!(
"{} is a read-only format: afdata reads its structure and never writes it",
self.name()
),
}
}
#[cfg(any(feature = "toml", feature = "yaml"))]
fn frontmatter_save_error() -> DocumentError {
DocumentError::UnsupportedOperation {
format: "frontmatter".to_string(),
operation: "save".to_string(),
detail: "frontmatter mode has no whole-document re-render; the Markdown body is not \
part of the parsed value — use source-preserving set/unset"
.to_string(),
}
}
#[must_use]
pub const fn cli_name(self) -> &'static str {
match self {
Self::Json => "json",
#[cfg(feature = "toml")]
Self::Toml => "toml",
#[cfg(feature = "yaml")]
Self::Yaml => "yaml",
#[cfg(feature = "dotenv")]
Self::Dotenv => "dotenv",
#[cfg(feature = "ini")]
Self::Ini => "ini",
#[cfg(feature = "toml")]
Self::TomlFrontmatter => "toml-frontmatter",
#[cfg(feature = "yaml")]
Self::YamlFrontmatter => "yaml-frontmatter",
#[cfg(feature = "markdown")]
Self::Markdown => "markdown",
}
}
pub fn detect(path: &Path) -> Option<Self> {
match Self::extension_kind(path)? {
#[cfg(feature = "dotenv")]
"dotenv" => Some(Format::Dotenv),
"json" => Some(Format::Json),
#[cfg(feature = "toml")]
"toml" => Some(Format::Toml),
#[cfg(feature = "yaml")]
"yaml" => Some(Format::Yaml),
#[cfg(feature = "ini")]
"ini" => Some(Format::Ini),
_ => None,
}
}
#[must_use]
pub fn unavailable(path: &Path) -> Option<&'static str> {
if Self::detect(path).is_some() {
return None;
}
match Self::extension_kind(path)? {
"dotenv" => Some("dotenv"),
"toml" => Some("toml"),
"yaml" => Some("yaml"),
"ini" => Some("ini"),
_ => None,
}
}
fn extension_kind(path: &Path) -> Option<&'static str> {
let file_name = path.file_name().and_then(|name| name.to_str())?;
let file_name_lower = file_name.to_lowercase();
if file_name_lower == ".env"
|| file_name_lower.starts_with(".env.")
|| path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("env"))
{
return Some("dotenv");
}
match path
.extension()
.and_then(|ext| ext.to_str())?
.to_lowercase()
.as_str()
{
"json" => Some("json"),
"toml" => Some("toml"),
"yaml" | "yml" => Some("yaml"),
"ini" => Some("ini"),
_ => None,
}
}
pub fn load(&self, content: &str) -> DocumentResult<Value> {
match self {
Format::Json => json::load(content),
#[cfg(feature = "toml")]
Format::Toml => toml::load(content),
#[cfg(feature = "yaml")]
Format::Yaml => yaml::load(content),
#[cfg(feature = "dotenv")]
Format::Dotenv => dotenv::load(content),
#[cfg(feature = "ini")]
Format::Ini => ini::load(content),
#[cfg(feature = "toml")]
Format::TomlFrontmatter => {
toml::load(frontmatter::split(content, frontmatter::Delimiter::Plus)?.frontmatter)
}
#[cfg(feature = "yaml")]
Format::YamlFrontmatter => {
yaml::load(frontmatter::split(content, frontmatter::Delimiter::Dash)?.frontmatter)
}
#[cfg(feature = "markdown")]
Format::Markdown => markdown::load(content),
}
}
pub fn save(&self, value: &Value) -> DocumentResult<String> {
match self {
Format::Json => json::save(value),
#[cfg(feature = "toml")]
Format::Toml => toml::save(value),
#[cfg(feature = "yaml")]
Format::Yaml => yaml::save(value),
#[cfg(feature = "dotenv")]
Format::Dotenv => dotenv::save(value),
#[cfg(feature = "ini")]
Format::Ini => ini::save(value),
#[cfg(feature = "toml")]
Format::TomlFrontmatter => Err(Self::frontmatter_save_error()),
#[cfg(feature = "yaml")]
Format::YamlFrontmatter => Err(Self::frontmatter_save_error()),
#[cfg(feature = "markdown")]
Format::Markdown => Err(self.read_only_error("save")),
}
}
}
#[cfg(feature = "dotenv")]
pub use dotenv::load as load_dotenv;
pub use json::{load as load_json, save as save_json};
#[cfg(feature = "markdown")]
pub use markdown::load as load_markdown;
#[cfg(feature = "toml")]
pub use toml::{load as load_toml, save as save_toml};
#[cfg(feature = "yaml")]
pub use yaml::{load as load_yaml, save as save_yaml};
#[cfg(all(
test,
feature = "toml",
feature = "yaml",
feature = "dotenv",
feature = "ini",
feature = "markdown"
))]
mod tests {
use super::Format;
use std::path::Path;
#[test]
fn format_names_are_stable() {
let cases = [
(Format::Json, "JSON"),
(Format::Toml, "TOML"),
(Format::Yaml, "YAML"),
(Format::Dotenv, "dotenv"),
(Format::Ini, "INI"),
(Format::TomlFrontmatter, "TOML frontmatter"),
(Format::YamlFrontmatter, "YAML frontmatter"),
(Format::Markdown, "Markdown"),
];
for (format, expected) in cases {
assert_eq!(format.name(), expected);
}
let cli_names = [
(Format::Json, "json"),
(Format::Toml, "toml"),
(Format::Yaml, "yaml"),
(Format::Dotenv, "dotenv"),
(Format::Ini, "ini"),
(Format::TomlFrontmatter, "toml-frontmatter"),
(Format::YamlFrontmatter, "yaml-frontmatter"),
(Format::Markdown, "markdown"),
];
for (format, expected) in cli_names {
assert_eq!(format.cli_name(), expected);
}
}
#[test]
fn markdown_is_the_only_read_only_format() {
for format in [
Format::Json,
Format::Toml,
Format::Yaml,
Format::Dotenv,
Format::Ini,
Format::TomlFrontmatter,
Format::YamlFrontmatter,
] {
let name = format.name();
assert!(!format.is_read_only(), "{name} must stay writable");
}
assert!(Format::Markdown.is_read_only());
}
#[test]
fn markdown_is_never_detected_from_an_extension() {
assert_eq!(Format::detect(Path::new("README.md")), None);
assert_eq!(Format::detect(Path::new("README.markdown")), None);
}
}