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/// When `ext_mode` is `true`, field writes are redirected to extension writes.
18/// This is used by `flatten_ext` to write fields as extensions.
19///
20/// # Example
21///
22/// ```ignore
23/// c.record(|rec| {
24///     rec.field("name", "Alice")?;
25///     rec.field_optional("age", Some(30))?;
26///     Ok(())
27/// })?;
28/// ```
29pub struct RecordWriter<'a> {
30    constructor: &'a mut DocumentConstructor,
31    ext_mode: bool,
32}
33
34impl<'a> RecordWriter<'a> {
35    /// Create a new RecordWriter with `ext_mode` disabled.
36    pub(crate) fn new(constructor: &'a mut DocumentConstructor) -> Self {
37        Self {
38            constructor,
39            ext_mode: false,
40        }
41    }
42
43    /// Create a new RecordWriter with the specified `ext_mode`.
44    ///
45    /// This is primarily used by generated derive code that needs to write
46    /// flattened extension fields onto an existing node.
47    pub fn new_with_ext_mode(constructor: &'a mut DocumentConstructor, ext_mode: bool) -> Self {
48        Self {
49            constructor,
50            ext_mode,
51        }
52    }
53
54    /// Write a required field.
55    ///
56    /// In `ext_mode`, this redirects to an extension write.
57    ///
58    /// # Example
59    ///
60    /// ```ignore
61    /// rec.field("name", "Alice")?;
62    /// ```
63    pub fn field<T: IntoEure>(&mut self, name: &str, value: T) -> Result<(), T::Error> {
64        if self.ext_mode {
65            return self.constructor.set_extension(name, value);
66        }
67        let scope = self.constructor.begin_scope();
68        self.constructor
69            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))
70            .map_err(WriteError::from)?;
71        T::write(value, self.constructor)?;
72        self.constructor
73            .end_scope(scope)
74            .map_err(WriteError::from)?;
75        Ok(())
76    }
77
78    /// Write a required field using a marker type.
79    ///
80    /// This enables writing types from external crates that can't implement
81    /// `IntoEure` directly due to Rust's orphan rule.
82    ///
83    /// In `ext_mode`, this redirects to an extension write via the marker type.
84    ///
85    /// # Example
86    ///
87    /// ```ignore
88    /// // DurationDef implements IntoEure<std::time::Duration>
89    /// rec.field_via::<DurationDef, _>("timeout", duration)?;
90    /// ```
91    pub fn field_via<M, T>(&mut self, name: &str, value: T) -> Result<(), M::Error>
92    where
93        M: IntoEure<T>,
94    {
95        if self.ext_mode {
96            let ident: crate::identifier::Identifier = name
97                .parse()
98                .map_err(|_| WriteError::InvalidIdentifier(name.into()))?;
99            let scope = self.constructor.begin_scope();
100            self.constructor
101                .navigate(PathSegment::Extension(ident))
102                .map_err(WriteError::from)?;
103            M::write(value, self.constructor)?;
104            self.constructor
105                .end_scope(scope)
106                .map_err(WriteError::from)?;
107            return Ok(());
108        }
109        let scope = self.constructor.begin_scope();
110        self.constructor
111            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))
112            .map_err(WriteError::from)?;
113        M::write(value, self.constructor)?;
114        self.constructor
115            .end_scope(scope)
116            .map_err(WriteError::from)?;
117        Ok(())
118    }
119
120    /// Write an optional field.
121    /// Does nothing if the value is `None`.
122    ///
123    /// # Example
124    ///
125    /// ```ignore
126    /// rec.field_optional("age", self.age)?;
127    /// ```
128    pub fn field_optional<T: IntoEure>(
129        &mut self,
130        name: &str,
131        value: Option<T>,
132    ) -> Result<(), T::Error> {
133        if let Some(v) = value {
134            self.field(name, v)?;
135        }
136        Ok(())
137    }
138
139    /// Write a field using a custom writer closure.
140    ///
141    /// Useful for nested structures that need custom handling.
142    ///
143    /// # Example
144    ///
145    /// ```ignore
146    /// rec.field_with("address", |c| {
147    ///     c.record(|rec| {
148    ///         rec.field("city", "Tokyo")?;
149    ///         Ok(())
150    ///     })
151    /// })?;
152    /// ```
153    pub fn field_with<F, T>(&mut self, name: &str, f: F) -> Result<T, WriteError>
154    where
155        F: FnOnce(&mut DocumentConstructor) -> Result<T, WriteError>,
156    {
157        let scope = self.constructor.begin_scope();
158        self.constructor
159            .navigate(PathSegment::Value(ObjectKey::String(name.to_string())))
160            .map_err(WriteError::from)?;
161        let result = f(self.constructor)?;
162        self.constructor
163            .end_scope(scope)
164            .map_err(WriteError::from)?;
165        Ok(result)
166    }
167
168    /// Write an optional field using a custom writer closure.
169    /// Does nothing if the value is `None`.
170    ///
171    /// # Example
172    ///
173    /// ```ignore
174    /// rec.field_with_optional("metadata", self.metadata.as_ref(), |c, meta| {
175    ///     c.write(meta)
176    /// })?;
177    /// ```
178    pub fn field_with_optional<T, F, R>(
179        &mut self,
180        name: &str,
181        value: Option<T>,
182        f: F,
183    ) -> Result<Option<R>, WriteError>
184    where
185        F: FnOnce(&mut DocumentConstructor, T) -> Result<R, WriteError>,
186    {
187        if let Some(v) = value {
188            let result = self.field_with(name, |c| f(c, v))?;
189            Ok(Some(result))
190        } else {
191            Ok(None)
192        }
193    }
194
195    /// Flatten a value's fields into this record writer.
196    ///
197    /// The flattened type's fields are written as if they were direct fields
198    /// of this record. The current `ext_mode` is inherited.
199    ///
200    /// # Example
201    ///
202    /// ```ignore
203    /// rec.flatten(value.address)?;
204    /// ```
205    pub fn flatten<M, T>(&mut self, value: T) -> Result<(), M::Error>
206    where
207        M: IntoEure<T>,
208    {
209        M::write_flatten(value, self)
210    }
211
212    /// Flatten a value's fields as extensions into this record.
213    ///
214    /// Creates a temporary `RecordWriter` with `ext_mode: true`, so that
215    /// all field writes from the flattened type become extension writes.
216    ///
217    /// # Example
218    ///
219    /// ```ignore
220    /// rec.flatten_ext(value.ext)?;
221    /// ```
222    pub fn flatten_ext<M, T>(&mut self, value: T) -> Result<(), M::Error>
223    where
224        M: IntoEure<T>,
225    {
226        let mut ext_rec = RecordWriter::new_with_ext_mode(self.constructor, true);
227        M::write_flatten(value, &mut ext_rec)
228    }
229
230    /// Get a mutable reference to the underlying DocumentConstructor.
231    ///
232    /// Useful for advanced use cases that need direct access.
233    pub fn constructor(&mut self) -> &mut DocumentConstructor {
234        self.constructor
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    extern crate alloc;
241    use alloc::string::{String, ToString};
242
243    use super::*;
244    use crate::document::node::NodeValue;
245    use crate::text::Text;
246    use crate::value::PrimitiveValue;
247
248    #[test]
249    fn test_field() {
250        let mut c = DocumentConstructor::new();
251        c.record(|rec| {
252            rec.field("name", "Alice")?;
253            Ok::<(), WriteError>(())
254        })
255        .unwrap();
256        let doc = c.finish();
257        let map = doc.root().as_map().unwrap();
258        let name_id = map.get(&ObjectKey::String("name".to_string())).unwrap();
259        let node = doc.node(*name_id);
260        assert_eq!(
261            node.content,
262            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("Alice")))
263        );
264    }
265
266    #[test]
267    fn test_field_optional_some() {
268        let mut c = DocumentConstructor::new();
269        c.record(|rec| {
270            rec.field_optional("age", Some(30i32))?;
271            Ok::<(), WriteError>(())
272        })
273        .unwrap();
274        let doc = c.finish();
275        let map = doc.root().as_map().unwrap();
276        assert!(map.get(&ObjectKey::String("age".to_string())).is_some());
277    }
278
279    #[test]
280    fn test_field_optional_none() {
281        let mut c = DocumentConstructor::new();
282        c.record(|rec| {
283            rec.field_optional::<i32>("age", None)?;
284            Ok::<(), WriteError>(())
285        })
286        .unwrap();
287        let doc = c.finish();
288        let map = doc.root().as_map().unwrap();
289        assert!(map.get(&ObjectKey::String("age".to_string())).is_none());
290    }
291
292    #[test]
293    fn test_field_with() {
294        let mut c = DocumentConstructor::new();
295        c.record(|rec| {
296            rec.field_with("nested", |c| {
297                c.record(|rec| {
298                    rec.field("inner", "value")?;
299                    Ok::<(), WriteError>(())
300                })
301            })?;
302            Ok::<(), WriteError>(())
303        })
304        .unwrap();
305        let doc = c.finish();
306        let map = doc.root().as_map().unwrap();
307        let nested_id = map.get(&ObjectKey::String("nested".to_string())).unwrap();
308        let nested = doc.node(*nested_id).as_map().unwrap();
309        assert!(
310            nested
311                .get(&ObjectKey::String("inner".to_string()))
312                .is_some()
313        );
314    }
315
316    #[test]
317    fn test_multiple_fields() {
318        let mut c = DocumentConstructor::new();
319        c.record(|rec| {
320            rec.field("name", "Bob")?;
321            rec.field("age", 25i32)?;
322            rec.field("active", true)?;
323            Ok::<(), WriteError>(())
324        })
325        .unwrap();
326        let doc = c.finish();
327        let map = doc.root().as_map().unwrap();
328        assert_eq!(map.len(), 3);
329    }
330
331    // Manually implement IntoEure flatten support for testing
332    struct TestAddress {
333        city: String,
334        country: String,
335    }
336
337    impl IntoEure for TestAddress {
338        type Error = WriteError;
339
340        fn write(value: TestAddress, c: &mut DocumentConstructor) -> Result<(), Self::Error> {
341            c.record(|rec| {
342                rec.field("city", value.city)?;
343                rec.field("country", value.country)?;
344                Ok::<(), WriteError>(())
345            })
346        }
347
348        fn write_flatten(
349            value: TestAddress,
350            rec: &mut super::RecordWriter<'_>,
351        ) -> Result<(), Self::Error> {
352            rec.field("city", value.city)?;
353            rec.field("country", value.country)?;
354            Ok(())
355        }
356    }
357
358    #[test]
359    fn test_flatten() {
360        let mut c = DocumentConstructor::new();
361        c.record(|rec| {
362            rec.field("name", "Alice")?;
363            rec.flatten::<TestAddress, _>(TestAddress {
364                city: "Tokyo".to_string(),
365                country: "Japan".to_string(),
366            })?;
367            Ok::<(), WriteError>(())
368        })
369        .unwrap();
370        let doc = c.finish();
371        let map = doc.root().as_map().unwrap();
372        assert_eq!(map.len(), 3);
373        assert!(map.get(&ObjectKey::String("name".to_string())).is_some());
374        assert!(map.get(&ObjectKey::String("city".to_string())).is_some());
375        assert!(map.get(&ObjectKey::String("country".to_string())).is_some());
376    }
377
378    struct TestMeta {
379        version: i32,
380        deprecated: bool,
381    }
382
383    impl IntoEure for TestMeta {
384        type Error = WriteError;
385
386        fn write(value: TestMeta, c: &mut DocumentConstructor) -> Result<(), Self::Error> {
387            c.record(|rec| {
388                rec.field("version", value.version)?;
389                rec.field("deprecated", value.deprecated)?;
390                Ok::<(), WriteError>(())
391            })
392        }
393
394        fn write_flatten(
395            value: TestMeta,
396            rec: &mut super::RecordWriter<'_>,
397        ) -> Result<(), Self::Error> {
398            rec.field("version", value.version)?;
399            rec.field("deprecated", value.deprecated)?;
400            Ok(())
401        }
402    }
403
404    #[test]
405    fn test_flatten_ext() {
406        use crate::identifier::Identifier;
407
408        let mut c = DocumentConstructor::new();
409        c.record(|rec| {
410            rec.field("name", "test")?;
411            rec.flatten_ext::<TestMeta, _>(TestMeta {
412                version: 2,
413                deprecated: true,
414            })?;
415            Ok::<(), WriteError>(())
416        })
417        .unwrap();
418        let doc = c.finish();
419        let root = doc.root();
420        // "name" should be a record field
421        let map = root.as_map().unwrap();
422        assert_eq!(map.len(), 1);
423        assert!(map.get(&ObjectKey::String("name".to_string())).is_some());
424        // "version" and "deprecated" should be extensions
425        assert!(
426            root.extensions
427                .contains_key(&"version".parse::<Identifier>().unwrap())
428        );
429        assert!(
430            root.extensions
431                .contains_key(&"deprecated".parse::<Identifier>().unwrap())
432        );
433    }
434}