agent_first_data/document/format/
mod.rs1#[allow(unused_imports)]
4use crate::document::{DocumentError, DocumentResult, Value};
5use std::path::Path;
6
7#[cfg(feature = "dotenv")]
8pub mod dotenv;
9pub mod frontmatter;
12#[cfg(feature = "ini")]
13pub mod ini;
14pub mod json;
17#[cfg(feature = "markdown")]
18pub mod markdown;
19#[cfg(feature = "toml")]
20pub mod toml;
21#[cfg(feature = "yaml")]
22pub mod yaml;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Format {
26 Json,
27 Toml,
28 Yaml,
29 Dotenv,
30 Ini,
31 TomlFrontmatter,
34 YamlFrontmatter,
37 Markdown,
42}
43
44impl Format {
45 #[must_use]
47 pub const fn name(self) -> &'static str {
48 match self {
49 Self::Json => "JSON",
50 Self::Toml => "TOML",
51 Self::Yaml => "YAML",
52 Self::Dotenv => "dotenv",
53 Self::Ini => "INI",
54 Self::TomlFrontmatter => "TOML frontmatter",
55 Self::YamlFrontmatter => "YAML frontmatter",
56 Self::Markdown => "Markdown",
57 }
58 }
59
60 #[must_use]
67 pub const fn is_read_only(self) -> bool {
68 matches!(self, Self::Markdown)
69 }
70
71 #[must_use]
83 pub const fn array_rule(self) -> Option<crate::document::ArrayRule<'static>> {
84 match self {
85 Self::Markdown => Some(crate::document::ArrayRule {
86 field: "text",
87 match_kind: crate::document::MatchKind::Contains,
88 }),
89 _ => None,
90 }
91 }
92
93 pub(crate) fn read_only_error(self, operation: &str) -> DocumentError {
97 DocumentError::UnsupportedOperation {
98 format: self.name().to_string(),
99 operation: operation.to_string(),
100 detail: format!(
101 "{} is a read-only format: afdata reads its structure and never writes it",
102 self.name()
103 ),
104 }
105 }
106
107 #[must_use]
111 pub const fn cli_name(self) -> &'static str {
112 match self {
113 Self::Json => "json",
114 Self::Toml => "toml",
115 Self::Yaml => "yaml",
116 Self::Dotenv => "dotenv",
117 Self::Ini => "ini",
118 Self::TomlFrontmatter => "toml-frontmatter",
119 Self::YamlFrontmatter => "yaml-frontmatter",
120 Self::Markdown => "markdown",
121 }
122 }
123
124 pub fn detect(path: &Path) -> Option<Self> {
126 let file_name = path.file_name().and_then(|name| name.to_str())?;
127 let file_name_lower = file_name.to_lowercase();
128 if file_name_lower == ".env"
129 || file_name_lower.starts_with(".env.")
130 || path
131 .extension()
132 .and_then(|ext| ext.to_str())
133 .is_some_and(|ext| ext.eq_ignore_ascii_case("env"))
134 {
135 return Some(Format::Dotenv);
136 }
137
138 path.extension().and_then(|ext| ext.to_str()).and_then(|s| {
139 match s.to_lowercase().as_str() {
140 "json" => Some(Format::Json),
141 "toml" => Some(Format::Toml),
142 "yaml" | "yml" => Some(Format::Yaml),
143 "ini" => Some(Format::Ini),
144 _ => None,
145 }
146 })
147 }
148
149 pub fn load(&self, content: &str) -> DocumentResult<Value> {
151 match self {
152 Format::Json => json::load(content),
153
154 #[cfg(feature = "toml")]
155 Format::Toml => toml::load(content),
156 #[cfg(not(feature = "toml"))]
157 Format::Toml => Err(DocumentError::UnsupportedOperation {
158 format: "TOML".to_string(),
159 operation: "load".to_string(),
160 detail: "requires Cargo feature `toml`".to_string(),
161 }),
162
163 #[cfg(feature = "yaml")]
164 Format::Yaml => yaml::load(content),
165 #[cfg(not(feature = "yaml"))]
166 Format::Yaml => Err(DocumentError::UnsupportedOperation {
167 format: "YAML".to_string(),
168 operation: "load".to_string(),
169 detail: "requires Cargo feature `yaml`".to_string(),
170 }),
171
172 #[cfg(feature = "dotenv")]
173 Format::Dotenv => dotenv::load(content),
174 #[cfg(not(feature = "dotenv"))]
175 Format::Dotenv => Err(DocumentError::UnsupportedOperation {
176 format: "dotenv".to_string(),
177 operation: "load".to_string(),
178 detail: "requires Cargo feature `dotenv`".to_string(),
179 }),
180
181 #[cfg(feature = "ini")]
182 Format::Ini => ini::load(content),
183 #[cfg(not(feature = "ini"))]
184 Format::Ini => Err(DocumentError::UnsupportedOperation {
185 format: "INI".to_string(),
186 operation: "load".to_string(),
187 detail: "requires Cargo feature `ini`".to_string(),
188 }),
189
190 #[cfg(feature = "toml")]
191 Format::TomlFrontmatter => {
192 toml::load(frontmatter::split(content, frontmatter::Delimiter::Plus)?.frontmatter)
193 }
194 #[cfg(not(feature = "toml"))]
195 Format::TomlFrontmatter => Err(DocumentError::UnsupportedOperation {
196 format: "TOML frontmatter".to_string(),
197 operation: "load".to_string(),
198 detail: "requires Cargo feature `toml`".to_string(),
199 }),
200
201 #[cfg(feature = "yaml")]
202 Format::YamlFrontmatter => {
203 yaml::load(frontmatter::split(content, frontmatter::Delimiter::Dash)?.frontmatter)
204 }
205 #[cfg(not(feature = "yaml"))]
206 Format::YamlFrontmatter => Err(DocumentError::UnsupportedOperation {
207 format: "YAML frontmatter".to_string(),
208 operation: "load".to_string(),
209 detail: "requires Cargo feature `yaml`".to_string(),
210 }),
211
212 #[cfg(feature = "markdown")]
213 Format::Markdown => markdown::load(content),
214 #[cfg(not(feature = "markdown"))]
215 Format::Markdown => Err(DocumentError::UnsupportedOperation {
216 format: "Markdown".to_string(),
217 operation: "load".to_string(),
218 detail: "requires Cargo feature `markdown`".to_string(),
219 }),
220 }
221 }
222
223 pub fn save(&self, value: &Value) -> DocumentResult<String> {
225 match self {
226 Format::Json => json::save(value),
227
228 #[cfg(feature = "toml")]
229 Format::Toml => toml::save(value),
230 #[cfg(not(feature = "toml"))]
231 Format::Toml => Err(DocumentError::UnsupportedOperation {
232 format: "TOML".to_string(),
233 operation: "save".to_string(),
234 detail: "requires Cargo feature `toml`".to_string(),
235 }),
236
237 #[cfg(feature = "yaml")]
238 Format::Yaml => yaml::save(value),
239 #[cfg(not(feature = "yaml"))]
240 Format::Yaml => Err(DocumentError::UnsupportedOperation {
241 format: "YAML".to_string(),
242 operation: "save".to_string(),
243 detail: "requires Cargo feature `yaml`".to_string(),
244 }),
245
246 #[cfg(feature = "dotenv")]
247 Format::Dotenv => dotenv::save(value),
248 #[cfg(not(feature = "dotenv"))]
249 Format::Dotenv => Err(DocumentError::UnsupportedOperation {
250 format: "dotenv".to_string(),
251 operation: "save".to_string(),
252 detail: "requires Cargo feature `dotenv`".to_string(),
253 }),
254
255 #[cfg(feature = "ini")]
256 Format::Ini => ini::save(value),
257 #[cfg(not(feature = "ini"))]
258 Format::Ini => Err(DocumentError::UnsupportedOperation {
259 format: "INI".to_string(),
260 operation: "save".to_string(),
261 detail: "requires Cargo feature `ini`".to_string(),
262 }),
263
264 Format::TomlFrontmatter | Format::YamlFrontmatter => {
269 Err(DocumentError::UnsupportedOperation {
270 format: "frontmatter".to_string(),
271 operation: "save".to_string(),
272 detail:
273 "frontmatter mode has no whole-document re-render; the Markdown body is \
274 not part of the parsed value — use source-preserving set/unset"
275 .to_string(),
276 })
277 }
278
279 Format::Markdown => Err(self.read_only_error("save")),
282 }
283 }
284}
285
286#[cfg(feature = "dotenv")]
287pub use dotenv::load as load_dotenv;
288pub use json::{load as load_json, save as save_json};
289#[cfg(feature = "markdown")]
290pub use markdown::load as load_markdown;
291#[cfg(feature = "toml")]
292pub use toml::{load as load_toml, save as save_toml};
293#[cfg(feature = "yaml")]
294pub use yaml::{load as load_yaml, save as save_yaml};
295
296#[cfg(test)]
297mod tests {
298 use super::Format;
299 use std::path::Path;
300
301 #[test]
302 fn format_names_are_stable() {
303 let cases = [
304 (Format::Json, "JSON"),
305 (Format::Toml, "TOML"),
306 (Format::Yaml, "YAML"),
307 (Format::Dotenv, "dotenv"),
308 (Format::Ini, "INI"),
309 (Format::TomlFrontmatter, "TOML frontmatter"),
310 (Format::YamlFrontmatter, "YAML frontmatter"),
311 (Format::Markdown, "Markdown"),
312 ];
313
314 for (format, expected) in cases {
315 assert_eq!(format.name(), expected);
316 }
317
318 let cli_names = [
319 (Format::Json, "json"),
320 (Format::Toml, "toml"),
321 (Format::Yaml, "yaml"),
322 (Format::Dotenv, "dotenv"),
323 (Format::Ini, "ini"),
324 (Format::TomlFrontmatter, "toml-frontmatter"),
325 (Format::YamlFrontmatter, "yaml-frontmatter"),
326 (Format::Markdown, "markdown"),
327 ];
328 for (format, expected) in cli_names {
329 assert_eq!(format.cli_name(), expected);
330 }
331 }
332
333 #[test]
334 fn markdown_is_the_only_read_only_format() {
335 for format in [
336 Format::Json,
337 Format::Toml,
338 Format::Yaml,
339 Format::Dotenv,
340 Format::Ini,
341 Format::TomlFrontmatter,
342 Format::YamlFrontmatter,
343 ] {
344 let name = format.name();
345 assert!(!format.is_read_only(), "{name} must stay writable");
346 }
347 assert!(Format::Markdown.is_read_only());
348 }
349
350 #[test]
351 fn markdown_is_never_detected_from_an_extension() {
352 assert_eq!(Format::detect(Path::new("README.md")), None);
356 assert_eq!(Format::detect(Path::new("README.markdown")), None);
357 }
358}