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)]
300#[non_exhaustive]
301pub enum DocumentEvent {
302    /// 遇到标题。
303    Heading {
304        /// 标题级别。
305        level: u8,
306        /// 富文本片段。
307        runs: Vec<crate::DocumentTextRun>,
308    },
309    /// 遇到段落。
310    Paragraph(Vec<crate::DocumentTextRun>),
311    /// 遇到表格。
312    Table(crate::DocumentTable),
313    /// 遇到列表。
314    List(crate::DocumentList),
315    /// 遇到图片。
316    Image(crate::DocumentImage),
317    /// 遇到分页。
318    PageBreak,
319    /// 遇到分栏。
320    ColumnBreak,
321    /// 遇到代码块。
322    CodeBlock {
323        /// 可选语言标记。
324        language: Option<String>,
325        /// 代码文本。
326        code: String,
327    },
328    /// 遇到分区。
329    Section {
330        /// 分区类型。
331        section_type: Option<String>,
332    },
333    /// 文档开始。
334    DocumentStart,
335    /// 文档结束。
336    DocumentEnd,
337}
338
339// ============================================================================
340// EventSink — 事件消费接口
341// ============================================================================
342
343/// 事件消费回调接口。
344///
345/// 实现此 trait 以处理流式读取过程中产生的文档事件。
346/// 类比 Java: `ReadListener<T>` 的回调方法(`invoke` / `doAfterAllAnalysed`)。
347pub trait EventSink {
348    /// 处理一个文档事件。
349    ///
350    /// # 错误
351    /// 返回错误将中止读取。
352    fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()>;
353
354    /// 读取完成时调用。
355    fn on_complete(&mut self) {}
356}
357
358/// 将事件流收集为 `DocumentContent` 的默认实现。
359///
360/// 类比 Java: `ReadListener` 的默认收集行为。
361pub struct ContentCollector {
362    blocks: Vec<crate::DocumentBlock>,
363}
364
365impl ContentCollector {
366    /// 创建新的收集器。
367    #[must_use]
368    pub fn new() -> Self {
369        Self { blocks: Vec::new() }
370    }
371
372    /// 将收集的事件转换为语义文档。
373    #[must_use]
374    pub fn into_content(self) -> crate::DocumentContent {
375        crate::DocumentContent {
376            metadata: crate::DocumentMeta::default(),
377            blocks: self.blocks,
378        }
379    }
380}
381
382impl Default for ContentCollector {
383    fn default() -> Self {
384        Self::new()
385    }
386}
387
388impl EventSink for ContentCollector {
389    fn on_event(&mut self, event: &DocumentEvent) -> crate::Result<()> {
390        match event {
391            DocumentEvent::Heading { level, runs } => {
392                self.blocks.push(crate::DocumentBlock::Heading {
393                    level: *level,
394                    runs: runs.clone(),
395                });
396            }
397            DocumentEvent::Paragraph(runs) => {
398                self.blocks
399                    .push(crate::DocumentBlock::Paragraph(runs.clone()));
400            }
401            DocumentEvent::Table(table) => {
402                self.blocks.push(crate::DocumentBlock::Table(table.clone()));
403            }
404            DocumentEvent::List(list) => {
405                self.blocks.push(crate::DocumentBlock::List(list.clone()));
406            }
407            DocumentEvent::Image(image) => {
408                self.blocks.push(crate::DocumentBlock::Image(image.clone()));
409            }
410            DocumentEvent::PageBreak => {
411                self.blocks.push(crate::DocumentBlock::PageBreak);
412            }
413            DocumentEvent::ColumnBreak => {
414                self.blocks.push(crate::DocumentBlock::ColumnBreak);
415            }
416            DocumentEvent::CodeBlock { language, code } => {
417                self.blocks.push(crate::DocumentBlock::CodeBlock {
418                    language: language.clone(),
419                    code: code.clone(),
420                });
421            }
422            DocumentEvent::Section { section_type } => {
423                self.blocks.push(crate::DocumentBlock::Section {
424                    blocks: Vec::new(),
425                    section_type: section_type.clone(),
426                });
427            }
428            DocumentEvent::DocumentStart | DocumentEvent::DocumentEnd => {}
429        }
430        Ok(())
431    }
432}
433
434#[cfg(test)]
435mod event_tests {
436    use super::*;
437
438    #[test]
439    fn document_event_debug() {
440        let event = DocumentEvent::DocumentStart;
441        assert_eq!(format!("{event:?}"), "DocumentStart");
442    }
443
444    #[test]
445    fn document_event_heading() {
446        let event = DocumentEvent::Heading {
447            level: 1,
448            runs: vec![crate::DocumentTextRun {
449                text: "Title".into(),
450                ..crate::DocumentTextRun::default()
451            }],
452        };
453        match &event {
454            DocumentEvent::Heading { level, runs } => {
455                assert_eq!(*level, 1);
456                assert_eq!(runs[0].text, "Title");
457            }
458            _ => panic!("expected Heading"),
459        }
460    }
461
462    #[test]
463    fn content_collector_roundtrip() {
464        let mut collector = ContentCollector::new();
465        collector.on_event(&DocumentEvent::DocumentStart).unwrap();
466        collector
467            .on_event(&DocumentEvent::Paragraph(vec![crate::DocumentTextRun {
468                text: "Hello".into(),
469                ..crate::DocumentTextRun::default()
470            }]))
471            .unwrap();
472        collector.on_event(&DocumentEvent::PageBreak).unwrap();
473        collector.on_event(&DocumentEvent::DocumentEnd).unwrap();
474
475        let content = collector.into_content();
476        assert_eq!(content.blocks.len(), 2);
477        assert!(matches!(
478            content.blocks[0],
479            crate::DocumentBlock::Paragraph(_)
480        ));
481        assert!(matches!(content.blocks[1], crate::DocumentBlock::PageBreak));
482    }
483
484    #[test]
485    fn content_collector_table_and_list() {
486        let mut collector = ContentCollector::new();
487        collector
488            .on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
489            .unwrap();
490        collector
491            .on_event(&DocumentEvent::List(crate::DocumentList {
492                ordered: false,
493                start_number: None,
494                items: vec![],
495            }))
496            .unwrap();
497        let content = collector.into_content();
498        assert_eq!(content.blocks.len(), 2);
499    }
500
501    #[test]
502    fn content_collector_codeblock() {
503        let mut collector = ContentCollector::new();
504        collector
505            .on_event(&DocumentEvent::CodeBlock {
506                language: Some("rust".into()),
507                code: "fn main() {}".into(),
508            })
509            .unwrap();
510        let content = collector.into_content();
511        match &content.blocks[0] {
512            crate::DocumentBlock::CodeBlock { language, code } => {
513                assert_eq!(language.as_deref(), Some("rust"));
514                assert_eq!(code, "fn main() {}");
515            }
516            _ => panic!("expected CodeBlock"),
517        }
518    }
519
520    #[test]
521    fn content_collector_section() {
522        let mut collector = ContentCollector::new();
523        collector
524            .on_event(&DocumentEvent::Section {
525                section_type: Some("continuous".into()),
526            })
527            .unwrap();
528        let content = collector.into_content();
529        match &content.blocks[0] {
530            crate::DocumentBlock::Section {
531                blocks,
532                section_type,
533            } => {
534                assert!(blocks.is_empty());
535                assert_eq!(section_type.as_deref(), Some("continuous"));
536            }
537            _ => panic!("expected Section"),
538        }
539    }
540}
541
542#[cfg(test)]
543mod trait_coverage_tests {
544    use super::*;
545
546    struct NoopHandler;
547    impl DocWriteHandler for NoopHandler {}
548
549    #[test]
550    fn noop_handler_all_defaults() {
551        let mut h = NoopHandler;
552        assert_eq!(NoopHandler::order(), 0);
553        let ctx = DocWriteContext {
554            path: "test".into(),
555        };
556        h.before_document(&ctx).unwrap();
557        h.after_document(&ctx).unwrap();
558        let pctx = ParagraphContext { index: 0 };
559        h.before_paragraph(&pctx).unwrap();
560        h.after_paragraph(&pctx).unwrap();
561        let tctx = TableWriteContext {
562            index: 0,
563            row_count: 1,
564        };
565        h.before_table(&tctx).unwrap();
566        h.after_table(&tctx).unwrap();
567        let cctx = CellContext {
568            row: 0,
569            column: 0,
570            value: DocValue::Empty,
571        };
572        h.before_cell(&cctx).unwrap();
573        h.after_cell(&cctx).unwrap();
574    }
575
576    #[test]
577    fn read_listener_defaults() {
578        struct TestListener;
579        impl DocReadListener<String> for TestListener {
580            fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
581                Ok(())
582            }
583        }
584        let mut listener = TestListener;
585        let ctx = DocReadContext {
586            path: "test".into(),
587            index: 0,
588        };
589        assert!(listener.has_next(&ctx));
590        assert!(matches!(
591            listener.on_error(&crate::DocError::Document("x".into()), &ctx),
592            ErrorAction::Stop
593        ));
594        listener.on_complete(&ctx);
595    }
596
597    #[test]
598    fn read_listener_invoke_table_default() {
599        struct TestListener;
600        impl DocReadListener<String> for TestListener {
601            fn invoke(&mut self, _: String, _: &DocReadContext) -> crate::Result<()> {
602                Ok(())
603            }
604        }
605        let mut listener = TestListener;
606        let ctx = DocReadContext {
607            path: "test".into(),
608            index: 0,
609        };
610        let table = TableData {
611            headers: None,
612            rows: vec![],
613        };
614        listener.invoke_table(&table, &ctx).unwrap();
615    }
616
617    #[test]
618    fn content_collector_all_event_types() {
619        let mut c = ContentCollector::new();
620        c.on_event(&DocumentEvent::DocumentStart).unwrap();
621        c.on_event(&DocumentEvent::Heading {
622            level: 1,
623            runs: vec![],
624        })
625        .unwrap();
626        c.on_event(&DocumentEvent::Paragraph(vec![])).unwrap();
627        c.on_event(&DocumentEvent::Table(crate::DocumentTable { rows: vec![] }))
628            .unwrap();
629        c.on_event(&DocumentEvent::List(crate::DocumentList {
630            ordered: false,
631            start_number: None,
632            items: vec![],
633        }))
634        .unwrap();
635        c.on_event(&DocumentEvent::Image(crate::DocumentImage {
636            alt_text: None,
637            data: None,
638            extension: None,
639        }))
640        .unwrap();
641        c.on_event(&DocumentEvent::PageBreak).unwrap();
642        c.on_event(&DocumentEvent::ColumnBreak).unwrap();
643        c.on_event(&DocumentEvent::CodeBlock {
644            language: None,
645            code: String::new(),
646        })
647        .unwrap();
648        c.on_event(&DocumentEvent::Section { section_type: None })
649            .unwrap();
650        c.on_event(&DocumentEvent::DocumentEnd).unwrap();
651        c.on_complete();
652        let content = c.into_content();
653        assert_eq!(content.blocks.len(), 9); // DocumentStart/DocumentEnd produce no blocks
654    }
655
656    #[test]
657    fn content_collector_default() {
658        let c = ContentCollector::default();
659        let content = c.into_content();
660        assert!(content.blocks.is_empty());
661    }
662
663    #[test]
664    fn doc_read_context_clone_debug() {
665        let ctx = DocReadContext {
666            path: "test".into(),
667            index: 5,
668        };
669        let ctx2 = ctx.clone();
670        assert_eq!(ctx2.index, 5);
671        assert!(format!("{ctx:?}").contains("test"));
672    }
673
674    #[test]
675    fn doc_write_context_clone_debug() {
676        let ctx = DocWriteContext {
677            path: "out.docx".into(),
678        };
679        let ctx2 = ctx.clone();
680        assert_eq!(ctx2.path, "out.docx");
681        assert!(format!("{ctx:?}").contains("out.docx"));
682    }
683
684    #[test]
685    fn paragraph_context_clone_debug() {
686        let ctx = ParagraphContext { index: 3 };
687        let ctx2 = ctx.clone();
688        assert_eq!(ctx2.index, 3);
689        assert!(format!("{ctx:?}").contains('3'));
690    }
691
692    #[test]
693    fn table_write_context_clone_debug() {
694        let ctx = TableWriteContext {
695            index: 1,
696            row_count: 10,
697        };
698        let ctx2 = ctx.clone();
699        assert_eq!(ctx2.index, 1);
700        assert_eq!(ctx2.row_count, 10);
701        assert!(format!("{ctx:?}").contains("10"));
702    }
703
704    #[test]
705    fn cell_context_clone_debug() {
706        let ctx = CellContext {
707            row: 2,
708            column: 3,
709            value: DocValue::Int(42),
710        };
711        let ctx2 = ctx.clone();
712        assert_eq!(ctx2.row, 2);
713        assert!(format!("{ctx:?}").contains("42"));
714    }
715
716    #[test]
717    fn document_event_clone_debug() {
718        let events = vec![
719            DocumentEvent::DocumentStart,
720            DocumentEvent::DocumentEnd,
721            DocumentEvent::PageBreak,
722            DocumentEvent::ColumnBreak,
723            DocumentEvent::Heading {
724                level: 1,
725                runs: vec![],
726            },
727            DocumentEvent::Paragraph(vec![]),
728            DocumentEvent::Section { section_type: None },
729            DocumentEvent::CodeBlock {
730                language: None,
731                code: String::new(),
732            },
733        ];
734        for event in &events {
735            let _clone = event.clone();
736            let _debug = format!("{event:?}");
737        }
738    }
739}