Skip to main content

aam_core/
macros.rs

1/// Generates an AAM-based config loader struct that reads `.aam` files from
2/// a directory and exposes typed field accessors.
3///
4/// # Syntax
5///
6/// ```text
7/// define_aam_loader! {
8///     name: MyLoader,
9///     dir: "packages/",
10///
11///     list: {
12///         build_deps: String,
13///         run_deps: String,
14///     },
15///     opt: {
16///         updating_time: u64,
17///         version: String,
18///     },
19///     req: {
20///         description: String,
21///     },
22/// }
23/// ```
24///
25/// **Field sections** (all optional, omit any empty section):
26/// - `list: { name: InnerType }` → `fn name(&self, id) -> Result<Vec<InnerType>>`
27///   Returns empty vec when the key is missing.
28/// - `opt: { name: Type }` → `fn name(&self, id) -> Result<Option<Type>>`
29///   Returns `None` when the key is missing.
30/// - `req: { name: Type }` → `fn name(&self, id) -> Result<Type>`
31///   Returns an error when the key is missing.
32///
33/// **Generated methods** (always present):
34/// - `new()`, `load_dir(path)`, `load_aam(path)`
35/// - `get_dep(id) -> Option<Arc<AAM>>`
36/// - `get_all_ids() -> Vec<String>`
37///
38/// # Example
39///
40/// ```
41/// use aam_core::define_aam_loader;
42///
43/// define_aam_loader! {
44///     name: Deps,
45///     dir: "packages",
46///
47///     list: {
48///         build_deps: String,
49///         tags: String,
50///     },
51///     opt: {
52///         updating_time: u64,
53///     },
54///     req: {
55///         description: String,
56///     },
57/// }
58///
59/// // The struct Deps is generated with:
60/// // - Deps::new()
61/// // - deps.load_dir("path/") -> anyhow::Result<()>
62/// // - deps.get_dep("id") -> Option<Arc<AAM>>
63/// // - deps.get_all_ids() -> Vec<String>
64/// // - deps.build_deps("id") -> anyhow::Result<Vec<String>>
65/// // - deps.tags("id") -> anyhow::Result<Vec<String>>
66/// // - deps.updating_time("id") -> anyhow::Result<Option<u64>>
67/// // - deps.description("id") -> anyhow::Result<String>
68/// ```
69#[macro_export]
70macro_rules! define_aam_loader {
71    // ── Entry: full form with all sections ──
72    {
73        name: $struct_name:ident,
74        dir: $dir:expr,
75        list: { $($list_field:ident: $list_ty:ty),* $(,)? },
76        opt: { $($opt_field:ident: $opt_ty:ty),* $(,)? },
77        req: { $($req_field:ident: $req_ty:ty),* $(,)? },
78        $(,)?
79    } => {
80        define_aam_loader! {
81            @impl
82            name: $struct_name,
83            list: { $($list_field: $list_ty),* },
84            opt: { $($opt_field: $opt_ty),* },
85            req: { $($req_field: $req_ty),* },
86        }
87    };
88    // ── Two sections: list + opt ──
89    {
90        name: $struct_name:ident,
91        dir: $dir:expr,
92        list: { $($list_field:ident: $list_ty:ty),* $(,)? },
93        opt: { $($opt_field:ident: $opt_ty:ty),* $(,)? },
94        $(,)?
95    } => {
96        define_aam_loader! {
97            @impl
98            name: $struct_name,
99            list: { $($list_field: $list_ty),* },
100            opt: { $($opt_field: $opt_ty),* },
101            req: { },
102        }
103    };
104    // ── Two sections: list + req ──
105    {
106        name: $struct_name:ident,
107        dir: $dir:expr,
108        list: { $($list_field:ident: $list_ty:ty),* $(,)? },
109        req: { $($req_field:ident: $req_ty:ty),* $(,)? },
110        $(,)?
111    } => {
112        define_aam_loader! {
113            @impl
114            name: $struct_name,
115            list: { $($list_field: $list_ty),* },
116            opt: { },
117            req: { $($req_field: $req_ty),* },
118        }
119    };
120    // ── Two sections: opt + req ──
121    {
122        name: $struct_name:ident,
123        dir: $dir:expr,
124        opt: { $($opt_field:ident: $opt_ty:ty),* $(,)? },
125        req: { $($req_field:ident: $req_ty:ty),* $(,)? },
126        $(,)?
127    } => {
128        define_aam_loader! {
129            @impl
130            name: $struct_name,
131            list: { },
132            opt: { $($opt_field: $opt_ty),* },
133            req: { $($req_field: $req_ty),* },
134        }
135    };
136    // ── One section: list ──
137    {
138        name: $struct_name:ident,
139        dir: $dir:expr,
140        list: { $($list_field:ident: $list_ty:ty),* $(,)? },
141        $(,)?
142    } => {
143        define_aam_loader! {
144            @impl
145            name: $struct_name,
146            list: { $($list_field: $list_ty),* },
147            opt: { },
148            req: { },
149        }
150    };
151    // ── One section: opt ──
152    {
153        name: $struct_name:ident,
154        dir: $dir:expr,
155        opt: { $($opt_field:ident: $opt_ty:ty),* $(,)? },
156        $(,)?
157    } => {
158        define_aam_loader! {
159            @impl
160            name: $struct_name,
161            list: { },
162            opt: { $($opt_field: $opt_ty),* },
163            req: { },
164        }
165    };
166    // ── One section: req ──
167    {
168        name: $struct_name:ident,
169        dir: $dir:expr,
170        req: { $($req_field:ident: $req_ty:ty),* $(,)? },
171        $(,)?
172    } => {
173        define_aam_loader! {
174            @impl
175            name: $struct_name,
176            list: { },
177            opt: { },
178            req: { $($req_field: $req_ty),* },
179        }
180    };
181    // ── No fields ──
182    {
183        name: $struct_name:ident,
184        dir: $dir:expr,
185        $(,)?
186    } => {
187        define_aam_loader! {
188            @impl
189            name: $struct_name,
190            list: { },
191            opt: { },
192            req: { },
193        }
194    };
195    // ── Internal implementation ──
196    {
197        @impl
198        name: $struct_name:ident,
199        list: { $($list_field:ident: $list_ty:ty),* $(,)? },
200        opt: { $($opt_field:ident: $opt_ty:ty),* $(,)? },
201        req: { $($req_field:ident: $req_ty:ty),* $(,)? },
202        $(,)?
203    } => {
204        pub struct $struct_name {
205            deps: ::std::collections::HashMap<String, ::std::sync::Arc<$crate::aam::AAM>>,
206        }
207
208        impl Default for $struct_name {
209            fn default() -> Self {
210                Self::new()
211            }
212        }
213
214        #[allow(dead_code)]
215        impl $struct_name {
216            pub fn new() -> Self {
217                $struct_name {
218                    deps: ::std::collections::HashMap::new(),
219                }
220            }
221
222            pub fn load_dir<P: AsRef<::std::path::Path>>(&mut self, path: P) -> ::anyhow::Result<()> {
223                let files: Vec<_> = ::anyhow::Context::with_context(
224                    ::std::fs::read_dir(&path),
225                    || format!("Failed to read dir: {}", path.as_ref().display())
226                )?
227                    .filter_map(|entry| entry.ok())
228                    .map(|entry| entry.path())
229                    .filter(|p| {
230                        p.is_file()
231                            && p.extension().and_then(|s| s.to_str()) == Some("aam")
232                            && p.file_name().and_then(|s| s.to_str()) != Some("config.aam")
233                    })
234                    .collect();
235
236                for file in files {
237                    self.load_aam(&file)?;
238                }
239
240                Ok(())
241            }
242
243            pub fn load_aam<P: AsRef<::std::path::Path>>(&mut self, path: P) -> ::anyhow::Result<()> {
244                let model = $crate::aam::AAM::load(&path)
245                    .map_err(|e| ::anyhow::anyhow!("Failed to load {}: {:?}", path.as_ref().display(), e))?;
246
247                let id = model
248                    .get("id")
249                    .ok_or_else(|| ::anyhow::anyhow!("No 'id' in {}", path.as_ref().display()))?
250                    .to_string();
251
252                self.deps.insert(id, ::std::sync::Arc::new(model));
253                Ok(())
254            }
255
256            pub fn get_dep(&self, id: &str) -> Option<::std::sync::Arc<$crate::aam::AAM>> {
257                self.deps.get(id).cloned()
258            }
259
260            pub fn get_all_ids(&self) -> Vec<String> {
261                self.deps.keys().cloned().collect()
262            }
263
264            // ── List field getters (empty vec when missing) ──
265            $(
266                pub fn $list_field(&self, id: &str) -> ::anyhow::Result<Vec<$list_ty>> {
267                    let target = self
268                        .deps
269                        .get(id)
270                        .ok_or_else(|| ::anyhow::anyhow!("Package '{}' not found", id))?;
271
272                    let Some(raw) = target.get(stringify!($list_field)) else {
273                        return Ok(::std::vec::Vec::new());
274                    };
275
276                    let parsed = $crate::found_value::FoundValue::new(raw)
277                        .parse_list::<$list_ty>();
278                    match parsed {
279                        Some(Ok(v)) => Ok(v),
280                        Some(Err(_)) => Err(::anyhow::anyhow!(
281                            "Failed to parse list '{}' in package '{}'. Raw: {:?}",
282                            stringify!($list_field), id, raw
283                        )),
284                        None => Err(::anyhow::anyhow!(
285                            "Expected list format for '{}' in package '{}'. Raw: {:?}",
286                            stringify!($list_field), id, raw
287                        )),
288                    }
289                }
290            )*
291
292            // ── Optional scalar getters (None when missing) ──
293            $(
294                pub fn $opt_field(&self, id: &str) -> ::anyhow::Result<Option<$opt_ty>> {
295                    let target = self
296                        .deps
297                        .get(id)
298                        .ok_or_else(|| ::anyhow::anyhow!("Package '{}' not found", id))?;
299
300                    let Some(raw) = target.get(stringify!($opt_field)) else {
301                        return Ok(None);
302                    };
303
304                    let parsed = raw.parse::<$opt_ty>().map(Some);
305                    ::anyhow::Context::with_context(parsed, || {
306                            format!(
307                                "Failed to parse '{}' as {} for package '{}'. Raw: {:?}",
308                                stringify!($opt_field), stringify!($opt_ty), id, raw
309                            )
310                        })
311                }
312            )*
313
314            // ── Required scalar getters (error when missing) ──
315            $(
316                pub fn $req_field(&self, id: &str) -> ::anyhow::Result<$req_ty> {
317                    let target = self
318                        .deps
319                        .get(id)
320                        .ok_or_else(|| ::anyhow::anyhow!("Package '{}' not found", id))?;
321
322                    let raw = target
323                        .get(stringify!($req_field))
324                        .ok_or_else(|| ::anyhow::anyhow!(
325                            "Required field '{}' not found in package '{}'",
326                            stringify!($req_field), id
327                        ))?;
328
329                    let parsed = raw.parse::<$req_ty>();
330                    ::anyhow::Context::with_context(parsed, || {
331                            format!(
332                                "Failed to parse '{}' as {} for package '{}'. Raw: {:?}",
333                                stringify!($req_field), stringify!($req_ty), id, raw
334                            )
335                        })
336                }
337            )*
338        }
339    };
340}
341
342#[cfg(test)]
343mod tests {
344    use std::io::Write;
345
346    define_aam_loader! {
347        name: TestDeps,
348        dir: "does_not_matter",
349
350        list: {
351            build_deps: String,
352        },
353        opt: {
354            version: u64,
355        },
356        req: {
357            name: String,
358        },
359    }
360
361    #[test]
362    fn test_define_aam_loader_basic() {
363        let temp = tempfile::tempdir().unwrap();
364        let file_path = temp.path().join("pkg.aam");
365        let mut f = std::fs::File::create(&file_path).unwrap();
366        writeln!(
367            f,
368            "id = my-pkg\nname = hello\nbuild_deps = [dep-a, dep-b]\nversion = 42\n"
369        )
370        .unwrap();
371        drop(f);
372
373        let mut deps = TestDeps::new();
374        deps.load_aam(&file_path).unwrap();
375
376        let name = deps.name("my-pkg").unwrap();
377        assert_eq!(name, "hello");
378
379        let bdeps = deps.build_deps("my-pkg").unwrap();
380        assert_eq!(bdeps, vec!["dep-a", "dep-b"]);
381
382        let ver = deps.version("my-pkg").unwrap();
383        assert_eq!(ver, Some(42));
384
385        let ids = deps.get_all_ids();
386        assert_eq!(ids, vec!["my-pkg"]);
387    }
388
389    #[test]
390    fn test_define_aam_loader_missing_opt() {
391        let temp = tempfile::tempdir().unwrap();
392        let file_path = temp.path().join("pkg2.aam");
393        let mut f = std::fs::File::create(&file_path).unwrap();
394        writeln!(f, "id = pkg2\nname = test\nbuild_deps = []\n").unwrap();
395        drop(f);
396
397        let mut deps = TestDeps::new();
398        deps.load_aam(&file_path).unwrap();
399
400        let ver = deps.version("pkg2").unwrap();
401        assert_eq!(ver, None);
402
403        let bdeps = deps.build_deps("pkg2").unwrap();
404        assert!(bdeps.is_empty());
405    }
406
407    #[test]
408    fn test_define_aam_loader_load_dir() {
409        let temp = tempfile::tempdir().unwrap();
410        for i in 1..=3 {
411            let file_path = temp.path().join(format!("pkg{i}.aam"));
412            let mut f = std::fs::File::create(&file_path).unwrap();
413            writeln!(f, "id = pkg{i}\nname = package-{i}\nbuild_deps = []\n").unwrap();
414            drop(f);
415        }
416
417        let mut deps = TestDeps::new();
418        deps.load_dir(temp.path()).unwrap();
419
420        let ids = deps.get_all_ids();
421        assert_eq!(ids.len(), 3);
422        assert_eq!(deps.name("pkg2").unwrap(), "package-2");
423    }
424}