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