Skip to main content

eure_document/write/
record.rs

1//! RecordWriter for writing record types to Eure documents.
2
3extern crate alloc;
4
5use alloc::string::ToString;
6
7use crate::document::constructor::DocumentConstructor;
8use crate::path::PathSegment;
9use crate::value::ObjectKey;
10
11use super::{IntoEure, WriteError};
12
13/// Helper for writing record (map with string keys) to Eure documents.
14///
15/// Used within the closure passed to [`DocumentConstructor::record`].
16///
17/// # Example
18///
19/// ```ignore
20/// c.record(|rec| {
21///     rec.field("name", "Alice")?;
22///     rec.field_optional("age", Some(30))?;
23///     Ok(())
24/// })?;
25/// ```
26pub struct RecordWriter<'a> {
27    constructor: &'a mut DocumentConstructor,
28}
29
30impl<'a> RecordWriter<'a> {
31    /// Create a new RecordWriter.
32    pub(crate) fn new(constructor: &'a mut DocumentConstructor) -> Self {
33        Self { constructor }
34    }
35
36    /// Write a required field.
37    ///
38    /// # Example
39    ///
40    /// ```ignore
41    /// rec.field("name", "Alice")?;
42    /// ```
43    pub fn field<T: IntoEure>(&mut self, name: &str, value: T) -> Result<(), WriteError> {
44        let scope = self.constructor.begin_scope();
45        self.constructor
46            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))?;
47        T::write(value, self.constructor)?;
48        self.constructor.end_scope(scope)?;
49        Ok(())
50    }
51
52    /// Write a required field using a marker type.
53    ///
54    /// This enables writing types from external crates that can't implement
55    /// `IntoEure` directly due to Rust's orphan rule.
56    ///
57    /// # Example
58    ///
59    /// ```ignore
60    /// // DurationDef implements IntoEure<std::time::Duration>
61    /// rec.field_via::<DurationDef, _>("timeout", duration)?;
62    /// ```
63    pub fn field_via<M, T>(&mut self, name: &str, value: T) -> Result<(), WriteError>
64    where
65        M: IntoEure<T>,
66    {
67        let scope = self.constructor.begin_scope();
68        self.constructor
69            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))?;
70        M::write(value, self.constructor)?;
71        self.constructor.end_scope(scope)?;
72        Ok(())
73    }
74
75    /// Write an optional field.
76    /// Does nothing if the value is `None`.
77    ///
78    /// # Example
79    ///
80    /// ```ignore
81    /// rec.field_optional("age", self.age)?;
82    /// ```
83    pub fn field_optional<T: IntoEure>(
84        &mut self,
85        name: &str,
86        value: Option<T>,
87    ) -> Result<(), WriteError> {
88        if let Some(v) = value {
89            self.field(name, v)?;
90        }
91        Ok(())
92    }
93
94    /// Write a field using a custom writer closure.
95    ///
96    /// Useful for nested structures that need custom handling.
97    ///
98    /// # Example
99    ///
100    /// ```ignore
101    /// rec.field_with("address", |c| {
102    ///     c.record(|rec| {
103    ///         rec.field("city", "Tokyo")?;
104    ///         Ok(())
105    ///     })
106    /// })?;
107    /// ```
108    pub fn field_with<F, T>(&mut self, name: &str, f: F) -> Result<T, WriteError>
109    where
110        F: FnOnce(&mut DocumentConstructor) -> Result<T, WriteError>,
111    {
112        let scope = self.constructor.begin_scope();
113        self.constructor
114            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))?;
115        let result = f(self.constructor)?;
116        self.constructor.end_scope(scope)?;
117        Ok(result)
118    }
119
120    /// Write an optional field using a custom writer closure.
121    /// Does nothing if the value is `None`.
122    ///
123    /// # Example
124    ///
125    /// ```ignore
126    /// rec.field_with_optional("metadata", self.metadata.as_ref(), |c, meta| {
127    ///     c.write(meta)
128    /// })?;
129    /// ```
130    pub fn field_with_optional<T, F, R>(
131        &mut self,
132        name: &str,
133        value: Option<T>,
134        f: F,
135    ) -> Result<Option<R>, WriteError>
136    where
137        F: FnOnce(&mut DocumentConstructor, T) -> Result<R, WriteError>,
138    {
139        if let Some(v) = value {
140            let result = self.field_with(name, |c| f(c, v))?;
141            Ok(Some(result))
142        } else {
143            Ok(None)
144        }
145    }
146
147    /// Get a mutable reference to the underlying DocumentConstructor.
148    ///
149    /// Useful for advanced use cases that need direct access.
150    pub fn constructor(&mut self) -> &mut DocumentConstructor {
151        self.constructor
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158    use crate::document::node::NodeValue;
159    use crate::text::Text;
160    use crate::value::PrimitiveValue;
161
162    #[test]
163    fn test_field() {
164        let mut c = DocumentConstructor::new();
165        c.record(|rec| {
166            rec.field("name", "Alice")?;
167            Ok(())
168        })
169        .unwrap();
170        let doc = c.finish();
171        let map = doc.root().as_map().unwrap();
172        let name_id = map.get(&ObjectKey::String("name".to_string())).unwrap();
173        let node = doc.node(*name_id);
174        assert_eq!(
175            node.content,
176            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice")))
177        );
178    }
179
180    #[test]
181    fn test_field_optional_some() {
182        let mut c = DocumentConstructor::new();
183        c.record(|rec| {
184            rec.field_optional("age", Some(30i32))?;
185            Ok(())
186        })
187        .unwrap();
188        let doc = c.finish();
189        let map = doc.root().as_map().unwrap();
190        assert!(map.get(&ObjectKey::String("age".to_string())).is_some());
191    }
192
193    #[test]
194    fn test_field_optional_none() {
195        let mut c = DocumentConstructor::new();
196        c.record(|rec| {
197            rec.field_optional::<i32>("age", None)?;
198            Ok(())
199        })
200        .unwrap();
201        let doc = c.finish();
202        let map = doc.root().as_map().unwrap();
203        assert!(map.get(&ObjectKey::String("age".to_string())).is_none());
204    }
205
206    #[test]
207    fn test_field_with() {
208        let mut c = DocumentConstructor::new();
209        c.record(|rec| {
210            rec.field_with("nested", |c| {
211                c.record(|rec| {
212                    rec.field("inner", "value")?;
213                    Ok(())
214                })
215            })?;
216            Ok(())
217        })
218        .unwrap();
219        let doc = c.finish();
220        let map = doc.root().as_map().unwrap();
221        let nested_id = map.get(&ObjectKey::String("nested".to_string())).unwrap();
222        let nested = doc.node(*nested_id).as_map().unwrap();
223        assert!(
224            nested
225                .get(&ObjectKey::String("inner".to_string()))
226                .is_some()
227        );
228    }
229
230    #[test]
231    fn test_multiple_fields() {
232        let mut c = DocumentConstructor::new();
233        c.record(|rec| {
234            rec.field("name", "Bob")?;
235            rec.field("age", 25i32)?;
236            rec.field("active", true)?;
237            Ok(())
238        })
239        .unwrap();
240        let doc = c.finish();
241        let map = doc.root().as_map().unwrap();
242        assert_eq!(map.len(), 3);
243    }
244}