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}