Skip to main content

aam_core/
builder.rs

1//! Fluent builder for constructing AAML configuration content programmatically.
2//!
3//! [`AAMBuilder`] accumulates lines in memory and can either return them as a
4//! `String` or write them directly to a file. Useful in tests and code generators.
5//!
6
7#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
8
9//! # High-level directive API
10//!
11//! | Method | Directive emitted |
12//! |---|---|
13//! | [`AAMBuilder::schema`] | `@schema Name { ... }` |
14//! | [`AAMBuilder::derive`] | `@derive file.aam` / `@derive file.aam::A::B` |
15//! | [`AAMBuilder::import`] | `@import file.aam` |
16//! | [`AAMBuilder::type_alias`] | `@type alias = type` |
17//! | [`AAMBuilder::comment`] | `# ...` |
18//!
19//! # Example
20//! ```
21//! use aam_core::builder::{AAMBuilder, SchemaField};
22//!
23//! let mut b = AAMBuilder::new();
24//! b.comment("Server configuration")
25//!  .type_alias("port_t", "i32")
26//!  .schema("Server", [
27//!      SchemaField::required("host", "string"),
28//!      SchemaField::required("port", "port_t"),
29//!      SchemaField::optional("debug", "bool"),
30//!  ])
31//!  .add_line("host", "localhost")
32//!  .add_line("port", "8080");
33//!
34//! let content = b.build();
35//! assert!(content.contains("@schema Server {"));
36//! assert!(content.contains("host = localhost"));
37//! ```
38
39use std::fmt::Display;
40use std::fmt::Write;
41use std::io;
42use std::ops::Deref;
43use std::path::Path;
44
45/// A single field declaration inside a `@schema` block.
46///
47/// Fields declared with [`SchemaField::optional`] are emitted with a `*` suffix,
48/// meaning the key does not have to be present in the data map, but its value is
49/// still type-checked when it *is* present.
50///
51/// # Example
52/// ```
53/// use aam_core::builder::SchemaField;
54///
55/// let f = SchemaField::required("host", "string");
56/// assert_eq!(f.to_aaml(), "host: string");
57///
58/// let g = SchemaField::optional("debug", "bool");
59/// assert_eq!(g.to_aaml(), "debug*: bool");
60/// ```
61#[derive(Debug, Clone)]
62#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
63pub struct SchemaField {
64    name: String,
65    type_name: String,
66    optional: bool,
67}
68
69impl SchemaField {
70    /// Creates a **required** field (rendered as `name: type`).
71    pub fn required(name: impl Into<String>, type_name: impl Into<String>) -> Self {
72        Self {
73            name: name.into(),
74            type_name: type_name.into(),
75            optional: false,
76        }
77    }
78
79    /// Creates an **optional** field (rendered as `name*: type`).
80    pub fn optional(name: impl Into<String>, type_name: impl Into<String>) -> Self {
81        Self {
82            name: name.into(),
83            type_name: type_name.into(),
84            optional: true,
85        }
86    }
87
88    pub fn to_aaml_writer(&self, mut w: impl Write) -> std::fmt::Result {
89        write!(
90            w,
91            "{}{}: {}",
92            self.name,
93            if self.optional { "*" } else { "" },
94            self.type_name
95        )
96    }
97
98    /// Renders the field as an AAML field declaration string.
99    #[must_use]
100    pub fn to_aaml(&self) -> String {
101        let mut s = String::new();
102        self.to_aaml_writer(&mut s).unwrap();
103        s
104    }
105}
106
107/// Enumeration of all built-in AAML types for use with the Builder API.
108///
109/// Can be passed directly to [`AAMBuilder::type_alias`] and similar methods.
110///
111/// Only available with the `builder-extras` feature (enabled by default).
112///
113/// # Example
114/// ```
115/// use aam_core::builder::BuiltInType;
116///
117/// let t = BuiltInType::I32;
118/// assert_eq!(t.to_string(), "i32");
119///
120/// let v = BuiltInType::List(Box::new(BuiltInType::String));
121/// assert_eq!(v.to_string(), "list<string>");
122/// ```
123#[cfg(feature = "builder-extras")]
124#[derive(Debug, Clone, PartialEq)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126pub enum BuiltInType {
127    // ── Primitives ──────────────────────────────────────────────────────────────
128    I32,
129    F64,
130    String,
131    Bool,
132    Color,
133
134    // ── Math types ──────────────────────────────────────────────────────────────
135    Vector2,
136    Vector3,
137    Vector4,
138    Quaternion,
139    Matrix3x3,
140    Matrix4x4,
141
142    // ── Time types ──────────────────────────────────────────────────────────────
143    DateTime,
144    Duration,
145    Year,
146    Day,
147    Hour,
148    Minute,
149
150    // ── Physics types (common) ──────────────────────────────────────────────────
151    Kilogram,
152    Meter,
153
154    // ── Special ─────────────────────────────────────────────────────────────────
155    /// Generic inline object type (`schema`).
156    Schema,
157
158    /// Any other type specified as a raw string (e.g. `"physics::newton"`).
159    Custom(String),
160
161    /// List of another type (e.g. `list<f64>`).
162    List(Box<Self>),
163}
164
165#[cfg(feature = "builder-extras")]
166impl Display for BuiltInType {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::I32 => write!(f, "i32"),
170            Self::F64 => write!(f, "f64"),
171            Self::String => write!(f, "string"),
172            Self::Bool => write!(f, "bool"),
173            Self::Color => write!(f, "color"),
174            Self::Vector2 => write!(f, "math::vector2"),
175            Self::Vector3 => write!(f, "math::vector3"),
176            Self::Vector4 => write!(f, "math::vector4"),
177            Self::Quaternion => write!(f, "math::quaternion"),
178            Self::Matrix3x3 => write!(f, "math::matrix3x3"),
179            Self::Matrix4x4 => write!(f, "math::matrix4x4"),
180            Self::DateTime => write!(f, "time::datetime"),
181            Self::Duration => write!(f, "time::duration"),
182            Self::Year => write!(f, "time::year"),
183            Self::Day => write!(f, "time::day"),
184            Self::Hour => write!(f, "time::hour"),
185            Self::Minute => write!(f, "time::minute"),
186            Self::Kilogram => write!(f, "physics::kilogram"),
187            Self::Meter => write!(f, "physics::meter"),
188            Self::Schema => write!(f, "schema"),
189            Self::Custom(s) => write!(f, "{s}"),
190            Self::List(inner) => write!(f, "list<{inner}>"),
191        }
192    }
193}
194
195#[cfg(feature = "builder-extras")]
196impl From<&str> for BuiltInType {
197    fn from(s: &str) -> Self {
198        match s {
199            "i32" => Self::I32,
200            "f64" => Self::F64,
201            "string" => Self::String,
202            "bool" => Self::Bool,
203            "color" => Self::Color,
204            "math::vector2" => Self::Vector2,
205            "math::vector3" => Self::Vector3,
206            "math::vector4" => Self::Vector4,
207            "math::quaternion" => Self::Quaternion,
208            "math::matrix3x3" => Self::Matrix3x3,
209            "math::matrix4x4" => Self::Matrix4x4,
210            "time::datetime" => Self::DateTime,
211            "time::duration" => Self::Duration,
212            "time::year" => Self::Year,
213            "time::day" => Self::Day,
214            "time::hour" => Self::Hour,
215            "time::minute" => Self::Minute,
216            "physics::kilogram" => Self::Kilogram,
217            "physics::meter" => Self::Meter,
218            "schema" => Self::Schema,
219            _ => s
220                .strip_prefix("list<")
221                .and_then(|s| s.strip_suffix('>'))
222                .map_or_else(
223                    || Self::Custom(s.to_string()),
224                    |inner| Self::List(Box::new(Self::from(inner))),
225                ),
226        }
227    }
228}
229
230/// A builder for inline object literals `{ key = value, ... }`.
231///
232/// Only available with the `builder-extras` feature (enabled by default).
233///
234/// # Example
235/// ```
236/// use aam_core::builder::InlineObject;
237///
238/// let obj = InlineObject::new()
239///     .with_field("system", "cmake")
240///     .with_field("command", "cmake")
241///     .with_field("args", "[\"-G\", \"Ninja\"]");
242///
243/// assert_eq!(
244///     obj.to_string(),
245///     r#"{ system = cmake, command = cmake, args = ["-G", "Ninja"] }"#
246/// );
247/// ```
248#[cfg(feature = "builder-extras")]
249#[derive(Debug, Clone)]
250#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
251pub struct InlineObject {
252    fields: Vec<(String, String)>,
253}
254
255#[cfg(feature = "builder-extras")]
256impl InlineObject {
257    /// Creates a new empty inline object.
258    #[must_use]
259    pub const fn new() -> Self {
260        Self { fields: Vec::new() }
261    }
262
263    /// Adds a field to the inline object (builder-pattern, consumes self).
264    #[must_use]
265    pub fn with_field(mut self, key: &str, value: &str) -> Self {
266        self.fields.push((key.to_string(), value.to_string()));
267        self
268    }
269
270    /// Adds a field by mutable reference.
271    pub fn add_field(&mut self, key: &str, value: &str) -> &mut Self {
272        self.fields.push((key.to_string(), value.to_string()));
273        self
274    }
275
276    /// Returns the fields as a slice.
277    #[must_use]
278    pub fn fields(&self) -> &[(String, String)] {
279        &self.fields
280    }
281
282    /// Renders the object as an AAML inline object string.
283    #[must_use]
284    pub fn to_aaml(&self) -> String {
285        self.to_string()
286    }
287}
288
289#[cfg(feature = "builder-extras")]
290impl Default for InlineObject {
291    fn default() -> Self {
292        Self::new()
293    }
294}
295
296#[cfg(feature = "builder-extras")]
297impl Display for InlineObject {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        write!(f, "{{ ")?;
300        for (i, (k, v)) in self.fields.iter().enumerate() {
301            if i > 0 {
302                write!(f, ", ")?;
303            }
304            write!(f, "{k} = {v}")?;
305        }
306        write!(f, " }}")
307    }
308}
309
310#[cfg(feature = "builder-extras")]
311impl From<Vec<(String, String)>> for InlineObject {
312    fn from(fields: Vec<(String, String)>) -> Self {
313        Self { fields }
314    }
315}
316
317#[cfg(feature = "builder-extras")]
318impl From<InlineObject> for String {
319    fn from(obj: InlineObject) -> Self {
320        obj.to_string()
321    }
322}
323
324/// Parses an inline object string `{ key = value, ... }` into a `HashMap<String, String>`.
325///
326/// Nested structures (e.g. `[a, b]` inside values) are preserved as raw strings.
327///
328/// # Feature
329/// Enabled by default under the `builder-extras` feature.
330///
331/// # Example
332/// ```
333/// use aam_core::builder::parse_inline_to_map;
334///
335/// let map = parse_inline_to_map(r#"{ system = cmake, args = ["-G", "Ninja"] }"#).unwrap();
336/// assert_eq!(map.get("system").unwrap(), "cmake");
337/// assert_eq!(map.get("args").unwrap(), r#"["-G", "Ninja"]"#);
338/// ```
339#[cfg(feature = "builder-extras")]
340pub fn parse_inline_to_map(
341    s: &str,
342) -> Result<std::collections::HashMap<String, String>, crate::error::AamlError> {
343    let pairs = crate::aaml::parsing::parse_inline_object(s)?;
344    Ok(pairs.into_iter().collect())
345}
346
347/// Accumulates AAML source lines and can flush them to a file or a `String`.
348///
349/// # Example
350/// ```
351/// use aam_core::builder::AAMBuilder;
352///
353/// let mut b = AAMBuilder::new();
354/// b.add_line("host", "localhost");
355/// b.add_line("port", "8080");
356/// let content = b.build();
357/// assert!(content.contains("host = localhost"));
358/// ```
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360pub struct AAMBuilder {
361    buffer: String,
362}
363
364impl AAMBuilder {
365    /// Creates a new empty builder.
366    #[must_use]
367    pub const fn new() -> Self {
368        Self {
369            buffer: String::new(),
370        }
371    }
372
373    /// Creates a new builder with the given initial buffer capacity.
374    #[must_use]
375    pub fn with_capacity(capacity: usize) -> Self {
376        Self {
377            buffer: String::with_capacity(capacity),
378        }
379    }
380
381    // ── Internal helpers ─────────────────────────────────────────────────────
382
383    fn push_sep(&mut self) {
384        if !self.buffer.is_empty() {
385            self.buffer.push('\n');
386        }
387    }
388
389    // ── Key-value assignments ─────────────────────────────────────────────────
390
391    /// Appends a `key = value` assignment line.
392    ///
393    /// A newline separator is inserted automatically between entries.
394    /// Returns `&mut self` for chaining.
395    pub fn add_line(&mut self, key: &str, value: &str) -> &mut Self {
396        self.push_sep();
397        self.buffer.push_str(key);
398        self.buffer.push_str(" = ");
399        self.buffer.push_str(value);
400        self
401    }
402
403    // ── Comments ──────────────────────────────────────────────────────────────
404
405    /// Appends a `# text` comment line.
406    ///
407    /// Returns `&mut self` for chaining.
408    pub fn comment(&mut self, text: &str) -> &mut Self {
409        self.push_sep();
410        self.buffer.push_str("# ");
411        self.buffer.push_str(text);
412        self
413    }
414
415    // ── Directives ────────────────────────────────────────────────────────────
416
417    /// Appends a `@schema Name { field1: type1, field2*: type2, ... }` directive.
418    ///
419    /// Use [`SchemaField::required`] and [`SchemaField::optional`] to build the
420    /// field list.
421    ///
422    /// # Example
423    /// ```
424    /// use aam_core::builder::{AAMBuilder, SchemaField};
425    ///
426    /// let mut b = AAMBuilder::new();
427    /// b.schema("Point", [
428    ///     SchemaField::required("x", "f64"),
429    ///     SchemaField::required("y", "f64"),
430    ///     SchemaField::optional("z", "f64"),
431    /// ]);
432    /// assert!(b.build().contains("@schema Point {"));
433    /// ```
434    pub fn schema(
435        &mut self,
436        name: &str,
437        fields: impl IntoIterator<Item = SchemaField>,
438    ) -> &mut Self {
439        self.push_sep();
440        self.buffer.push_str("@schema ");
441        self.buffer.push_str(name);
442        self.buffer.push_str(" { ");
443
444        let mut first = true;
445        for field in fields {
446            if !first {
447                self.buffer.push_str(", ");
448            }
449            field.to_aaml_writer(&mut self.buffer).unwrap();
450            first = false;
451        }
452
453        self.buffer.push_str(" }");
454        self
455    }
456
457    /// Appends a `@schema Name { ... }` directive using a multiline block format.
458    ///
459    /// Each field is placed on its own indented line, which is more readable for
460    /// schemas with many fields.
461    ///
462    /// # Example
463    /// ```
464    /// use aam_core::builder::{AAMBuilder, SchemaField};
465    ///
466    /// let mut b = AAMBuilder::new();
467    /// b.schema_multiline("Server", [
468    ///     SchemaField::required("host", "string"),
469    ///     SchemaField::optional("port", "i32"),
470    /// ]);
471    /// let out = b.build();
472    /// assert!(out.contains("    host: string"));
473    /// ```
474    pub fn schema_multiline(
475        &mut self,
476        name: &str,
477        fields: impl IntoIterator<Item = SchemaField>,
478    ) -> &mut Self {
479        self.push_sep();
480        write!(&mut self.buffer, "@schema {name} {{").unwrap();
481        for field in fields {
482            write!(&mut self.buffer, "\n    ").unwrap();
483            field.to_aaml_writer(&mut self.buffer).unwrap();
484        }
485        self.buffer.push_str("\n}");
486        self
487    }
488
489    /// Appends a `@derive path` or `@derive path::Schema1::Schema2` directive.
490    ///
491    /// Pass `schemas` as an empty iterator (e.g. `[]`) to derive the entire file.
492    ///
493    /// # Example
494    /// ```
495    /// use aam_core::builder::AAMBuilder;
496    ///
497    /// let mut b = AAMBuilder::new();
498    /// b.derive("base.aam", ["Server", "Database"]);
499    /// assert!(b.build().contains("@derive base.aam::Server::Database"));
500    ///
501    /// let mut b2 = AAMBuilder::new();
502    /// b2.derive("base.aam", [] as [&str; 0]);
503    /// assert!(b2.build().contains("@derive base.aam"));
504    /// ```
505    pub fn derive(
506        &mut self,
507        path: &str,
508        schemas: impl IntoIterator<Item = impl AsRef<str>>,
509    ) -> &mut Self {
510        self.push_sep();
511        self.buffer.push_str("@derive ");
512        self.buffer.push_str(path);
513        for schema in schemas {
514            self.buffer.push_str("::");
515            self.buffer.push_str(schema.as_ref());
516        }
517        self
518    }
519
520    /// Appends a `@import path` directive.
521    ///
522    /// # Example
523    /// ```
524    /// use aam_core::builder::AAMBuilder;
525    ///
526    /// let mut b = AAMBuilder::new();
527    /// b.import("shared.aam");
528    /// assert!(b.build().contains("@import shared.aam"));
529    /// ```
530    pub fn import(&mut self, path: &str) -> &mut Self {
531        self.push_sep();
532        self.buffer.push_str("@import ");
533        self.buffer.push_str(path);
534        self
535    }
536
537    /// Appends a `@type alias = type_name` directive.
538    ///
539    /// # Example
540    /// ```
541    /// use aam_core::builder::AAMBuilder;
542    ///
543    /// let mut b = AAMBuilder::new();
544    /// b.type_alias("pos", "math::vector3");
545    /// assert!(b.build().contains("@type pos = math::vector3"));
546    /// ```
547    pub fn type_alias(&mut self, alias: &str, type_name: &str) -> &mut Self {
548        self.push_sep();
549        self.buffer.push_str("@type ");
550        self.buffer.push_str(alias);
551        self.buffer.push_str(" = ");
552        self.buffer.push_str(type_name);
553        self
554    }
555
556    /// Appends a `@type alias = builtin_type` directive using a [`BuiltInType`] enum.
557    ///
558    /// Only available with the `builder-extras` feature (enabled by default).
559    ///
560    /// # Example
561    /// ```
562    /// use aam_core::builder::{AAMBuilder, BuiltInType};
563    ///
564    /// let mut b = AAMBuilder::new();
565    /// b.type_alias_builtin("pos", &BuiltInType::Vector3);
566    /// assert!(b.build().contains("@type pos = math::vector3"));
567    /// ```
568    #[cfg(feature = "builder-extras")]
569    pub fn type_alias_builtin(&mut self, alias: &str, builtin: &BuiltInType) -> &mut Self {
570        self.type_alias(alias, &builtin.to_string())
571    }
572
573    /// Appends a `key = value` line where the value is an inline object.
574    ///
575    /// Only available with the `builder-extras` feature (enabled by default).
576    ///
577    /// # Example
578    /// ```
579    /// use aam_core::builder::{AAMBuilder, InlineObject};
580    ///
581    /// let mut b = AAMBuilder::new();
582    /// let obj = InlineObject::new()
583    ///     .with_field("system", "cmake")
584    ///     .with_field("command", "cmake");
585    /// b.add_inline("build", &obj);
586    /// assert!(b.build().contains("build = { system = cmake, command = cmake }"));
587    /// ```
588    #[cfg(feature = "builder-extras")]
589    pub fn add_inline(&mut self, key: &str, obj: &InlineObject) -> &mut Self {
590        self.add_line(key, &obj.to_string())
591    }
592
593    /// Appends a `key = value` line where the value is parsed from an inline object string.
594    ///
595    /// Only available with the `builder-extras` feature (enabled by default).
596    ///
597    /// # Example
598    /// ```
599    /// use aam_core::builder::AAMBuilder;
600    ///
601    /// let mut b = AAMBuilder::new();
602    /// b.add_inline_str("build", r#"{ system = cmake, command = cmake }"#);
603    /// assert!(b.build().contains("build = { system = cmake, command = cmake }"));
604    /// ```
605    #[cfg(feature = "builder-extras")]
606    pub fn add_inline_str(&mut self, key: &str, inline_str: &str) -> &mut Self {
607        self.add_line(key, inline_str)
608    }
609
610    /// Appends a raw line as-is (e.g. a directive not covered by the typed API).
611    ///
612    /// A newline separator is inserted automatically between entries.
613    ///
614    /// > **Note:** Prefer the typed directive methods ([`schema`](Self::schema),
615    /// > [`derive`](Self::derive), [`import`](Self::import),
616    /// > [`type_alias`](Self::type_alias)) over this method when possible.
617    ///
618    /// Returns `&mut self` for chaining.
619    #[deprecated(
620        since = "1.1.0",
621        note = "Prefer the typed directive methods (schema, derive, import, type_alias) over this method when possible."
622    )]
623    pub fn add_raw(&mut self, raw_line: &str) -> &mut Self {
624        self.push_sep();
625        self.buffer.push_str(raw_line);
626        self
627    }
628
629    // ── Output ────────────────────────────────────────────────────────────────
630
631    /// Writes the accumulated content to the file at `path`.
632    ///
633    /// The file is created or truncated.
634    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
635        std::fs::write(path, self.buffer.as_bytes())
636    }
637
638    /// Consumes the builder and returns the accumulated content as a `String`.
639    #[must_use]
640    pub fn build(self) -> String {
641        self.buffer
642    }
643
644    /// Returns a clone of the accumulated content as a `String`.
645    #[must_use]
646    pub fn as_string(&self) -> String {
647        self.buffer.clone()
648    }
649}
650
651impl Deref for AAMBuilder {
652    type Target = str;
653
654    fn deref(&self) -> &Self::Target {
655        &self.buffer
656    }
657}
658
659impl Default for AAMBuilder {
660    fn default() -> Self {
661        Self::new()
662    }
663}
664
665impl Display for AAMBuilder {
666    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
667        write!(f, "{}", self.buffer)
668    }
669}