Skip to main content

easydoc_core/
traits.rs

1//! 核心扩展 trait 体系。
2//!
3//! 这些 trait 构成 easydoc-rust 扩展性的骨干,对标 easyexcel-rust 的
4//! `ExcelRow`、`Converter<T>`、`ReadListener<T>`、`WriteHandler`。
5//! 新增 trait 必须放在本文件,按"模型-转换-读-写-事件"分组。
6//!
7//! 对应 Java: `com.alibaba.excel` (`EasyExcel` 4.0.3) 的核心扩展点
8
9use crate::converter::ConverterRegistry;
10use crate::error::Result;
11use crate::metadata::TableColumn;
12use crate::types::{CellData, DocValue, ErrorAction, RowData, TableData};
13
14// ============================================================================
15// DocxRow — struct ↔ table row mapping
16// ============================================================================
17
18/// 结构体与 DOCX 表格行的双向映射。
19///
20/// 类似 easyexcel-rust 的 `ExcelRow`。通过 `#[derive(DocxRow)]` 自动生成实现。
21///
22/// 对应 Java: `com.alibaba.excel.annotation.ExcelProperty` + 反射机制
23///
24/// # 示例
25///
26/// ```ignore
27/// #[derive(DocxRow)]
28/// struct User {
29///     #[docx(name = "姓名", order = 0)]
30///     name: String,
31///     #[docx(name = "年龄", order = 1)]
32///     age: u32,
33/// }
34/// ```
35pub trait DocxRow {
36    /// 返回列 schema:表头名称、索引和格式提示。
37    ///
38    /// 对应 Java: `ExcelProperty` 注解的 `value()` / `order()` / `format()` 等属性
39    fn schema() -> &'static [TableColumn]
40    where
41        Self: Sized;
42
43    /// 从原始单元格值反序列化为结构体(使用默认转换器)。
44    ///
45    /// 对应 Java: `EasyExcel` 内部通过反射将 `ReadCellData` 映射到字段
46    fn from_row(row: &RowData) -> Result<Self>
47    where
48        Self: Sized;
49
50    /// 使用自定义转换器注册表从原始单元格值反序列化为结构体。
51    ///
52    /// 对应 Java: `ConverterRegistry` + `ExcelProperty(converter = ...)`
53    fn from_row_with_converters(row: &RowData, registry: &ConverterRegistry) -> Result<Self>
54    where
55        Self: Sized;
56
57    /// 将自身序列化为单元格值列表(使用默认转换器)。
58    ///
59    /// 对应 Java: `EasyExcel` 内部通过反射将字段值转为 `WriteCellData`
60    fn to_row(&self) -> Result<Vec<CellData>>;
61
62    /// 使用自定义转换器注册表将自身序列化为单元格值列表。
63    ///
64    /// 对应 Java: `ConverterRegistry` + `ExcelProperty(converter = ...)`
65    fn to_row_with_converters(&self, registry: &ConverterRegistry) -> Result<Vec<CellData>>;
66}
67
68// ============================================================================
69// DocConverter — bidirectional type conversion
70// ============================================================================
71
72/// Rust 类型 `T` 与 [`DocValue`] 之间的双向转换。
73///
74/// 对应 Java: `com.alibaba.excel.converters.Converter<T>`
75///
76/// 通过 [`ConverterRegistry`] 或 builder 的 `register_converter` 方法注册自定义转换器。
77pub trait DocConverter<T> {
78    /// 返回此转换器处理的 `TypeId`。
79    ///
80    /// 对应 Java: `Converter#supportJavaTypeKey`
81    fn support_type() -> std::any::TypeId
82    where
83        Self: Sized;
84
85    /// 将 Rust 值转换为文档值(用于写入)。
86    ///
87    /// 对应 Java: `Converter#convertToExcelData`
88    ///
89    /// # Errors
90    ///
91    /// 值无法转换时返回 [`crate::DocError::Conversion`]。
92    fn to_doc_value(&self, value: &T, column: &TableColumn) -> Result<DocValue>;
93
94    /// 将文档值转换回 Rust 值(用于读取)。
95    ///
96    /// 对应 Java: `Converter#convertToJavaData`
97    ///
98    /// # Errors
99    ///
100    /// 值无法转换时返回 [`crate::DocError::Conversion`]。
101    fn from_doc_value(&self, value: &DocValue, column: &TableColumn) -> Result<T>;
102}
103
104// ============================================================================
105// DocReadListener — streaming read callbacks
106// ============================================================================
107
108/// 读取监听器回调的上下文信息。
109#[derive(Debug, Clone)]
110pub struct DocReadContext {
111    /// 当前文档路径。
112    pub path: String,
113    /// 当前段落或表格索引(从零开始)。
114    pub index: usize,
115}
116
117/// 流式读取过程中接收已解析内容的监听器。
118///
119/// 对应 Java: `com.alibaba.excel.read.listener.ReadListener<T>`
120pub trait DocReadListener<T> {
121    /// 每解析一个数据项(段落文本或表格行)时调用。
122    ///
123    /// 对应 Java: `ReadListener#invoke`
124    ///
125    /// # Errors
126    ///
127    /// 返回错误将停止读取;可恢复错误请通过 `on_error` 返回 `ErrorAction`。
128    fn invoke(&mut self, data: T, context: &DocReadContext) -> Result<()>;
129
130    /// 遇到完整表格时调用。
131    ///
132    /// 对应 Java: `ReadListener#invokeHead`(表头行场景)
133    fn invoke_table(&mut self, table: &TableData, context: &DocReadContext) -> Result<()> {
134        let _ = (table, context);
135        Ok(())
136    }
137
138    /// 所有内容解析完成后调用。
139    ///
140    /// 对应 Java: `ReadListener#doAfterAllAnalysed`
141    fn on_complete(&mut self, _context: &DocReadContext) {}
142
143    /// 读取过程中发生非致命错误时调用。
144    ///
145    /// 对应 Java: `ReadListener#onException`
146    ///
147    /// 返回 [`ErrorAction::Stop`] 以传播错误,或
148    /// [`ErrorAction::Skip`] / [`ErrorAction::Continue`] 以继续。
149    fn on_error(
150        &mut self,
151        _error: &crate::error::DocError,
152        _context: &DocReadContext,
153    ) -> ErrorAction {
154        ErrorAction::Stop
155    }
156
157    /// 处理每个数据项前检查是否应继续读取。返回 `false` 可提前终止。
158    ///
159    /// 对应 Java: `ReadListener#hasNext`
160    fn has_next(&self, _context: &DocReadContext) -> bool {
161        true
162    }
163}
164
165// ============================================================================
166// DocWriteHandler — write lifecycle hooks
167// ============================================================================
168
169/// 文档级写入事件的上下文。
170#[derive(Debug, Clone)]
171pub struct DocWriteContext {
172    /// 输出路径。
173    pub path: String,
174}
175
176/// 段落级写入事件的上下文。
177#[derive(Debug, Clone)]
178pub struct ParagraphContext {
179    /// 段落索引(从零开始)。
180    pub index: usize,
181}
182
183/// 表格级写入事件的上下文。
184#[derive(Debug, Clone)]
185pub struct TableWriteContext {
186    /// 表格索引(从零开始)。
187    pub index: usize,
188    /// 表格行数。
189    pub row_count: usize,
190}
191
192/// 单元格级写入事件的上下文。
193#[derive(Debug, Clone)]
194pub struct CellContext {
195    /// 行索引(从零开始)。
196    pub row: usize,
197    /// 列索引(从零开始)。
198    pub column: usize,
199    /// 单元格值。
200    pub value: DocValue,
201}
202
203/// 写入生命周期拦截器 -- 在文档、段落、表格和单元格级别提供钩子。
204///
205/// 对应 Java: `com.alibaba.excel.write.handler.WriteHandler`
206///
207/// 所有方法均有空默认实现;只需覆盖需要的钩子。
208pub trait DocWriteHandler {
209    /// 执行顺序(值越小越先执行)。
210    ///
211    /// 对应 Java: `WriteHandler` 的 `order` 属性
212    #[must_use]
213    fn order() -> i32 {
214        0
215    }
216
217    /// 文档创建前调用。
218    ///
219    /// 对应 Java: `WorkbookWriteHandler#beforeWorkbookCreate`
220    fn before_document(&mut self, _ctx: &DocWriteContext) -> Result<()> {
221        Ok(())
222    }
223
224    /// 文档完成后调用。
225    ///
226    /// 对应 Java: `WorkbookWriteHandler#afterWorkbookWrite`
227    fn after_document(&mut self, _ctx: &DocWriteContext) -> Result<()> {
228        Ok(())
229    }
230
231    /// 段落写入前调用。
232    fn before_paragraph(&mut self, _ctx: &ParagraphContext) -> Result<()> {
233        Ok(())
234    }
235
236    /// 段落写入后调用。
237    fn after_paragraph(&mut self, _ctx: &ParagraphContext) -> Result<()> {
238        Ok(())
239    }
240
241    /// 表格写入前调用。
242    ///
243    /// 对应 Java: `SheetWriteHandler#beforeSheetCreate`
244    fn before_table(&mut self, _ctx: &TableWriteContext) -> Result<()> {
245        Ok(())
246    }
247
248    /// 表格写入后调用。
249    ///
250    /// 对应 Java: `SheetWriteHandler#afterSheetWrite`
251    fn after_table(&mut self, _ctx: &TableWriteContext) -> Result<()> {
252        Ok(())
253    }
254
255    /// 单元格写入前调用。
256    ///
257    /// 对应 Java: `CellWriteHandler#beforeCellCreate`
258    fn before_cell(&mut self, _ctx: &CellContext) -> Result<()> {
259        Ok(())
260    }
261
262    /// 单元格写入后调用。
263    ///
264    /// 对应 Java: `CellWriteHandler#afterCellWrite`
265    fn after_cell(&mut self, _ctx: &CellContext) -> Result<()> {
266        Ok(())
267    }
268}
269
270// ============================================================================
271// DocumentReader — 统一读取入口 trait
272// ============================================================================
273
274/// 统一的文档读取接口。
275///
276/// 后端实现(如 `office_oxide`)实现此 trait 即可接入 easydoc 读取体系。
277/// 无直接 Java 对应(Java `EasyExcel` 不提供统一读取抽象),是 easydoc-rust 自创。
278pub trait DocumentReader {
279    /// 读取文件并返回语义文档模型。
280    ///
281    /// # 错误
282    /// 文件无法打开或解析时返回错误。
283    fn read_model(&self, path: &std::path::Path) -> crate::Result<crate::DocumentContent>;
284
285    /// 读取文件并以事件流方式推送内容。
286    ///
287    /// # 错误
288    /// 文件无法打开或解析时返回错误。
289    fn read_events(&self, path: &std::path::Path, sink: &mut dyn EventSink) -> crate::Result<()>;
290}
291
292// ============================================================================
293// DocumentEvent — 文档事件枚举
294// ============================================================================
295
296/// 文档解析过程中产生的事件。
297///
298/// 用于流式读取场景,替代一次性返回完整文档模型。
299#[derive(Clone, Debug, PartialEq)]
300pub enum DocumentEvent {
301    /// 遇到标题。
302    Heading {
303        /// 标题级别。
304        level: u8,
305        /// 富文本片段。
306        runs: Vec<crate::DocumentTextRun>,
307    },
308    /// 遇到段落。
309    Paragraph(Vec<crate::DocumentTextRun>),
310    /// 遇到表格。
311    Table(crate::DocumentTable),
312    /// 遇到列表。
313    List(crate::DocumentList),
314    /// 遇到图片。
315    Image(crate::DocumentImage),
316    /// 遇到分页。
317    PageBreak,
318    /// 遇到分栏。
319    ColumnBreak,
320    /// 遇到代码块。
321    CodeBlock {
322        /// 可选语言标记。
323        language: Option<String>,
324        /// 代码文本。
325        code: String,
326    },
327    /// 遇到分区。
328    Section {
329        /// 分区类型。
330        section_type: Option<String>,
331    },
332    /// 文档开始。
333    DocumentStart,
334    /// 文档结束。
335    DocumentEnd,
336}
337
338// ============================================================================
339// EventSink — 事件消费接口
340// ============================================================================
341
342/// 事件消费回调接口。
343///
344/// 实现此 trait 以处理流式读取过程中产生的文档事件。
345/// 类比 Java: `ReadListener<T>` 的回调方法(`invoke` / `doAfterAllAnalysed`)。
346pub trait EventSink {
347    /// 处理一个文档事件。
348    ///
349    /// # 错误
350    /// 返回错误将中止读取。
351    fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()>;
352
353    /// 读取完成时调用。
354    fn on_complete(&mut self) {}
355}
356
357/// 将事件流收集为 `DocumentContent` 的默认实现。
358///
359/// 类比 Java: `ReadListener` 的默认收集行为。
360pub struct ContentCollector {
361    blocks: Vec<crate::DocumentBlock>,
362}
363
364impl ContentCollector {
365    /// 创建新的收集器。
366    #[must_use]
367    pub fn new() -> Self {
368        Self { blocks: Vec::new() }
369    }
370
371    /// 将收集的事件转换为语义文档。
372    #[must_use]
373    pub fn into_content(self) -> crate::DocumentContent {
374        crate::DocumentContent {
375            metadata: crate::DocumentMeta::default(),
376            blocks: self.blocks,
377        }
378    }
379}
380
381impl Default for ContentCollector {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387impl EventSink for ContentCollector {
388    fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()> {
389        match event {
390            DocumentEvent::Heading { level, runs } => {
391                self.blocks.push(crate::DocumentBlock::Heading {
392                    level: *level,
393                    runs: runs.clone(),
394                });
395            }
396            DocumentEvent::Paragraph(runs) => {
397                self.blocks
398                    .push(crate::DocumentBlock::Paragraph(runs.clone()));
399            }
400            DocumentEvent::Table(table) => {
401                self.blocks.push(crate::DocumentBlock::Table(table.clone()));
402            }
403            DocumentEvent::List(list) => {
404                self.blocks.push(crate::DocumentBlock::List(list.clone()));
405            }
406            DocumentEvent::Image(image) => {
407                self.blocks.push(crate::DocumentBlock::Image(image.clone()));
408            }
409            DocumentEvent::PageBreak => {
410                self.blocks.push(crate::DocumentBlock::PageBreak);
411            }
412            DocumentEvent::ColumnBreak => {
413                self.blocks.push(crate::DocumentBlock::ColumnBreak);
414            }
415            DocumentEvent::CodeBlock { language, code } => {
416                self.blocks.push(crate::DocumentBlock::CodeBlock {
417                    language: language.clone(),
418                    code: code.clone(),
419                });
420            }
421            DocumentEvent::Section { section_type } => {
422                self.blocks.push(crate::DocumentBlock::Section {
423                    blocks: Vec::new(),
424                    section_type: section_type.clone(),
425                });
426            }
427            DocumentEvent::DocumentStart | DocumentEvent::DocumentEnd => {}
428        }
429        Ok(())
430    }
431}
432
433#[cfg(test)]
434mod event_tests {
435    use super::*;
436
437    #[test]
438    fn document_event_debug() {
439        let event = DocumentEvent::DocumentStart;
440        assert_eq!(format!("{event:?}"), "DocumentStart");
441    }
442
443    #[test]
444    fn document_event_heading() {
445        let event = DocumentEvent::Heading {
446            level: 1,
447            runs: vec![crate::DocumentTextRun {
448                text: "Title".into(),
449                ..crate::DocumentTextRun::default()
450            }],
451        };
452        match &event {
453            DocumentEvent::Heading { level, runs } => {
454                assert_eq!(*level, 1);
455                assert_eq!(runs[0].text, "Title");
456            }
457            _ => panic!("expected Heading"),
458        }
459    }
460
461    #[test]
462    fn content_collector_roundtrip() {
463        let mut collector = ContentCollector::new();
464        collector.on_event(&DocumentEvent::DocumentStart).unwrap();
465        collector
466            .on_event(&DocumentEvent::Paragraph(vec![crate::DocumentTextRun {
467                text: "Hello".into(),
468                ..crate::DocumentTextRun::default()
469            }]))
470            .unwrap();
471        collector.on_event(&DocumentEvent::PageBreak).unwrap();
472        collector.on_event(&DocumentEvent::DocumentEnd).unwrap();
473
474        let content = collector.into_content();
475        assert_eq!(content.blocks.len(), 2);
476        assert!(matches!(
477            content.blocks[0],
478            crate::DocumentBlock::Paragraph(_)
479        ));
480        assert!(matches!(content.blocks[1], crate::DocumentBlock::PageBreak));
481    }
482
483    #[test]
484    fn content_collector_table_and_list() {
485        let mut collector = ContentCollector::new();
486        collector
487            .on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
488            .unwrap();
489        collector
490            .on_event(&DocumentEvent::List(crate::DocumentList {
491                ordered: false,
492                start_number: None,
493                items: vec![],
494            }))
495            .unwrap();
496        let content = collector.into_content();
497        assert_eq!(content.blocks.len(), 2);
498    }
499
500    #[test]
501    fn content_collector_codeblock() {
502        let mut collector = ContentCollector::new();
503        collector
504            .on_event(&DocumentEvent::CodeBlock {
505                language: Some("rust".into()),
506                code: "fn main() {}".into(),
507            })
508            .unwrap();
509        let content = collector.into_content();
510        match &content.blocks[0] {
511            crate::DocumentBlock::CodeBlock { language, code } => {
512                assert_eq!(language.as_deref(), Some("rust"));
513                assert_eq!(code, "fn main() {}");
514            }
515            _ => panic!("expected CodeBlock"),
516        }
517    }
518
519    #[test]
520    fn content_collector_section() {
521        let mut collector = ContentCollector::new();
522        collector
523            .on_event(&DocumentEvent::Section {
524                section_type: Some("continuous".into()),
525            })
526            .unwrap();
527        let content = collector.into_content();
528        match &content.blocks[0] {
529            crate::DocumentBlock::Section {
530                blocks,
531                section_type,
532            } => {
533                assert!(blocks.is_empty());
534                assert_eq!(section_type.as_deref(), Some("continuous"));
535            }
536            _ => panic!("expected Section"),
537        }
538    }
539}
540
541#[cfg(test)]
542mod trait_coverage_tests {
543    use super::*;
544
545    struct NoopHandler;
546    impl DocWriteHandler for NoopHandler {}
547
548    #[test]
549    fn noop_handler_all_defaults() {
550        let mut h = NoopHandler;
551        assert_eq!(NoopHandler::order(), 0);
552        let ctx = DocWriteContext {
553            path: "test".into(),
554        };
555        h.before_document(&ctx).unwrap();
556        h.after_document(&ctx).unwrap();
557        let pctx = ParagraphContext { index: 0 };
558        h.before_paragraph(&pctx).unwrap();
559        h.after_paragraph(&pctx).unwrap();
560        let tctx = TableWriteContext {
561            index: 0,
562            row_count: 1,
563        };
564        h.before_table(&tctx).unwrap();
565        h.after_table(&tctx).unwrap();
566        let cctx = CellContext {
567            row: 0,
568            column: 0,
569            value: DocValue::Empty,
570        };
571        h.before_cell(&cctx).unwrap();
572        h.after_cell(&cctx).unwrap();
573    }
574
575    #[test]
576    fn read_listener_defaults() {
577        struct TestListener;
578        impl DocReadListener<String> for TestListener {
579            fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
580                Ok(())
581            }
582        }
583        let mut listener = TestListener;
584        let ctx = DocReadContext {
585            path: "test".into(),
586            index: 0,
587        };
588        assert!(listener.has_next(&ctx));
589        assert!(matches!(
590            listener.on_error(&crate::DocError::Document("x".into()), &ctx),
591            ErrorAction::Stop
592        ));
593        listener.on_complete(&ctx);
594    }
595
596    #[test]
597    fn read_listener_invoke_table_default() {
598        struct TestListener;
599        impl DocReadListener<String> for TestListener {
600            fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
601                Ok(())
602            }
603        }
604        let mut listener = TestListener;
605        let ctx = DocReadContext {
606            path: "test".into(),
607            index: 0,
608        };
609        let table = TableData {
610            headers: None,
611            rows: vec![],
612        };
613        listener.invoke_table(&table, &ctx).unwrap();
614    }
615
616    #[test]
617    fn content_collector_all_event_types() {
618        let mut c = ContentCollector::new();
619        c.on_event(&DocumentEvent::DocumentStart).unwrap();
620        c.on_event(&DocumentEvent::Heading {
621            level: 1,
622            runs: vec![],
623        })
624        .unwrap();
625        c.on_event(&DocumentEvent::Paragraph(vec![])).unwrap();
626        c.on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
627            .unwrap();
628        c.on_event(&DocumentEvent::List(crate::DocumentList {
629            ordered: false,
630            start_number: None,
631            items: vec![],
632        }))
633        .unwrap();
634        c.on_event(&DocumentEvent::Image(crate::DocumentImage {
635            alt_text: None,
636            data: None,
637            extension: None,
638        }))
639        .unwrap();
640        c.on_event(&DocumentEvent::PageBreak).unwrap();
641        c.on_event(&DocumentEvent::ColumnBreak).unwrap();
642        c.on_event(&DocumentEvent::CodeBlock {
643            language: None,
644            code: String::new(),
645        })
646        .unwrap();
647        c.on_event(&DocumentEvent::Section { section_type: None })
648            .unwrap();
649        c.on_event(&DocumentEvent::DocumentEnd).unwrap();
650        c.on_complete();
651        let content = c.into_content();
652        assert_eq!(content.blocks.len(), 9); // DocumentStart/DocumentEnd produce no blocks
653    }
654
655    #[test]
656    fn content_collector_default() {
657        let c = ContentCollector::default();
658        let content = c.into_content();
659        assert!(content.blocks.is_empty());
660    }
661
662    #[test]
663    fn doc_read_context_clone_debug() {
664        let ctx = DocReadContext {
665            path: "test".into(),
666            index: 5,
667        };
668        let ctx2 = ctx.clone();
669        assert_eq!(ctx2.index, 5);
670        assert!(format!("{ctx:?}").contains("test"));
671    }
672
673    #[test]
674    fn doc_write_context_clone_debug() {
675        let ctx = DocWriteContext {
676            path: "out.docx".into(),
677        };
678        let ctx2 = ctx.clone();
679        assert_eq!(ctx2.path, "out.docx");
680        assert!(format!("{ctx:?}").contains("out.docx"));
681    }
682
683    #[test]
684    fn paragraph_context_clone_debug() {
685        let ctx = ParagraphContext { index: 3 };
686        let ctx2 = ctx.clone();
687        assert_eq!(ctx2.index, 3);
688        assert!(format!("{ctx:?}").contains('3'));
689    }
690
691    #[test]
692    fn table_write_context_clone_debug() {
693        let ctx = TableWriteContext {
694            index: 1,
695            row_count: 10,
696        };
697        let ctx2 = ctx.clone();
698        assert_eq!(ctx2.index, 1);
699        assert_eq!(ctx2.row_count, 10);
700        assert!(format!("{ctx:?}").contains("10"));
701    }
702
703    #[test]
704    fn cell_context_clone_debug() {
705        let ctx = CellContext {
706            row: 2,
707            column: 3,
708            value: DocValue::Int(42),
709        };
710        let ctx2 = ctx.clone();
711        assert_eq!(ctx2.row, 2);
712        assert!(format!("{ctx:?}").contains("42"));
713    }
714
715    #[test]
716    fn document_event_clone_debug() {
717        let events = vec![
718            DocumentEvent::DocumentStart,
719            DocumentEvent::DocumentEnd,
720            DocumentEvent::PageBreak,
721            DocumentEvent::ColumnBreak,
722            DocumentEvent::Heading {
723                level: 1,
724                runs: vec![],
725            },
726            DocumentEvent::Paragraph(vec![]),
727            DocumentEvent::Section { section_type: None },
728            DocumentEvent::CodeBlock {
729                language: None,
730                code: String::new(),
731            },
732        ];
733        for event in &events {
734            let _clone = event.clone();
735            let _debug = format!("{event:?}");
736        }
737    }
738}