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)]
33pub enum Format {
34 Json,
35 #[cfg(feature = "toml")]
36 Toml,
37 #[cfg(feature = "yaml")]
38 Yaml,
39 #[cfg(feature = "dotenv")]
40 Dotenv,
41 #[cfg(feature = "ini")]
42 Ini,
43 #[cfg(feature = "toml")]
46 TomlFrontmatter,
47 #[cfg(feature = "yaml")]
50 YamlFrontmatter,
51 #[cfg(feature = "markdown")]
56 Markdown,
57}
58
59impl Format {
60 #[must_use]
62 pub const fn name(self) -> &'static str {
63 match self {
64 Self::Json => "JSON",
65 #[cfg(feature = "toml")]
66 Self::Toml => "TOML",
67 #[cfg(feature = "yaml")]
68 Self::Yaml => "YAML",
69 #[cfg(feature = "dotenv")]
70 Self::Dotenv => "dotenv",
71 #[cfg(feature = "ini")]
72 Self::Ini => "INI",
73 #[cfg(feature = "toml")]
74 Self::TomlFrontmatter => "TOML frontmatter",
75 #[cfg(feature = "yaml")]
76 Self::YamlFrontmatter => "YAML frontmatter",
77 #[cfg(feature = "markdown")]
78 Self::Markdown => "Markdown",
79 }
80 }
81
82 #[must_use]
89 pub const fn is_read_only(self) -> bool {
90 match self {
91 #[cfg(feature = "markdown")]
92 Self::Markdown => true,
93 _ => false,
94 }
95 }
96
97 #[must_use]
109 pub const fn array_rule(self) -> Option<crate::document::ArrayRule<'static>> {
110 match self {
111 #[cfg(feature = "markdown")]
112 Self::Markdown => Some(crate::document::ArrayRule {
113 field: "text",
114 match_kind: crate::document::MatchKind::Contains,
115 }),
116 _ => None,
117 }
118 }
119
120 pub(crate) fn read_only_error(self, operation: &str) -> DocumentError {
124 DocumentError::UnsupportedOperation {
125 format: self.name().to_string(),
126 operation: operation.to_string(),
127 detail: format!(
128 "{} is a read-only format: afdata reads its structure and never writes it",
129 self.name()
130 ),
131 }
132 }
133
134 #[cfg(any(feature = "toml", feature = "yaml"))]
138 fn frontmatter_save_error() -> DocumentError {
139 DocumentError::UnsupportedOperation {
140 format: "frontmatter".to_string(),
141 operation: "save".to_string(),
142 detail: "frontmatter mode has no whole-document re-render; the Markdown body is not \
143 part of the parsed value — use source-preserving set/unset"
144 .to_string(),
145 }
146 }
147
148 #[must_use]
152 pub const fn cli_name(self) -> &'static str {
153 match self {
154 Self::Json => "json",
155 #[cfg(feature = "toml")]
156 Self::Toml => "toml",
157 #[cfg(feature = "yaml")]
158 Self::Yaml => "yaml",
159 #[cfg(feature = "dotenv")]
160 Self::Dotenv => "dotenv",
161 #[cfg(feature = "ini")]
162 Self::Ini => "ini",
163 #[cfg(feature = "toml")]
164 Self::TomlFrontmatter => "toml-frontmatter",
165 #[cfg(feature = "yaml")]
166 Self::YamlFrontmatter => "yaml-frontmatter",
167 #[cfg(feature = "markdown")]
168 Self::Markdown => "markdown",
169 }
170 }
171
172 pub fn detect(path: &Path) -> Option<Self> {
174 match Self::extension_kind(path)? {
175 #[cfg(feature = "dotenv")]
176 "dotenv" => Some(Format::Dotenv),
177 "json" => Some(Format::Json),
178 #[cfg(feature = "toml")]
179 "toml" => Some(Format::Toml),
180 #[cfg(feature = "yaml")]
181 "yaml" => Some(Format::Yaml),
182 #[cfg(feature = "ini")]
183 "ini" => Some(Format::Ini),
184 _ => None,
185 }
186 }
187
188 #[must_use]
196 pub fn unavailable(path: &Path) -> Option<&'static str> {
197 if Self::detect(path).is_some() {
198 return None;
199 }
200 match Self::extension_kind(path)? {
201 "dotenv" => Some("dotenv"),
202 "toml" => Some("toml"),
203 "yaml" => Some("yaml"),
204 "ini" => Some("ini"),
205 _ => None,
207 }
208 }
209
210 fn extension_kind(path: &Path) -> Option<&'static str> {
215 let file_name = path.file_name().and_then(|name| name.to_str())?;
216 let file_name_lower = file_name.to_lowercase();
217 if file_name_lower == ".env"
218 || file_name_lower.starts_with(".env.")
219 || path
220 .extension()
221 .and_then(|ext| ext.to_str())
222 .is_some_and(|ext| ext.eq_ignore_ascii_case("env"))
223 {
224 return Some("dotenv");
225 }
226
227 match path
228 .extension()
229 .and_then(|ext| ext.to_str())?
230 .to_lowercase()
231 .as_str()
232 {
233 "json" => Some("json"),
234 "toml" => Some("toml"),
235 "yaml" | "yml" => Some("yaml"),
236 "ini" => Some("ini"),
237 _ => None,
238 }
239 }
240
241 pub fn load(&self, content: &str) -> DocumentResult<Value> {
243 match self {
244 Format::Json => json::load(content),
245
246 #[cfg(feature = "toml")]
247 Format::Toml => toml::load(content),
248
249 #[cfg(feature = "yaml")]
250 Format::Yaml => yaml::load(content),
251
252 #[cfg(feature = "dotenv")]
253 Format::Dotenv => dotenv::load(content),
254
255 #[cfg(feature = "ini")]
256 Format::Ini => ini::load(content),
257
258 #[cfg(feature = "toml")]
259 Format::TomlFrontmatter => {
260 toml::load(frontmatter::split(content, frontmatter::Delimiter::Plus)?.frontmatter)
261 }
262
263 #[cfg(feature = "yaml")]
264 Format::YamlFrontmatter => {
265 yaml::load(frontmatter::split(content, frontmatter::Delimiter::Dash)?.frontmatter)
266 }
267
268 #[cfg(feature = "markdown")]
269 Format::Markdown => markdown::load(content),
270 }
271 }
272
273 pub fn save(&self, value: &Value) -> DocumentResult<String> {
275 match self {
276 Format::Json => json::save(value),
277
278 #[cfg(feature = "toml")]
279 Format::Toml => toml::save(value),
280
281 #[cfg(feature = "yaml")]
282 Format::Yaml => yaml::save(value),
283
284 #[cfg(feature = "dotenv")]
285 Format::Dotenv => dotenv::save(value),
286
287 #[cfg(feature = "ini")]
288 Format::Ini => ini::save(value),
289
290 #[cfg(feature = "toml")]
295 Format::TomlFrontmatter => Err(Self::frontmatter_save_error()),
296 #[cfg(feature = "yaml")]
297 Format::YamlFrontmatter => Err(Self::frontmatter_save_error()),
298
299 #[cfg(feature = "markdown")]
302 Format::Markdown => Err(self.read_only_error("save")),
303 }
304 }
305}
306
307#[cfg(feature = "dotenv")]
308pub use dotenv::load as load_dotenv;
309pub use json::{load as load_json, save as save_json};
310#[cfg(feature = "markdown")]
311pub use markdown::load as load_markdown;
312#[cfg(feature = "toml")]
313pub use toml::{load as load_toml, save as save_toml};
314#[cfg(feature = "yaml")]
315pub use yaml::{load as load_yaml, save as save_yaml};
316
317#[cfg(all(
321 test,
322 feature = "toml",
323 feature = "yaml",
324 feature = "dotenv",
325 feature = "ini",
326 feature = "markdown"
327))]
328mod tests {
329 use super::Format;
330 use std::path::Path;
331
332 #[test]
333 fn format_names_are_stable() {
334 let cases = [
335 (Format::Json, "JSON"),
336 (Format::Toml, "TOML"),
337 (Format::Yaml, "YAML"),
338 (Format::Dotenv, "dotenv"),
339 (Format::Ini, "INI"),
340 (Format::TomlFrontmatter, "TOML frontmatter"),
341 (Format::YamlFrontmatter, "YAML frontmatter"),
342 (Format::Markdown, "Markdown"),
343 ];
344
345 for (format, expected) in cases {
346 assert_eq!(format.name(), expected);
347 }
348
349 let cli_names = [
350 (Format::Json, "json"),
351 (Format::Toml, "toml"),
352 (Format::Yaml, "yaml"),
353 (Format::Dotenv, "dotenv"),
354 (Format::Ini, "ini"),
355 (Format::TomlFrontmatter, "toml-frontmatter"),
356 (Format::YamlFrontmatter, "yaml-frontmatter"),
357 (Format::Markdown, "markdown"),
358 ];
359 for (format, expected) in cli_names {
360 assert_eq!(format.cli_name(), expected);
361 }
362 }
363
364 #[test]
365 fn markdown_is_the_only_read_only_format() {
366 for format in [
367 Format::Json,
368 Format::Toml,
369 Format::Yaml,
370 Format::Dotenv,
371 Format::Ini,
372 Format::TomlFrontmatter,
373 Format::YamlFrontmatter,
374 ] {
375 let name = format.name();
376 assert!(!format.is_read_only(), "{name} must stay writable");
377 }
378 assert!(Format::Markdown.is_read_only());
379 }
380
381 #[test]
382 fn markdown_is_never_detected_from_an_extension() {
383 assert_eq!(Format::detect(Path::new("README.md")), None);
387 assert_eq!(Format::detect(Path::new("README.markdown")), None);
388 }
389}