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}