Skip to main content

hwpforge_core/
document.rs

1//! The Document type with typestate pattern.
2//!
3//! [`Document<S>`] is the aggregate root of the Core DOM. It uses the
4//! **typestate pattern** to enforce document lifecycle at compile time:
5//!
6//! - [`Draft`] -- mutable, can add/remove sections
7//! - [`Validated`] -- immutable structure, safe for serialization/export
8//!
9//! The transition `Draft -> Validated` is one-way via [`Document::validate()`],
10//! which consumes the draft (move semantics prevent reuse).
11//!
12//! # Design Decisions
13//!
14//! - **Typestate, not enum** -- invalid operations are compile errors
15//!   (not runtime panics). See Appendix D in the detailed plan.
16//! - **Deserialize always to Draft** -- serialized data may be modified
17//!   externally; re-validation is mandatory.
18//! - **No `Styled` state in Phase 1** -- deferred to Phase 2 when
19//!   Blueprint (StyleRegistry) is available.
20//!
21//! # Examples
22//!
23//! ```
24//! use hwpforge_core::document::{Document, Draft, Validated};
25//! use hwpforge_core::section::Section;
26//! use hwpforge_core::paragraph::Paragraph;
27//! use hwpforge_core::run::Run;
28//! use hwpforge_core::PageSettings;
29//! use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
30//!
31//! let mut doc = Document::new();
32//! doc.add_section(Section::with_paragraphs(
33//!     vec![Paragraph::with_runs(
34//!         vec![Run::text("Hello", CharShapeIndex::new(0))],
35//!         ParaShapeIndex::new(0),
36//!     )],
37//!     PageSettings::a4(),
38//! ));
39//!
40//! let validated: Document<Validated> = doc.validate().unwrap();
41//! assert_eq!(validated.section_count(), 1);
42//! ```
43//!
44//! ```compile_fail
45//! // A validated document cannot add sections:
46//! use hwpforge_core::document::{Document, Validated};
47//! use hwpforge_core::section::Section;
48//! use hwpforge_core::PageSettings;
49//!
50//! # fn get_validated() -> Document<Validated> { todo!() }
51//! let mut validated = get_validated();
52//! validated.add_section(Section::new(PageSettings::a4()));
53//! // ERROR: no method named `add_section` found for `Document<Validated>`
54//! ```
55
56use std::marker::PhantomData;
57
58use schemars::JsonSchema;
59use serde::{Deserialize, Serialize};
60
61use crate::error::CoreResult;
62use crate::metadata::Metadata;
63use crate::section::Section;
64use crate::validate::validate_sections;
65
66/// Marker type: the document is a mutable draft.
67///
68/// A `Document<Draft>` can be modified (add sections, set metadata)
69/// and then validated via [`Document::validate()`].
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct Draft;
72
73/// Marker type: the document has passed structural validation.
74///
75/// A `Document<Validated>` is guaranteed to have:
76/// - At least 1 section
77/// - Every section has at least 1 paragraph
78/// - Every paragraph has at least 1 run
79/// - All table/control structural invariants hold
80///
81/// The only way to obtain a `Document<Validated>` is through
82/// [`Document<Draft>::validate()`].
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct Validated;
85
86/// The document aggregate root with compile-time state tracking.
87///
88/// The generic parameter `S` determines which operations are available:
89///
90/// | State | Mutable | Serializable | Exportable |
91/// |-------|---------|-------------|-----------|
92/// | [`Draft`] | Yes | Yes | No (must validate first) |
93/// | [`Validated`] | No | Yes | Yes |
94///
95/// # Typestate Safety
96///
97/// The `_state` field is private and zero-sized. There is no way to
98/// construct a `Document<Validated>` except through `validate()`.
99///
100/// # Examples
101///
102/// ```
103/// use hwpforge_core::document::Document;
104/// use hwpforge_core::Metadata;
105///
106/// let doc = Document::with_metadata(Metadata::new().with_title("Report"));
107/// assert!(doc.is_empty());
108/// ```
109pub struct Document<S = Draft> {
110    sections: Vec<Section>,
111    metadata: Metadata,
112    _state: PhantomData<S>,
113}
114
115// ---------------------------------------------------------------------------
116// Shared methods (any state)
117// ---------------------------------------------------------------------------
118
119impl<S> Document<S> {
120    /// Returns a slice of all sections.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use hwpforge_core::document::Document;
126    ///
127    /// let doc = Document::new();
128    /// assert!(doc.sections().is_empty());
129    /// ```
130    pub fn sections(&self) -> &[Section] {
131        &self.sections
132    }
133
134    /// Returns a reference to the document metadata.
135    pub fn metadata(&self) -> &Metadata {
136        &self.metadata
137    }
138
139    /// Returns the number of sections.
140    pub fn section_count(&self) -> usize {
141        self.sections.len()
142    }
143
144    /// Returns `true` if the document has no sections.
145    pub fn is_empty(&self) -> bool {
146        self.sections.is_empty()
147    }
148}
149
150// ---------------------------------------------------------------------------
151// Draft-only methods
152// ---------------------------------------------------------------------------
153
154impl Document<Draft> {
155    /// Creates a new empty draft document with default metadata.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use hwpforge_core::document::Document;
161    ///
162    /// let doc = Document::new();
163    /// assert!(doc.is_empty());
164    /// ```
165    pub fn new() -> Self {
166        Self { sections: Vec::new(), metadata: Metadata::default(), _state: PhantomData }
167    }
168
169    /// Creates a new draft document with the given metadata.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use hwpforge_core::document::Document;
175    /// use hwpforge_core::Metadata;
176    ///
177    /// let doc = Document::with_metadata(Metadata::new().with_title("Test"));
178    /// assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
179    /// ```
180    pub fn with_metadata(metadata: Metadata) -> Self {
181        Self { sections: Vec::new(), metadata, _state: PhantomData }
182    }
183
184    /// Appends a section to the draft document.
185    ///
186    /// # Examples
187    ///
188    /// ```
189    /// use hwpforge_core::document::Document;
190    /// use hwpforge_core::section::Section;
191    /// use hwpforge_core::PageSettings;
192    ///
193    /// let mut doc = Document::new();
194    /// doc.add_section(Section::new(PageSettings::a4()));
195    /// assert_eq!(doc.section_count(), 1);
196    /// ```
197    pub fn add_section(&mut self, section: Section) {
198        self.sections.push(section);
199    }
200
201    /// Sets the document metadata.
202    pub fn set_metadata(&mut self, metadata: Metadata) {
203        self.metadata = metadata;
204    }
205
206    /// Returns a mutable reference to the metadata.
207    pub fn metadata_mut(&mut self) -> &mut Metadata {
208        &mut self.metadata
209    }
210
211    /// Returns a mutable slice of sections.
212    pub fn sections_mut(&mut self) -> &mut [Section] {
213        &mut self.sections
214    }
215
216    /// Validates the document structure and transitions to `Validated`.
217    ///
218    /// Consumes `self` (move semantics). On success, returns a
219    /// `Document<Validated>`. On failure, returns a `CoreError`.
220    ///
221    /// # Errors
222    ///
223    /// Returns [`CoreError::Validation`](crate::error::CoreError::Validation) if the document violates any
224    /// structural invariant (empty sections, empty paragraphs, etc.).
225    ///
226    /// # Examples
227    ///
228    /// ```
229    /// use hwpforge_core::document::Document;
230    /// use hwpforge_core::section::Section;
231    /// use hwpforge_core::paragraph::Paragraph;
232    /// use hwpforge_core::run::Run;
233    /// use hwpforge_core::PageSettings;
234    /// use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
235    ///
236    /// let mut doc = Document::new();
237    /// doc.add_section(Section::with_paragraphs(
238    ///     vec![Paragraph::with_runs(
239    ///         vec![Run::text("Hello", CharShapeIndex::new(0))],
240    ///         ParaShapeIndex::new(0),
241    ///     )],
242    ///     PageSettings::a4(),
243    /// ));
244    ///
245    /// let validated = doc.validate().unwrap();
246    /// assert_eq!(validated.section_count(), 1);
247    /// ```
248    ///
249    /// ```
250    /// use hwpforge_core::document::Document;
251    ///
252    /// let doc = Document::new(); // empty
253    /// assert!(doc.validate().is_err());
254    /// ```
255    pub fn validate(self) -> CoreResult<Document<Validated>> {
256        validate_sections(&self.sections)?;
257        Ok(Document { sections: self.sections, metadata: self.metadata, _state: PhantomData })
258    }
259}
260
261impl Default for Document<Draft> {
262    fn default() -> Self {
263        Self::new()
264    }
265}
266
267// ---------------------------------------------------------------------------
268// Manual trait impls (avoid T: Trait bounds on phantom type S)
269// ---------------------------------------------------------------------------
270
271impl<S> std::fmt::Debug for Document<S> {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.debug_struct("Document")
274            .field("sections", &self.sections)
275            .field("metadata", &self.metadata)
276            .finish()
277    }
278}
279
280impl<S> Clone for Document<S> {
281    fn clone(&self) -> Self {
282        Self {
283            sections: self.sections.clone(),
284            metadata: self.metadata.clone(),
285            _state: PhantomData,
286        }
287    }
288}
289
290impl<S> PartialEq for Document<S> {
291    fn eq(&self, other: &Self) -> bool {
292        self.sections == other.sections && self.metadata == other.metadata
293    }
294}
295
296impl<S> Eq for Document<S> {}
297
298impl<S> std::fmt::Display for Document<S> {
299    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300        write!(f, "Document({} sections)", self.sections.len())
301    }
302}
303
304// ---------------------------------------------------------------------------
305// Serde: serialize any state, deserialize only to Draft
306// ---------------------------------------------------------------------------
307
308impl<S> Serialize for Document<S> {
309    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
310        use serde::ser::SerializeStruct;
311        let mut state = serializer.serialize_struct("Document", 2)?;
312        state.serialize_field("sections", &self.sections)?;
313        state.serialize_field("metadata", &self.metadata)?;
314        state.end()
315    }
316}
317
318impl<'de> Deserialize<'de> for Document<Draft> {
319    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
320        #[derive(Deserialize)]
321        struct DocumentData {
322            sections: Vec<Section>,
323            metadata: Metadata,
324        }
325
326        let data = DocumentData::deserialize(deserializer)?;
327        Ok(Document { sections: data.sections, metadata: data.metadata, _state: PhantomData })
328    }
329}
330
331// ---------------------------------------------------------------------------
332// JsonSchema: hide PhantomData
333// ---------------------------------------------------------------------------
334
335impl<S> JsonSchema for Document<S> {
336    fn schema_name() -> std::borrow::Cow<'static, str> {
337        "Document".into()
338    }
339
340    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
341        schemars::json_schema!({
342            "type": "object",
343            "properties": {
344                "sections": gen.subschema_for::<Vec<Section>>(),
345                "metadata": gen.subschema_for::<crate::metadata::Metadata>(),
346            },
347            "required": ["sections", "metadata"]
348        })
349    }
350}
351
352// ---------------------------------------------------------------------------
353// Send + Sync verification
354// ---------------------------------------------------------------------------
355
356const _: () = {
357    #[allow(dead_code)]
358    fn assert_send<T: Send>() {}
359    #[allow(dead_code)]
360    fn assert_sync<T: Sync>() {}
361    #[allow(dead_code)]
362    fn verify() {
363        assert_send::<Document<Draft>>();
364        assert_sync::<Document<Draft>>();
365        assert_send::<Document<Validated>>();
366        assert_sync::<Document<Validated>>();
367    }
368};
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use crate::error::CoreError;
374    use crate::page::PageSettings;
375    use crate::paragraph::Paragraph;
376    use crate::run::Run;
377    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
378
379    fn valid_section() -> Section {
380        Section::with_paragraphs(
381            vec![Paragraph::with_runs(
382                vec![Run::text("Hello", CharShapeIndex::new(0))],
383                ParaShapeIndex::new(0),
384            )],
385            PageSettings::a4(),
386        )
387    }
388
389    // === Construction ===
390
391    #[test]
392    fn new_creates_empty_draft() {
393        let doc = Document::new();
394        assert!(doc.is_empty());
395        assert_eq!(doc.section_count(), 0);
396        assert!(doc.metadata().title.is_none());
397    }
398
399    #[test]
400    fn with_metadata() {
401        let meta = Metadata { title: Some("Test".to_string()), ..Metadata::default() };
402        let doc = Document::with_metadata(meta);
403        assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
404    }
405
406    #[test]
407    fn default_is_new() {
408        let a = Document::new();
409        let b = Document::default();
410        assert_eq!(a, b);
411    }
412
413    // === Draft mutations ===
414
415    #[test]
416    fn add_section() {
417        let mut doc = Document::new();
418        doc.add_section(valid_section());
419        assert_eq!(doc.section_count(), 1);
420        assert!(!doc.is_empty());
421    }
422
423    #[test]
424    fn add_multiple_sections() {
425        let mut doc = Document::new();
426        doc.add_section(valid_section());
427        doc.add_section(valid_section());
428        doc.add_section(valid_section());
429        assert_eq!(doc.section_count(), 3);
430    }
431
432    #[test]
433    fn set_metadata() {
434        let mut doc = Document::new();
435        doc.set_metadata(Metadata { title: Some("New".to_string()), ..Metadata::default() });
436        assert_eq!(doc.metadata().title.as_deref(), Some("New"));
437    }
438
439    #[test]
440    fn metadata_mut() {
441        let mut doc = Document::new();
442        doc.metadata_mut().title = Some("Mutated".to_string());
443        assert_eq!(doc.metadata().title.as_deref(), Some("Mutated"));
444    }
445
446    #[test]
447    fn sections_mut() {
448        let mut doc = Document::new();
449        doc.add_section(valid_section());
450        doc.add_section(valid_section());
451        assert_eq!(doc.sections_mut().len(), 2);
452    }
453
454    // === Validation (Draft -> Validated) ===
455
456    #[test]
457    fn validate_success() {
458        let mut doc = Document::new();
459        doc.add_section(valid_section());
460        let validated = doc.validate().unwrap();
461        assert_eq!(validated.section_count(), 1);
462    }
463
464    #[test]
465    fn validate_empty_document_fails() {
466        let doc = Document::new();
467        let err = doc.validate().unwrap_err();
468        assert!(matches!(err, CoreError::Validation(_)));
469    }
470
471    #[test]
472    fn validate_empty_section_fails() {
473        let mut doc = Document::new();
474        doc.add_section(Section::new(PageSettings::a4()));
475        assert!(doc.validate().is_err());
476    }
477
478    #[test]
479    fn validate_consumes_draft() {
480        let mut doc = Document::new();
481        doc.add_section(valid_section());
482        let _validated = doc.validate().unwrap();
483        // doc is moved -- attempting to use it would be a compile error
484    }
485
486    // === Validated state ===
487
488    #[test]
489    fn validated_has_read_methods() {
490        let mut doc = Document::new();
491        doc.add_section(valid_section());
492        let validated = doc.validate().unwrap();
493
494        assert_eq!(validated.section_count(), 1);
495        assert!(!validated.is_empty());
496        assert_eq!(validated.sections().len(), 1);
497        assert!(validated.metadata().title.is_none());
498    }
499
500    // === Display ===
501
502    #[test]
503    fn display_draft() {
504        let doc = Document::new();
505        assert_eq!(doc.to_string(), "Document(0 sections)");
506    }
507
508    #[test]
509    fn display_validated() {
510        let mut doc = Document::new();
511        doc.add_section(valid_section());
512        let validated = doc.validate().unwrap();
513        assert_eq!(validated.to_string(), "Document(1 sections)");
514    }
515
516    // === Equality ===
517
518    #[test]
519    fn equality_draft() {
520        let mut a = Document::new();
521        a.add_section(valid_section());
522        let mut b = Document::new();
523        b.add_section(valid_section());
524        assert_eq!(a, b);
525    }
526
527    #[test]
528    fn equality_validated() {
529        let mut a = Document::new();
530        a.add_section(valid_section());
531        let mut b = Document::new();
532        b.add_section(valid_section());
533        let va = a.validate().unwrap();
534        let vb = b.validate().unwrap();
535        assert_eq!(va, vb);
536    }
537
538    // === Clone ===
539
540    #[test]
541    fn clone_draft() {
542        let mut doc = Document::new();
543        doc.add_section(valid_section());
544        let cloned = doc.clone();
545        assert_eq!(doc, cloned);
546    }
547
548    #[test]
549    fn clone_validated() {
550        let mut doc = Document::new();
551        doc.add_section(valid_section());
552        let validated = doc.validate().unwrap();
553        let cloned = validated.clone();
554        assert_eq!(validated, cloned);
555    }
556
557    // === Serde ===
558
559    #[test]
560    fn serde_roundtrip_draft() {
561        let mut doc = Document::new();
562        doc.add_section(valid_section());
563        doc.set_metadata(Metadata { title: Some("Test".to_string()), ..Metadata::default() });
564
565        let json = serde_json::to_string(&doc).unwrap();
566        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
567        assert_eq!(doc, back);
568    }
569
570    #[test]
571    fn serde_roundtrip_validated_deserializes_to_draft() {
572        let mut doc = Document::new();
573        doc.add_section(valid_section());
574        let validated = doc.validate().unwrap();
575
576        let json = serde_json::to_string(&validated).unwrap();
577        // Deserialize always produces Draft
578        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
579        // Must re-validate
580        let re_validated = back.validate().unwrap();
581        assert_eq!(validated, re_validated);
582    }
583
584    #[test]
585    fn serde_empty_document() {
586        let doc = Document::new();
587        let json = serde_json::to_string(&doc).unwrap();
588        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
589        assert_eq!(doc, back);
590    }
591
592    // === Complex document ===
593
594    #[test]
595    fn complex_document_roundtrip() {
596        use crate::control::Control;
597        use crate::image::{Image, ImageFormat};
598        use crate::table::{Table, TableCell, TableRow};
599        use hwpforge_foundation::HwpUnit;
600
601        let cell = TableCell::new(
602            vec![Paragraph::with_runs(
603                vec![Run::text("cell", CharShapeIndex::new(0))],
604                ParaShapeIndex::new(0),
605            )],
606            HwpUnit::from_mm(50.0).unwrap(),
607        );
608        let table = Table::new(vec![TableRow::new(vec![cell])]);
609
610        let link = Control::Hyperlink {
611            text: "click".to_string(),
612            url: "https://example.com".to_string(),
613        };
614
615        let img = Image::new(
616            "test.png",
617            HwpUnit::from_mm(10.0).unwrap(),
618            HwpUnit::from_mm(10.0).unwrap(),
619            ImageFormat::Png,
620        );
621
622        let section = Section::with_paragraphs(
623            vec![
624                Paragraph::with_runs(
625                    vec![
626                        Run::text("Hello ", CharShapeIndex::new(0)),
627                        Run::text("world", CharShapeIndex::new(1)),
628                    ],
629                    ParaShapeIndex::new(0),
630                ),
631                Paragraph::with_runs(
632                    vec![Run::table(table, CharShapeIndex::new(0))],
633                    ParaShapeIndex::new(1),
634                ),
635                Paragraph::with_runs(
636                    vec![Run::control(link, CharShapeIndex::new(0))],
637                    ParaShapeIndex::new(0),
638                ),
639                Paragraph::with_runs(
640                    vec![Run::image(img, CharShapeIndex::new(0))],
641                    ParaShapeIndex::new(0),
642                ),
643            ],
644            PageSettings::a4(),
645        );
646
647        let mut doc = Document::with_metadata(Metadata {
648            title: Some("Complex Doc".to_string()),
649            author: Some("Author".to_string()),
650            keywords: vec!["test".to_string()],
651            ..Metadata::default()
652        });
653        doc.add_section(section);
654
655        let validated = doc.validate().unwrap();
656        let json = serde_json::to_string_pretty(&validated).unwrap();
657        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
658        let re_validated = back.validate().unwrap();
659        assert_eq!(validated, re_validated);
660    }
661
662    // === Debug ===
663
664    #[test]
665    fn debug_output() {
666        let doc = Document::new();
667        let s = format!("{doc:?}");
668        assert!(s.contains("Document"), "debug: {s}");
669        assert!(s.contains("sections"), "debug: {s}");
670    }
671}