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    /// 문서의 문단을 문서 순서(pre-order)로 방문한다.
186    ///
187    /// 캐시 정규화([`Self::strip_layout_caches`]) 등 전 문단 일괄 변환의
188    /// 진입점이다. `Draft` 전용 — `Validated` 는 typestate 상 불변이므로
189    /// 비교가 필요하면 검증 전 사본에서 수행한다.
190    ///
191    /// # 재귀 대상 (정확한 목록)
192    ///
193    /// 섹션마다 본문 → 머리말 → 꼬리말 → 바탕쪽 순으로 돌고, 각 문단은
194    /// **자신을 먼저** 방문한 뒤 run 안으로 내려간다:
195    ///
196    /// - 표: 행 → 셀 → 셀 문단(중첩 표 포함), 그다음 표 캡션
197    /// - 도형/글상자: 본문 문단 + 캡션 (타원·다각형 동일; 선·사각형·호·
198    ///   곡선·연결선은 캡션만)
199    /// - 각주·미주: 본문 문단
200    /// - 메모: 본문 문단 + 앵커 run 안의 중첩
201    /// - 묶음 객체: 자식 컨트롤로 재귀
202    ///
203    /// # 방문하지 않는 것 (알려진 갭)
204    ///
205    /// **[`crate::image::Image::caption`] 안의 문단은 방문하지 않는다.**
206    /// [`crate::run::RunContent::Image`] 는 재귀 대상이 아니기 때문이다.
207    /// 표·글상자 캡션은 방문되므로 캡션 처리가 비대칭이다 — 의도된 설계가
208    /// 아니라 갭이며, 재귀 대상을 넓히면 캐시 정규화·편집 파이프라인 등
209    /// **모든 기존 호출자의 동작이 바뀌므로** 픽스처를 갖춘 별도 슬라이스로
210    /// 다룬다. `.docs/followups.md` 에 기록돼 있다.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use hwpforge_core::document::Document;
216    /// use hwpforge_core::page::PageSettings;
217    /// use hwpforge_core::paragraph::Paragraph;
218    /// use hwpforge_core::section::Section;
219    /// use hwpforge_foundation::ParaShapeIndex;
220    ///
221    /// let mut doc = Document::new();
222    /// doc.add_section(Section::with_paragraphs(
223    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
224    ///     PageSettings::a4(),
225    /// ));
226    /// let mut count = 0;
227    /// doc.for_each_paragraph_mut(|_| count += 1);
228    /// assert_eq!(count, 1);
229    /// ```
230    pub fn for_each_paragraph_mut<F: FnMut(&mut Paragraph)>(&mut self, mut f: F) {
231        for section in &mut self.sections {
232            section.walk_paragraphs_mut(&mut f);
233        }
234    }
235
236    /// [`Self::for_each_paragraph_mut`] 의 불변 쌍둥이 — **방문 순서와 재귀
237    /// 대상이 완전히 같다**. 재귀 대상 목록과 이미지 캡션 갭은 그쪽 문서에
238    /// 있다.
239    ///
240    /// 문서를 바꾸지 않고 훑기만 하는 호출자(자산 계획 수집·통계·검증)를
241    /// 위한 것이다. 순서가 같아야 두 순회를 같은 문단 번호로 짝지을 수
242    /// 있으므로, 한쪽만 고치면 안 된다 —
243    /// `layout::tests::immutable_walker_matches_the_mutable_one` 이 잠근다.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use hwpforge_core::document::Document;
249    /// use hwpforge_core::page::PageSettings;
250    /// use hwpforge_core::paragraph::Paragraph;
251    /// use hwpforge_core::section::Section;
252    /// use hwpforge_foundation::ParaShapeIndex;
253    ///
254    /// let mut doc = Document::new();
255    /// doc.add_section(Section::with_paragraphs(
256    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
257    ///     PageSettings::a4(),
258    /// ));
259    /// let mut count = 0;
260    /// doc.for_each_paragraph(|_| count += 1);
261    /// assert_eq!(count, 1);
262    /// ```
263    pub fn for_each_paragraph<F: FnMut(&Paragraph)>(&self, mut f: F) {
264        for section in &self.sections {
265            section.walk_paragraphs(&mut f);
266        }
267    }
268
269    /// 모든 문단의 줄 조판 캐시([`Paragraph::layout_cache`])를 제거한다.
270    ///
271    /// 문서 동등성 비교(admission/golden)의 정규화 단계: 네이티브 입력은
272    /// 캐시를 보유하고 우리 재인코드 산출물은 미보유가 정상이므로, 비교
273    /// 전 양쪽 사본에서 이 함수를 호출해 캐시 차이를 제거한다.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use hwpforge_core::document::Document;
279    /// use hwpforge_core::layout::LayoutCache;
280    /// use hwpforge_core::page::PageSettings;
281    /// use hwpforge_core::paragraph::Paragraph;
282    /// use hwpforge_core::section::Section;
283    /// use hwpforge_foundation::ParaShapeIndex;
284    ///
285    /// let mut para = Paragraph::new(ParaShapeIndex::new(0));
286    /// para.layout_cache = Some(LayoutCache::default());
287    /// let mut doc = Document::new();
288    /// doc.add_section(Section::with_paragraphs(vec![para], PageSettings::a4()));
289    /// doc.strip_layout_caches();
290    /// assert!(doc.sections()[0].paragraphs[0].layout_cache.is_none());
291    /// ```
292    pub fn strip_layout_caches(&mut self) {
293        self.for_each_paragraph_mut(|p| {
294            p.layout_cache = None;
295            // 표 수준 decode-only 캐시도 함께 제거 — 방문 문단의 직계 run 만
296            // 보면 충분하다 (중첩 표의 host 는 셀 문단이고, walker 가 셀
297            // 문단도 방문한다).
298            for run in &mut p.runs {
299                if let crate::run::RunContent::Table(table) = &mut run.content {
300                    table.layout_cache = None;
301                }
302            }
303        });
304    }
305
306    /// Appends a section to the draft document.
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use hwpforge_core::document::Document;
312    /// use hwpforge_core::section::Section;
313    /// use hwpforge_core::PageSettings;
314    ///
315    /// let mut doc = Document::new();
316    /// doc.add_section(Section::new(PageSettings::a4()));
317    /// assert_eq!(doc.section_count(), 1);
318    /// ```
319    pub fn add_section(&mut self, section: Section) {
320        self.sections.push(section);
321    }
322
323    /// Sets the document metadata.
324    pub fn set_metadata(&mut self, metadata: Metadata) {
325        self.metadata = metadata;
326    }
327
328    /// Returns a mutable reference to the metadata.
329    pub fn metadata_mut(&mut self) -> &mut Metadata {
330        &mut self.metadata
331    }
332
333    /// Returns a mutable slice of sections.
334    pub fn sections_mut(&mut self) -> &mut [Section] {
335        &mut self.sections
336    }
337
338    /// Validates the document structure and transitions to `Validated`.
339    ///
340    /// Consumes `self` (move semantics). On success, returns a
341    /// `Document<Validated>`. On failure, returns a `CoreError`.
342    ///
343    /// # Errors
344    ///
345    /// Returns [`CoreError::Validation`](crate::error::CoreError::Validation) if the document violates any
346    /// structural invariant (empty sections, empty paragraphs, etc.).
347    ///
348    /// # Examples
349    ///
350    /// ```
351    /// use hwpforge_core::document::Document;
352    /// use hwpforge_core::section::Section;
353    /// use hwpforge_core::paragraph::Paragraph;
354    /// use hwpforge_core::run::Run;
355    /// use hwpforge_core::PageSettings;
356    /// use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
357    ///
358    /// let mut doc = Document::new();
359    /// doc.add_section(Section::with_paragraphs(
360    ///     vec![Paragraph::with_runs(
361    ///         vec![Run::text("Hello", CharShapeIndex::new(0))],
362    ///         ParaShapeIndex::new(0),
363    ///     )],
364    ///     PageSettings::a4(),
365    /// ));
366    ///
367    /// let validated = doc.validate().unwrap();
368    /// assert_eq!(validated.section_count(), 1);
369    /// ```
370    ///
371    /// ```
372    /// use hwpforge_core::document::Document;
373    ///
374    /// let doc = Document::new(); // empty
375    /// assert!(doc.validate().is_err());
376    /// ```
377    pub fn validate(self) -> CoreResult<Document<Validated>> {
378        validate_sections(&self.sections)?;
379        Ok(Document { sections: self.sections, metadata: self.metadata, _state: PhantomData })
380    }
381}
382
383impl Default for Document<Draft> {
384    fn default() -> Self {
385        Self::new()
386    }
387}
388
389// ---------------------------------------------------------------------------
390// Manual trait impls (avoid T: Trait bounds on phantom type S)
391// ---------------------------------------------------------------------------
392
393impl<S> std::fmt::Debug for Document<S> {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        f.debug_struct("Document")
396            .field("sections", &self.sections)
397            .field("metadata", &self.metadata)
398            .finish()
399    }
400}
401
402impl<S> Clone for Document<S> {
403    fn clone(&self) -> Self {
404        Self {
405            sections: self.sections.clone(),
406            metadata: self.metadata.clone(),
407            _state: PhantomData,
408        }
409    }
410}
411
412impl<S> PartialEq for Document<S> {
413    fn eq(&self, other: &Self) -> bool {
414        self.sections == other.sections && self.metadata == other.metadata
415    }
416}
417
418impl<S> Eq for Document<S> {}
419
420impl<S> std::fmt::Display for Document<S> {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "Document({} sections)", self.sections.len())
423    }
424}
425
426// ---------------------------------------------------------------------------
427// Serde: serialize any state, deserialize only to Draft
428// ---------------------------------------------------------------------------
429
430impl<S> Serialize for Document<S> {
431    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
432        use serde::ser::SerializeStruct;
433        let mut state = serializer.serialize_struct("Document", 2)?;
434        state.serialize_field("sections", &self.sections)?;
435        state.serialize_field("metadata", &self.metadata)?;
436        state.end()
437    }
438}
439
440impl<'de> Deserialize<'de> for Document<Draft> {
441    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
442        #[derive(Deserialize)]
443        struct DocumentData {
444            sections: Vec<Section>,
445            metadata: Metadata,
446        }
447
448        let data = DocumentData::deserialize(deserializer)?;
449        Ok(Document { sections: data.sections, metadata: data.metadata, _state: PhantomData })
450    }
451}
452
453// ---------------------------------------------------------------------------
454// JsonSchema: hide PhantomData
455// ---------------------------------------------------------------------------
456
457impl<S> JsonSchema for Document<S> {
458    fn schema_name() -> std::borrow::Cow<'static, str> {
459        "Document".into()
460    }
461
462    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
463        schemars::json_schema!({
464            "type": "object",
465            "properties": {
466                "sections": gen.subschema_for::<Vec<Section>>(),
467                "metadata": gen.subschema_for::<crate::metadata::Metadata>(),
468            },
469            "required": ["sections", "metadata"]
470        })
471    }
472}
473
474// ---------------------------------------------------------------------------
475// Send + Sync verification
476// ---------------------------------------------------------------------------
477
478const _: () = {
479    #[allow(dead_code)]
480    fn assert_send<T: Send>() {}
481    #[allow(dead_code)]
482    fn assert_sync<T: Sync>() {}
483    #[allow(dead_code)]
484    fn verify() {
485        assert_send::<Document<Draft>>();
486        assert_sync::<Document<Draft>>();
487        assert_send::<Document<Validated>>();
488        assert_sync::<Document<Validated>>();
489    }
490};
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use crate::error::CoreError;
496    use crate::page::PageSettings;
497    use crate::paragraph::Paragraph;
498    use crate::run::Run;
499    use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
500
501    fn valid_section() -> Section {
502        Section::with_paragraphs(
503            vec![Paragraph::with_runs(
504                vec![Run::text("Hello", CharShapeIndex::new(0))],
505                ParaShapeIndex::new(0),
506            )],
507            PageSettings::a4(),
508        )
509    }
510
511    // === Construction ===
512
513    #[test]
514    fn new_creates_empty_draft() {
515        let doc = Document::new();
516        assert!(doc.is_empty());
517        assert_eq!(doc.section_count(), 0);
518        assert!(doc.metadata().title.is_none());
519    }
520
521    #[test]
522    fn with_metadata() {
523        let meta = Metadata { title: Some("Test".to_string()), ..Metadata::default() };
524        let doc = Document::with_metadata(meta);
525        assert_eq!(doc.metadata().title.as_deref(), Some("Test"));
526    }
527
528    #[test]
529    fn default_is_new() {
530        let a = Document::new();
531        let b = Document::default();
532        assert_eq!(a, b);
533    }
534
535    // === Draft mutations ===
536
537    #[test]
538    fn add_section() {
539        let mut doc = Document::new();
540        doc.add_section(valid_section());
541        assert_eq!(doc.section_count(), 1);
542        assert!(!doc.is_empty());
543    }
544
545    #[test]
546    fn add_multiple_sections() {
547        let mut doc = Document::new();
548        doc.add_section(valid_section());
549        doc.add_section(valid_section());
550        doc.add_section(valid_section());
551        assert_eq!(doc.section_count(), 3);
552    }
553
554    #[test]
555    fn set_metadata() {
556        let mut doc = Document::new();
557        doc.set_metadata(Metadata { title: Some("New".to_string()), ..Metadata::default() });
558        assert_eq!(doc.metadata().title.as_deref(), Some("New"));
559    }
560
561    #[test]
562    fn metadata_mut() {
563        let mut doc = Document::new();
564        doc.metadata_mut().title = Some("Mutated".to_string());
565        assert_eq!(doc.metadata().title.as_deref(), Some("Mutated"));
566    }
567
568    #[test]
569    fn sections_mut() {
570        let mut doc = Document::new();
571        doc.add_section(valid_section());
572        doc.add_section(valid_section());
573        assert_eq!(doc.sections_mut().len(), 2);
574    }
575
576    // === Validation (Draft -> Validated) ===
577
578    #[test]
579    fn validate_success() {
580        let mut doc = Document::new();
581        doc.add_section(valid_section());
582        let validated = doc.validate().unwrap();
583        assert_eq!(validated.section_count(), 1);
584    }
585
586    #[test]
587    fn validate_empty_document_fails() {
588        let doc = Document::new();
589        let err = doc.validate().unwrap_err();
590        assert!(matches!(err, CoreError::Validation(_)));
591    }
592
593    #[test]
594    fn validate_empty_section_fails() {
595        let mut doc = Document::new();
596        doc.add_section(Section::new(PageSettings::a4()));
597        assert!(doc.validate().is_err());
598    }
599
600    #[test]
601    fn validate_consumes_draft() {
602        let mut doc = Document::new();
603        doc.add_section(valid_section());
604        let _validated = doc.validate().unwrap();
605        // doc is moved -- attempting to use it would be a compile error
606    }
607
608    // === Validated state ===
609
610    #[test]
611    fn validated_has_read_methods() {
612        let mut doc = Document::new();
613        doc.add_section(valid_section());
614        let validated = doc.validate().unwrap();
615
616        assert_eq!(validated.section_count(), 1);
617        assert!(!validated.is_empty());
618        assert_eq!(validated.sections().len(), 1);
619        assert!(validated.metadata().title.is_none());
620    }
621
622    // === Display ===
623
624    #[test]
625    fn display_draft() {
626        let doc = Document::new();
627        assert_eq!(doc.to_string(), "Document(0 sections)");
628    }
629
630    #[test]
631    fn display_validated() {
632        let mut doc = Document::new();
633        doc.add_section(valid_section());
634        let validated = doc.validate().unwrap();
635        assert_eq!(validated.to_string(), "Document(1 sections)");
636    }
637
638    // === Equality ===
639
640    #[test]
641    fn equality_draft() {
642        let mut a = Document::new();
643        a.add_section(valid_section());
644        let mut b = Document::new();
645        b.add_section(valid_section());
646        assert_eq!(a, b);
647    }
648
649    #[test]
650    fn equality_validated() {
651        let mut a = Document::new();
652        a.add_section(valid_section());
653        let mut b = Document::new();
654        b.add_section(valid_section());
655        let va = a.validate().unwrap();
656        let vb = b.validate().unwrap();
657        assert_eq!(va, vb);
658    }
659
660    // === Clone ===
661
662    #[test]
663    fn clone_draft() {
664        let mut doc = Document::new();
665        doc.add_section(valid_section());
666        let cloned = doc.clone();
667        assert_eq!(doc, cloned);
668    }
669
670    #[test]
671    fn clone_validated() {
672        let mut doc = Document::new();
673        doc.add_section(valid_section());
674        let validated = doc.validate().unwrap();
675        let cloned = validated.clone();
676        assert_eq!(validated, cloned);
677    }
678
679    // === Serde ===
680
681    #[test]
682    fn serde_roundtrip_draft() {
683        let mut doc = Document::new();
684        doc.add_section(valid_section());
685        doc.set_metadata(Metadata { title: Some("Test".to_string()), ..Metadata::default() });
686
687        let json = serde_json::to_string(&doc).unwrap();
688        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
689        assert_eq!(doc, back);
690    }
691
692    #[test]
693    fn serde_roundtrip_validated_deserializes_to_draft() {
694        let mut doc = Document::new();
695        doc.add_section(valid_section());
696        let validated = doc.validate().unwrap();
697
698        let json = serde_json::to_string(&validated).unwrap();
699        // Deserialize always produces Draft
700        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
701        // Must re-validate
702        let re_validated = back.validate().unwrap();
703        assert_eq!(validated, re_validated);
704    }
705
706    #[test]
707    fn serde_empty_document() {
708        let doc = Document::new();
709        let json = serde_json::to_string(&doc).unwrap();
710        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
711        assert_eq!(doc, back);
712    }
713
714    // === Complex document ===
715
716    #[test]
717    fn complex_document_roundtrip() {
718        use crate::control::Control;
719        use crate::image::{Image, ImageFormat};
720        use crate::table::{Table, TableCell, TableRow};
721        use hwpforge_foundation::HwpUnit;
722
723        let cell = TableCell::new(
724            vec![Paragraph::with_runs(
725                vec![Run::text("cell", CharShapeIndex::new(0))],
726                ParaShapeIndex::new(0),
727            )],
728            HwpUnit::from_mm(50.0).unwrap(),
729        );
730        let table = Table::new(vec![TableRow::new(vec![cell])]);
731
732        let link = Control::Hyperlink {
733            text: "click".to_string(),
734            url: "https://example.com".to_string(),
735        };
736
737        let img = Image::new(
738            "test.png",
739            HwpUnit::from_mm(10.0).unwrap(),
740            HwpUnit::from_mm(10.0).unwrap(),
741            ImageFormat::Png,
742        );
743
744        let section = Section::with_paragraphs(
745            vec![
746                Paragraph::with_runs(
747                    vec![
748                        Run::text("Hello ", CharShapeIndex::new(0)),
749                        Run::text("world", CharShapeIndex::new(1)),
750                    ],
751                    ParaShapeIndex::new(0),
752                ),
753                Paragraph::with_runs(
754                    vec![Run::table(table, CharShapeIndex::new(0))],
755                    ParaShapeIndex::new(1),
756                ),
757                Paragraph::with_runs(
758                    vec![Run::control(link, CharShapeIndex::new(0))],
759                    ParaShapeIndex::new(0),
760                ),
761                Paragraph::with_runs(
762                    vec![Run::image(img, CharShapeIndex::new(0))],
763                    ParaShapeIndex::new(0),
764                ),
765            ],
766            PageSettings::a4(),
767        );
768
769        let mut doc = Document::with_metadata(Metadata {
770            title: Some("Complex Doc".to_string()),
771            author: Some("Author".to_string()),
772            keywords: vec!["test".to_string()],
773            ..Metadata::default()
774        });
775        doc.add_section(section);
776
777        let validated = doc.validate().unwrap();
778        let json = serde_json::to_string_pretty(&validated).unwrap();
779        let back: Document<Draft> = serde_json::from_str(&json).unwrap();
780        let re_validated = back.validate().unwrap();
781        assert_eq!(validated, re_validated);
782    }
783
784    // === Debug ===
785
786    #[test]
787    fn debug_output() {
788        let doc = Document::new();
789        let s = format!("{doc:?}");
790        assert!(s.contains("Document"), "debug: {s}");
791        assert!(s.contains("sections"), "debug: {s}");
792    }
793}