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}