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 #[must_use]
180 pub fn from_cli_name(name: &str) -> Option<Self> {
181 match name.to_ascii_lowercase().as_str() {
182 "json" => Some(Self::Json),
183 #[cfg(feature = "toml")]
184 "toml" => Some(Self::Toml),
185 #[cfg(feature = "yaml")]
186 "yaml" | "yml" => Some(Self::Yaml),
187 #[cfg(feature = "dotenv")]
188 "dotenv" | "env" => Some(Self::Dotenv),
189 #[cfg(feature = "ini")]
190 "ini" => Some(Self::Ini),
191 #[cfg(feature = "toml")]
192 "toml-frontmatter" => Some(Self::TomlFrontmatter),
193 #[cfg(feature = "yaml")]
194 "yaml-frontmatter" => Some(Self::YamlFrontmatter),
195 #[cfg(feature = "markdown")]
196 "markdown" => Some(Self::Markdown),
197 _ => None,
198 }
199 }
200
201 pub fn detect(path: &Path) -> Option<Self> {
203 match Self::extension_kind(path)? {
204 #[cfg(feature = "dotenv")]
205 "dotenv" => Some(Format::Dotenv),
206 "json" => Some(Format::Json),
207 #[cfg(feature = "toml")]
208 "toml" => Some(Format::Toml),
209 #[cfg(feature = "yaml")]
210 "yaml" => Some(Format::Yaml),
211 #[cfg(feature = "ini")]
212 "ini" => Some(Format::Ini),
213 _ => None,
214 }
215 }
216
217 #[must_use]
225 pub fn unavailable(path: &Path) -> Option<&'static str> {
226 if Self::detect(path).is_some() {
227 return None;
228 }
229 match Self::extension_kind(path)? {
230 "dotenv" => Some("dotenv"),
231 "toml" => Some("toml"),
232 "yaml" => Some("yaml"),
233 "ini" => Some("ini"),
234 _ => None,
236 }
237 }
238
239 fn extension_kind(path: &Path) -> Option<&'static str> {
244 let file_name = path.file_name().and_then(|name| name.to_str())?;
245 let file_name_lower = file_name.to_lowercase();
246 if file_name_lower == ".env"
247 || file_name_lower.starts_with(".env.")
248 || path
249 .extension()
250 .and_then(|ext| ext.to_str())
251 .is_some_and(|ext| ext.eq_ignore_ascii_case("env"))
252 {
253 return Some("dotenv");
254 }
255
256 match path
257 .extension()
258 .and_then(|ext| ext.to_str())?
259 .to_lowercase()
260 .as_str()
261 {
262 "json" => Some("json"),
263 "toml" => Some("toml"),
264 "yaml" | "yml" => Some("yaml"),
265 "ini" => Some("ini"),
266 _ => None,
267 }
268 }
269
270 pub fn load(&self, content: &str) -> DocumentResult<Value> {
272 match self {
273 Format::Json => json::load(content),
274
275 #[cfg(feature = "toml")]
276 Format::Toml => toml::load(content),
277
278 #[cfg(feature = "yaml")]
279 Format::Yaml => yaml::load(content),
280
281 #[cfg(feature = "dotenv")]
282 Format::Dotenv => dotenv::load(content),
283
284 #[cfg(feature = "ini")]
285 Format::Ini => ini::load(content),
286
287 #[cfg(feature = "toml")]
288 Format::TomlFrontmatter => {
289 toml::load(frontmatter::split(content, frontmatter::Delimiter::Plus)?.frontmatter)
290 }
291
292 #[cfg(feature = "yaml")]
293 Format::YamlFrontmatter => {
294 yaml::load(frontmatter::split(content, frontmatter::Delimiter::Dash)?.frontmatter)
295 }
296
297 #[cfg(feature = "markdown")]
298 Format::Markdown => markdown::load(content),
299 }
300 }
301
302 pub fn save(&self, value: &Value) -> DocumentResult<String> {
304 match self {
305 Format::Json => json::save(value),
306
307 #[cfg(feature = "toml")]
308 Format::Toml => toml::save(value),
309
310 #[cfg(feature = "yaml")]
311 Format::Yaml => yaml::save(value),
312
313 #[cfg(feature = "dotenv")]
314 Format::Dotenv => dotenv::save(value),
315
316 #[cfg(feature = "ini")]
317 Format::Ini => ini::save(value),
318
319 #[cfg(feature = "toml")]
324 Format::TomlFrontmatter => Err(Self::frontmatter_save_error()),
325 #[cfg(feature = "yaml")]
326 Format::YamlFrontmatter => Err(Self::frontmatter_save_error()),
327
328 #[cfg(feature = "markdown")]
331 Format::Markdown => Err(self.read_only_error("save")),
332 }
333 }
334}
335
336#[cfg(feature = "dotenv")]
337pub use dotenv::load as load_dotenv;
338pub use json::{load as load_json, save as save_json};
339#[cfg(feature = "markdown")]
340pub use markdown::load as load_markdown;
341#[cfg(feature = "toml")]
342pub use toml::{load as load_toml, save as save_toml};
343#[cfg(feature = "yaml")]
344pub use yaml::{load as load_yaml, save as save_yaml};
345
346#[cfg(all(
350 test,
351 feature = "toml",
352 feature = "yaml",
353 feature = "dotenv",
354 feature = "ini",
355 feature = "markdown"
356))]
357mod tests {
358 use super::Format;
359 use std::path::Path;
360
361 #[test]
362 fn format_names_are_stable() {
363 let cases = [
364 (Format::Json, "JSON"),
365 (Format::Toml, "TOML"),
366 (Format::Yaml, "YAML"),
367 (Format::Dotenv, "dotenv"),
368 (Format::Ini, "INI"),
369 (Format::TomlFrontmatter, "TOML frontmatter"),
370 (Format::YamlFrontmatter, "YAML frontmatter"),
371 (Format::Markdown, "Markdown"),
372 ];
373
374 for (format, expected) in cases {
375 assert_eq!(format.name(), expected);
376 }
377
378 let cli_names = [
379 (Format::Json, "json"),
380 (Format::Toml, "toml"),
381 (Format::Yaml, "yaml"),
382 (Format::Dotenv, "dotenv"),
383 (Format::Ini, "ini"),
384 (Format::TomlFrontmatter, "toml-frontmatter"),
385 (Format::YamlFrontmatter, "yaml-frontmatter"),
386 (Format::Markdown, "markdown"),
387 ];
388 for (format, expected) in cli_names {
389 assert_eq!(format.cli_name(), expected);
390 }
391 }
392
393 #[test]
394 fn markdown_is_the_only_read_only_format() {
395 for format in [
396 Format::Json,
397 Format::Toml,
398 Format::Yaml,
399 Format::Dotenv,
400 Format::Ini,
401 Format::TomlFrontmatter,
402 Format::YamlFrontmatter,
403 ] {
404 let name = format.name();
405 assert!(!format.is_read_only(), "{name} must stay writable");
406 }
407 assert!(Format::Markdown.is_read_only());
408 }
409
410 #[test]
411 fn markdown_is_never_detected_from_an_extension() {
412 assert_eq!(Format::detect(Path::new("README.md")), None);
416 assert_eq!(Format::detect(Path::new("README.markdown")), None);
417 }
418}