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::paragraph::Paragraph;
64use crate::section::Section;
65use crate::validate::validate_sections;
66
67/// Marker type: the document is a mutable draft.
68///
69/// A `Document<Draft>` can be modified (add sections, set metadata)
70/// and then validated via [`Document::validate()`].
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Draft;
73
74/// Marker type: the document has passed structural validation.
75///
76/// A `Document<Validated>` is guaranteed to have:
77/// - At least 1 section
78/// - Every section has at least 1 paragraph
79/// - Every paragraph has at least 1 run
80/// - All table/control structural invariants hold
81///
82/// The only way to obtain a `Document<Validated>` is through
83/// [`Document<Draft>::validate()`].
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Validated;
86
87/// The document aggregate root with compile-time state tracking.
88///
89/// The generic parameter `S` determines which operations are available:
90///
91/// | State | Mutable | Serializable | Exportable |
92/// |-------|---------|-------------|-----------|
93/// | [`Draft`] | Yes | Yes | No (must validate first) |
94/// | [`Validated`] | No | Yes | Yes |
95///
96/// # Typestate Safety
97///
98/// The `_state` field is private and zero-sized. There is no way to
99/// construct a `Document<Validated>` except through `validate()`.
100///
101/// # Examples
102///
103/// ```
104/// use hwpforge_core::document::Document;
105/// use hwpforge_core::Metadata;
106///
107/// let doc = Document::with_metadata(Metadata::new().with_title("Report"));
108/// assert!(doc.is_empty());
109/// ```
110pub struct Document<S = Draft> {
111    sections: Vec<Section>,
112    metadata: Metadata,
113    _state: PhantomData<S>,
114}
115
116// ---------------------------------------------------------------------------
117// Shared methods (any state)
118// ---------------------------------------------------------------------------
119
120impl<S> Document<S> {
121    /// Returns a slice of all sections.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use hwpforge_core::document::Document;
127    ///
128    /// let doc = Document::new();
129    /// assert!(doc.sections().is_empty());
130    /// ```
131    pub fn sections(&self) -> &[Section] {
132        &self.sections
133    }
134
135    /// Returns a reference to the document metadata.
136    pub fn metadata(&self) -> &Metadata {
137        &self.metadata
138    }
139
140    /// Returns the number of sections.
141    pub fn section_count(&self) -> usize {
142        self.sections.len()
143    }
144
145    /// Returns `true` if the document has no sections.
146    pub fn is_empty(&self) -> bool {
147        self.sections.is_empty()
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Draft-only methods
153// ---------------------------------------------------------------------------
154
155impl Document<Draft> {
156    /// Creates a new empty draft document with default metadata.
157    ///
158    /// # Examples
159    ///
160    /// ```
161    /// use hwpforge_core::document::Document;
162    ///
163    /// let doc = Document::new();
164    /// assert!(doc.is_empty());
165    /// ```
166    pub fn new() -> Self {
167        Self { sections: Vec::new(), metadata: Metadata::default(), _state: PhantomData }
168    }
169
170    /// Creates a new draft document with the given metadata.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use hwpforge_core::document::Document;
176    /// use hwpforge_core::Metadata;
177    ///
178    /// let doc = Document::with_metadata(Metadata::new().with_title("Test"));
179    /// assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
180    /// ```
181    pub fn with_metadata(metadata: Metadata) -> Self {
182        Self { sections: Vec::new(), metadata, _state: PhantomData }
183    }
184
185    /// 문서의 모든 문단(전 섹션 본문·머리말·꼬리말·바탕쪽 + 표 셀·캡션·
186    /// 글상자·각주/미주·메모 등 중첩 문단 전부)을 문서 순서로 방문한다.
187    ///
188    /// 캐시 정규화([`Self::strip_layout_caches`]) 등 전 문단 일괄 변환의
189    /// 진입점이다. `Draft` 전용 — `Validated` 는 typestate 상 불변이므로
190    /// 비교가 필요하면 검증 전 사본에서 수행한다.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use hwpforge_core::document::Document;
196    /// use hwpforge_core::page::PageSettings;
197    /// use hwpforge_core::paragraph::Paragraph;
198    /// use hwpforge_core::section::Section;
199    /// use hwpforge_foundation::ParaShapeIndex;
200    ///
201    /// let mut doc = Document::new();
202    /// doc.add_section(Section::with_paragraphs(
203    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
204    ///     PageSettings::a4(),
205    /// ));
206    /// let mut count = 0;
207    /// doc.for_each_paragraph_mut(|_| count += 1);
208    /// assert_eq!(count, 1);
209    /// ```
210    pub fn for_each_paragraph_mut<F: FnMut(&mut Paragraph)>(&mut self, mut f: F) {
211        for section in &mut self.sections {
212            section.walk_paragraphs_mut(&mut f);
213        }
214    }
215
216    /// 모든 문단의 줄 조판 캐시([`Paragraph::layout_cache`])를 제거한다.
217    ///
218    /// 문서 동등성 비교(admission/golden)의 정규화 단계: 네이티브 입력은
219    /// 캐시를 보유하고 우리 재인코드 산출물은 미보유가 정상이므로, 비교
220    /// 전 양쪽 사본에서 이 함수를 호출해 캐시 차이를 제거한다.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use hwpforge_core::document::Document;
226    /// use hwpforge_core::layout::LayoutCache;
227    /// use hwpforge_core::page::PageSettings;
228    /// use hwpforge_core::paragraph::Paragraph;
229    /// use hwpforge_core::section::Section;
230    /// use hwpforge_foundation::ParaShapeIndex;
231    ///
232    /// let mut para = Paragraph::new(ParaShapeIndex::new(0));
233    /// para.layout_cache = Some(LayoutCache::default());
234    /// let mut doc = Document::new();
235    /// doc.add_section(Section::with_paragraphs(vec![para], PageSettings::a4()));
236    /// doc.strip_layout_caches();
237    /// assert!(doc.sections()[0].paragraphs[0].layout_cache.is_none());
238    /// ```
239    pub fn strip_layout_caches(&mut self) {
240        self.for_each_paragraph_mut(|p| p.layout_cache = None);
241    }
242
243    /// Appends a section to the draft document.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use hwpforge_core::document::Document;
249    /// use hwpforge_core::section::Section;
250    /// use hwpforge_core::PageSettings;
251    ///
252    /// let mut doc = Document::new();
253    /// doc.add_section(Section::new(PageSettings::a4()));
254    /// assert_eq!(doc.section_count(), 1);
255    /// ```
256    pub fn add_section(&mut self, section: Section) {
257        self.sections.push(section);
258    }
259
260    /// Sets the document metadata.
261    pub fn set_metadata(&mut self, metadata: Metadata) {
262        self.metadata = metadata;
263    }
264
265    /// Returns a mutable reference to the metadata.
266    pub fn metadata_mut(&mut self) -> &mut Metadata {
267        &mut self.metadata
268    }
269
270    /// Returns a mutable slice of sections.
271    pub fn sections_mut(&mut self) -> &mut [Section] {
272        &mut self.sections
273    }
274
275    /// Validates the document structure and transitions to `Validated`.
276    ///
277    /// Consumes `self` (move semantics). On success, returns a
278    /// `Document<Validated>`. On failure, returns a `CoreError`.
279    ///
280    /// # Errors
281    ///
282    /// Returns [`CoreError::Validation`](crate::error::CoreError::Validation) if the document violates any
283    /// structural invariant (empty sections, empty paragraphs, etc.).
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use hwpforge_core::document::Document;
289    /// use hwpforge_core::section::Section;
290    /// use hwpforge_core::paragraph::Paragraph;
291    /// use hwpforge_core::run::Run;
292    /// use hwpforge_core::PageSettings;
293    /// use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
294    ///
295    /// let mut doc = Document::new();
296    /// doc.add_section(Section::with_paragraphs(
297    ///     vec![Paragraph::with_runs(
298    ///         vec![Run::text("Hello", CharShapeIndex::new(0))],
299    ///         ParaShapeIndex::new(0),
300    ///     )],
301    ///     PageSettings::a4(),
302    /// ));
303    ///
304    /// let validated = doc.validate().unwrap();
305    /// assert_eq!(validated.section_count(), 1);
306    /// ```
307    ///
308    /// ```
309    /// use hwpforge_core::document::Document;
310    ///
311    /// let doc = Document::new(); // empty
312    /// assert!(doc.validate().is_err());
313    /// ```
314    pub fn validate(self) -> CoreResult<Document<Validated>> {
315        validate_sections(&self.sections)?;
316        Ok(Document { sections: self.sections, metadata: self.metadata, _state: PhantomData })
317    }
318}
319
320impl Default for Document<Draft> {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326// ---------------------------------------------------------------------------
327// Manual trait impls (avoid T: Trait bounds on phantom type S)
328// ---------------------------------------------------------------------------
329
330impl<S> std::fmt::Debug for Document<S> {
331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
332        f.debug_struct("Document")
333            .field("sections", &self.sections)
334            .field("metadata", &self.metadata)
335            .finish()
336    }
337}
338
339impl<S> Clone for Document<S> {
340    fn clone(&self) -> Self {
341        Self {
342            sections: self.sections.clone(),
343            metadata: self.metadata.clone(),
344            _state: PhantomData,
345        }
346    }
347}
348
349impl<S> PartialEq for Document<S> {
350    fn eq(&self, other: &Self) -> bool {
351        self.sections == other.sections && self.metadata == other.metadata
352    }
353}
354
355impl<S> Eq for Document<S> {}
356
357impl<S> std::fmt::Display for Document<S> {
358    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359        write!(f, "Document({} sections)", self.sections.len())
360    }
361}
362
363// ---------------------------------------------------------------------------
364// Serde: serialize any state, deserialize only to Draft
365// ---------------------------------------------------------------------------
366
367impl<S> Serialize for Document<S> {
368    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
369        use serde::ser::SerializeStruct;
370        let mut state = serializer.serialize_struct("Document", 2)?;
371        state.serialize_field("sections", &self.sections)?;
372        state.serialize_field("metadata", &self.metadata)?;
373        state.end()
374    }
375}
376
377impl<'de> Deserialize<'de> for Document<Draft> {
378    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
379        #[derive(Deserialize)]
380        struct DocumentData {
381            sections: Vec<Section>,
382            metadata: Metadata,
383        }
384
385        let data = DocumentData::deserialize(deserializer)?;
386        Ok(Document { sections: data.sections, metadata: data.metadata, _state: PhantomData })
387    }
388}
389
390// ---------------------------------------------------------------------------
391// JsonSchema: hide PhantomData
392// ---------------------------------------------------------------------------
393
394impl<S> JsonSchema for Document<S> {
395    fn schema_name() -> std::borrow::Cow<'static, str> {
396        "Document".into()
397    }
398
399    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
400        schemars::json_schema!({
401            "type": "object",
402            "properties": {
403                "sections": gen.subschema_for::<Vec<Section>>(),
404                "metadata": gen.subschema_for::<crate::metadata::Metadata>(),
405            },
406            "required": ["sections", "metadata"]
407        })
408    }
409}
410
411// ---------------------------------------------------------------------------
412// Send + Sync verification
413// ---------------------------------------------------------------------------
414
415const _: () = {
416    #[allow(dead_code)]
417    fn assert_send<T: Send>() {}
418    #[allow(dead_code)]
419    fn assert_sync<T: Sync>() {}
420    #[allow(dead_code)]
421    fn verify() {
422        assert_send::<Document<Draft>>();
423        assert_sync::<Document<Draft>>();
424        assert_send::<Document<Validated>>();
425        assert_sync::<Document<Validated>>();
426    }
427};
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use crate::error::CoreError;
433    use crate::page::PageSettings;
434    use crate::paragraph::Paragraph;
435    use crate::run::Run;
436    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
437
438    fn valid_section() -> Section {
439        Section::with_paragraphs(
440            vec![Paragraph::with_runs(
441                vec![Run::text("Hello", CharShapeIndex::new(0))],
442                ParaShapeIndex::new(0),
443            )],
444            PageSettings::a4(),
445        )
446    }
447
448    // === Construction ===
449
450    #[test]
451    fn new_creates_empty_draft() {
452        let doc = Document::new();
453        assert!(doc.is_empty());
454        assert_eq!(doc.section_count(), 0);
455        assert!(doc.metadata().title.is_none());
456    }
457
458    #[test]
459    fn with_metadata() {
460        let meta = Metadata { title: Some("Test".to_string()), ..Metadata::default() };
461        let doc = Document::with_metadata(meta);
462        assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
463    }
464
465    #[test]
466    fn default_is_new() {
467        let a = Document::new();
468        let b = Document::default();
469        assert_eq!(a, b);
470    }
471
472    // === Draft mutations ===
473
474    #[test]
475    fn add_section() {
476        let mut doc = Document::new();
477        doc.add_section(valid_section());
478        assert_eq!(doc.section_count(), 1);
479        assert!(!doc.is_empty());
480    }
481
482    #[test]
483    fn add_multiple_sections() {
484        let mut doc = Document::new();
485        doc.add_section(valid_section());
486        doc.add_section(valid_section());
487        doc.add_section(valid_section());
488        assert_eq!(doc.section_count(), 3);
489    }
490
491    #[test]
492    fn set_metadata() {
493        let mut doc = Document::new();
494        doc.set_metadata(Metadata { title: Some("New".to_string()), ..Metadata::default() });
495        assert_eq!(doc.metadata().title.as_deref(), Some("New"));
496    }
497
498    #[test]
499    fn metadata_mut() {
500        let mut doc = Document::new();
501        doc.metadata_mut().title = Some("Mutated".to_string());
502        assert_eq!(doc.metadata().title.as_deref(), Some("Mutated"));
503    }
504
505    #[test]
506    fn sections_mut() {
507        let mut doc = Document::new();
508        doc.add_section(valid_section());
509        doc.add_section(valid_section());
510        assert_eq!(doc.sections_mut().len(), 2);
511    }
512
513    // === Validation (Draft -> Validated) ===
514
515    #[test]
516    fn validate_success() {
517        let mut doc = Document::new();
518        doc.add_section(valid_section());
519        let validated = doc.validate().unwrap();
520        assert_eq!(validated.section_count(), 1);
521    }
522
523    #[test]
524    fn validate_empty_document_fails() {
525        let doc = Document::new();
526        let err = doc.validate().unwrap_err();
527        assert!(matches!(err, CoreError::Validation(_)));
528    }
529
530    #[test]
531    fn validate_empty_section_fails() {
532        let mut doc = Document::new();
533        doc.add_section(Section::new(PageSettings::a4()));
534        assert!(doc.validate().is_err());
535    }
536
537    #[test]
538    fn validate_consumes_draft() {
539        let mut doc = Document::new();
540        doc.add_section(valid_section());
541        let _validated = doc.validate().unwrap();
542        // doc is moved -- attempting to use it would be a compile error
543    }
544
545    // === Validated state ===
546
547    #[test]
548    fn validated_has_read_methods() {
549        let mut doc = Document::new();
550        doc.add_section(valid_section());
551        let validated = doc.validate().unwrap();
552
553        assert_eq!(validated.section_count(), 1);
554        assert!(!validated.is_empty());
555        assert_eq!(validated.sections().len(), 1);
556        assert!(validated.metadata().title.is_none());
557    }
558
559    // === Display ===
560
561    #[test]
562    fn display_draft() {
563        let doc = Document::new();
564        assert_eq!(doc.to_string(), "Document(0 sections)");
565    }
566
567    #[test]
568    fn display_validated() {
569        let mut doc = Document::new();
570        doc.add_section(valid_section());
571        let validated = doc.validate().unwrap();
572        assert_eq!(validated.to_string(), "Document(1 sections)");
573    }
574
575    // === Equality ===
576
577    #[test]
578    fn equality_draft() {
579        let mut a = Document::new();
580        a.add_section(valid_section());
581        let mut b = Document::new();
582        b.add_section(valid_section());
583        assert_eq!(a, b);
584    }
585
586    #[test]
587    fn equality_validated() {
588        let mut a = Document::new();
589        a.add_section(valid_section());
590        let mut b = Document::new();
591        b.add_section(valid_section());
592        let va = a.validate().unwrap();
593        let vb = b.validate().unwrap();
594        assert_eq!(va, vb);
595    }
596
597    // === Clone ===
598
599    #[test]
600    fn clone_draft() {
601        let mut doc = Document::new();
602        doc.add_section(valid_section());
603        let cloned = doc.clone();
604        assert_eq!(doc, cloned);
605    }
606
607    #[test]
608    fn clone_validated() {
609        let mut doc = Document::new();
610        doc.add_section(valid_section());
611        let validated = doc.validate().unwrap();
612        let cloned = validated.clone();
613        assert_eq!(validated, cloned);
614    }
615
616    // === Serde ===
617
618    #[test]
619    fn serde_roundtrip_draft() {
620        let mut doc = Document::new();
621        doc.add_section(valid_section());
622        doc.set_metadata(Metadata { title: Some("Test".to_string()), ..Metadata::default() });
623
624        let json = serde_json::to_string(&doc).unwrap();
625        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
626        assert_eq!(doc, back);
627    }
628
629    #[test]
630    fn serde_roundtrip_validated_deserializes_to_draft() {
631        let mut doc = Document::new();
632        doc.add_section(valid_section());
633        let validated = doc.validate().unwrap();
634
635        let json = serde_json::to_string(&validated).unwrap();
636        // Deserialize always produces Draft
637        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
638        // Must re-validate
639        let re_validated = back.validate().unwrap();
640        assert_eq!(validated, re_validated);
641    }
642
643    #[test]
644    fn serde_empty_document() {
645        let doc = Document::new();
646        let json = serde_json::to_string(&doc).unwrap();
647        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
648        assert_eq!(doc, back);
649    }
650
651    // === Complex document ===
652
653    #[test]
654    fn complex_document_roundtrip() {
655        use crate::control::Control;
656        use crate::image::{Image, ImageFormat};
657        use crate::table::{Table, TableCell, TableRow};
658        use hwpforge_foundation::HwpUnit;
659
660        let cell = TableCell::new(
661            vec![Paragraph::with_runs(
662                vec![Run::text("cell", CharShapeIndex::new(0))],
663                ParaShapeIndex::new(0),
664            )],
665            HwpUnit::from_mm(50.0).unwrap(),
666        );
667        let table = Table::new(vec![TableRow::new(vec![cell])]);
668
669        let link = Control::Hyperlink {
670            text: "click".to_string(),
671            url: "https://example.com".to_string(),
672        };
673
674        let img = Image::new(
675            "test.png",
676            HwpUnit::from_mm(10.0).unwrap(),
677            HwpUnit::from_mm(10.0).unwrap(),
678            ImageFormat::Png,
679        );
680
681        let section = Section::with_paragraphs(
682            vec![
683                Paragraph::with_runs(
684                    vec![
685                        Run::text("Hello ", CharShapeIndex::new(0)),
686                        Run::text("world", CharShapeIndex::new(1)),
687                    ],
688                    ParaShapeIndex::new(0),
689                ),
690                Paragraph::with_runs(
691                    vec![Run::table(table, CharShapeIndex::new(0))],
692                    ParaShapeIndex::new(1),
693                ),
694                Paragraph::with_runs(
695                    vec![Run::control(link, CharShapeIndex::new(0))],
696                    ParaShapeIndex::new(0),
697                ),
698                Paragraph::with_runs(
699                    vec![Run::image(img, CharShapeIndex::new(0))],
700                    ParaShapeIndex::new(0),
701                ),
702            ],
703            PageSettings::a4(),
704        );
705
706        let mut doc = Document::with_metadata(Metadata {
707            title: Some("Complex Doc".to_string()),
708            author: Some("Author".to_string()),
709            keywords: vec!["test".to_string()],
710            ..Metadata::default()
711        });
712        doc.add_section(section);
713
714        let validated = doc.validate().unwrap();
715        let json = serde_json::to_string_pretty(&validated).unwrap();
716        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
717        let re_validated = back.validate().unwrap();
718        assert_eq!(validated, re_validated);
719    }
720
721    // === Debug ===
722
723    #[test]
724    fn debug_output() {
725        let doc = Document::new();
726        let s = format!("{doc:?}");
727        assert!(s.contains("Document"), "debug: {s}");
728        assert!(s.contains("sections"), "debug: {s}");
729    }
730}