Skip to main content

easydoc_core/converter/
registry.rs

1//! 基于 `TypeId` 分发的全局转换器注册表。
2//!
3//! 对标 easyexcel-core 的 `ConverterRegistry`。
4//!
5//! 注册表使用类型擦除的转换器模式,以 [`TypeId`] 为键存储异构的
6//! [`DocConverter`] 实现。这允许在运行时为任意类型注册转换器,
7//! 并在调用处无需知道具体转换器类型即可查找。
8//!
9//! 对应 Java: com.alibaba.excel.converters.ConverterRegistry
10
11use crate::error::{DocError, Result};
12use crate::metadata::TableColumn;
13use crate::traits::DocConverter;
14use crate::types::DocValue;
15use std::any::{Any, TypeId};
16use std::collections::HashMap;
17use std::marker::PhantomData;
18
19// chrono support
20use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
21
22// ---------------------------------------------------------------------------
23// Type-erased converter infrastructure
24// ---------------------------------------------------------------------------
25
26/// 类型擦除的双向转换器。
27///
28/// 此 trait 是存储抽象,允许 [`ConverterRegistry`] 在单个 `HashMap` 中持有
29/// 不同类型的转换器。由 [`ConverterRegistry::find_converter`] 和
30/// [`ConverterRegistry::find_converter_by_name`] 返回,使 derive 生成的代码
31/// 可以在不知道具体转换器类型的情况下使用已注册的转换器。
32///
33/// 大多数用户应使用 [`DocConverter<T>`]。仅在需要通过类型擦除的注册表引用
34/// 调用转换器时才使用此 trait。
35pub trait ErasedConverter: Send + Sync {
36    /// 将 Rust 值(作为 `&dyn Any` 传入)转换为 [`DocValue`]。
37    ///
38    /// # Errors
39    ///
40    /// 值的具体类型与预期类型 `T` 不匹配,或转换本身失败时返回 [`DocError::Conversion`]。
41    fn to_doc_value_erased(&self, value: &dyn Any, column: &TableColumn) -> Result<DocValue>;
42
43    /// 将 [`DocValue`] 转换回 Rust 值(作为 `Box<dyn Any>` 返回)。
44    ///
45    /// # Errors
46    ///
47    /// 值无法转换为 `T` 时返回 [`DocError::Conversion`]。
48    fn from_doc_value_erased(&self, value: &DocValue, column: &TableColumn)
49    -> Result<Box<dyn Any>>;
50}
51
52/// 将类型化的 [`DocConverter<T>`] 桥接到类型擦除的 [`ErasedConverter`] 接口的具体包装器。
53///
54/// 包装器在委托给内部转换器之前,将传入的 `&dyn Any` 值向下转型为 `&T`,
55/// 并将 `from_doc_value` 的输出装箱为 `Box<dyn Any>`。
56struct TypedConverter<T: 'static, C: DocConverter<T>> {
57    converter: C,
58    // Use `fn() -> T` to avoid inheriting T's auto-traits (Send/Sync).
59    // TypedConverter only needs Send+Sync from C, not from T.
60    _phantom: PhantomData<fn() -> T>,
61}
62
63impl<T: 'static, C: DocConverter<T>> TypedConverter<T, C> {
64    /// Wraps a concrete converter for type-erased storage.
65    fn new(converter: C) -> Self {
66        Self {
67            converter,
68            _phantom: PhantomData,
69        }
70    }
71}
72
73impl<T: 'static, C: DocConverter<T> + Send + Sync + 'static> ErasedConverter
74    for TypedConverter<T, C>
75{
76    fn to_doc_value_erased(&self, value: &dyn Any, column: &TableColumn) -> Result<DocValue> {
77        let typed = value
78            .downcast_ref::<T>()
79            .ok_or_else(|| DocError::Conversion {
80                field: column.field_name.clone(),
81                value: format!("{value:?}"),
82                message: format!(
83                    "type mismatch: expected {}, found a different concrete type",
84                    std::any::type_name::<T>()
85                ),
86            })?;
87        self.converter.to_doc_value(typed, column)
88    }
89
90    fn from_doc_value_erased(
91        &self,
92        value: &DocValue,
93        column: &TableColumn,
94    ) -> Result<Box<dyn Any>> {
95        let typed = self.converter.from_doc_value(value, column)?;
96        Ok(Box::new(typed))
97    }
98}
99
100// ---------------------------------------------------------------------------
101// ConverterRegistry
102// ---------------------------------------------------------------------------
103
104/// 持有用户注册和内置 [`DocConverter`] 实例的注册表。
105///
106/// 转换器以它们处理的 Rust 类型的 `TypeId` 为键。
107/// 注册表通常通过 builder 的 `.register_converter()` 调用填充,
108/// 然后传递给 `from_row_with_converters` / `to_row_with_converters`。
109///
110/// 对应 Java: `com.alibaba.excel.converters.ConverterRegistry`
111///
112/// # Examples
113///
114/// ```
115/// use easydoc_core::{ConverterRegistry, DocConverter, DocValue, TableColumn};
116/// use easydoc_core::Result;
117///
118/// struct BoolToString;
119///
120/// impl DocConverter<bool> for BoolToString {
121///     fn support_type() -> std::any::TypeId { std::any::TypeId::of::<bool>() }
122///     fn to_doc_value(&self, value: &bool, _col: &TableColumn) -> Result<DocValue> {
123///         Ok(DocValue::String(if *value { "yes".into() } else { "no".into() }))
124///     }
125///     fn from_doc_value(&self, value: &DocValue, col: &TableColumn) -> Result<bool> {
126///         match value {
127///             DocValue::String(s) => Ok(s == "yes"),
128///             _ => Err(easydoc_core::DocError::Conversion {
129///                 field: col.field_name.clone(),
130///                 value: format!("{value:?}"),
131///                 message: "expected string".into(),
132///             }),
133///         }
134///     }
135/// }
136///
137/// let mut registry = ConverterRegistry::new();
138/// registry.register::<bool, _>(BoolToString);
139/// assert!(registry.contains::<bool>());
140/// ```
141#[derive(Default)]
142pub struct ConverterRegistry {
143    converters: HashMap<TypeId, Box<dyn ErasedConverter>>,
144    /// Reverse index: converter type name -> `TypeId`, for name-based lookup.
145    name_to_type: HashMap<String, TypeId>,
146}
147
148impl ConverterRegistry {
149    /// 创建空注册表。
150    #[must_use]
151    pub fn new() -> Self {
152        Self {
153            converters: HashMap::new(),
154            name_to_type: HashMap::new(),
155        }
156    }
157
158    /// 为类型 `T` 注册转换器。
159    ///
160    /// 如果 `T` 的转换器已注册,则替换。返回 `true` 表示新注册,`false` 表示替换。
161    ///
162    /// 对应 Java: `ConverterRegistry#registerConverter`
163    pub fn register<T: 'static, C: DocConverter<T> + Send + Sync + 'static>(
164        &mut self,
165        converter: C,
166    ) -> bool {
167        let type_id = TypeId::of::<T>();
168        let existed = self.converters.contains_key(&type_id);
169        let erased: Box<dyn ErasedConverter> = Box::new(TypedConverter::<T, C>::new(converter));
170        self.converters.insert(type_id, erased);
171        !existed
172    }
173
174    /// 为类型 `T` 注册转换器,并以人类可读名称索引。
175    ///
176    /// 名称可用于 [`find_converter_by_name`](Self::find_converter_by_name) 在不知道具体 Rust 类型的情况下查找转换器。
177    /// 当 `#[docx(converter = StatusConverter)]` 属性在 schema 中以字符串存储
178    /// 转换器类型名时非常有用。
179    ///
180    /// 返回 `true` 表示新注册,`false` 表示替换。
181    pub fn register_named<T: 'static, C: DocConverter<T> + Send + Sync + 'static>(
182        &mut self,
183        name: &str,
184        converter: C,
185    ) -> bool {
186        let type_id = TypeId::of::<T>();
187        let existed = self.converters.contains_key(&type_id);
188        let erased: Box<dyn ErasedConverter> = Box::new(TypedConverter::<T, C>::new(converter));
189        self.converters.insert(type_id, erased);
190        self.name_to_type.insert(name.to_owned(), type_id);
191        !existed
192    }
193
194    /// 返回类型 `T` 是否已注册转换器。
195    #[must_use]
196    pub fn contains<T: 'static>(&self) -> bool {
197        self.converters.contains_key(&TypeId::of::<T>())
198    }
199
200    /// 查找类型 `T` 的类型擦除转换器。
201    ///
202    /// 未注册时返回 `None`。这是 derive 生成代码的主要查找机制。
203    #[must_use]
204    pub fn find_converter<T: 'static>(&self) -> Option<&dyn ErasedConverter> {
205        self.converters
206            .get(&TypeId::of::<T>())
207            .map(std::convert::AsRef::as_ref)
208    }
209
210    /// 按已注册名称查找类型擦除转换器。
211    ///
212    /// 未注册时返回 `None`。支持 `#[docx(converter = StatusConverter)]` 模式。
213    #[must_use]
214    pub fn find_converter_by_name(&self, name: &str) -> Option<&dyn ErasedConverter> {
215        self.name_to_type
216            .get(name)
217            .and_then(|type_id| self.converters.get(type_id))
218            .map(std::convert::AsRef::as_ref)
219    }
220
221    /// 使用已注册的转换器将 Rust 值转换为 [`DocValue`]。
222    ///
223    /// 未注册自定义转换器时回退到内置转换。
224    ///
225    /// # Errors
226    ///
227    /// 找不到合适的转换器时返回 [`DocError::Conversion`]。
228    pub fn to_doc_value<V: 'static + std::fmt::Debug>(
229        &self,
230        value: &V,
231        column: &TableColumn,
232    ) -> Result<DocValue> {
233        if let Some(converter) = self.find_converter::<V>() {
234            return converter.to_doc_value_erased(value as &dyn Any, column);
235        }
236        // Fallback: try built-in conversion via Display/Debug
237        fallback_to_doc_value(value, column)
238    }
239
240    /// 将 [`DocValue`] 转换为 Rust 类型 `V`。
241    ///
242    /// # Errors
243    ///
244    /// 找不到合适的转换器或值无法转换时返回 [`DocError::Conversion`]。
245    pub fn from_doc_value<V: 'static>(&self, value: &DocValue, column: &TableColumn) -> Result<V> {
246        if let Some(converter) = self.find_converter::<V>() {
247            let boxed = converter.from_doc_value_erased(value, column)?;
248            return boxed
249                .downcast::<V>()
250                .map(|b| *b)
251                .map_err(|_| DocError::Conversion {
252                    field: column.field_name.clone(),
253                    value: format!("{value:?}"),
254                    message: format!(
255                        "converter returned wrong concrete type for {}",
256                        std::any::type_name::<V>()
257                    ),
258                });
259        }
260        fallback_from_doc_value(value, column)
261    }
262}
263
264impl std::fmt::Debug for ConverterRegistry {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("ConverterRegistry")
267            .field("count", &self.converters.len())
268            .field("named_count", &self.name_to_type.len())
269            .finish_non_exhaustive()
270    }
271}
272
273// ---------------------------------------------------------------------------
274// Fallback conversions (used when no custom converter is registered)
275// ---------------------------------------------------------------------------
276
277/// Trait for safe fallback conversion — used when no custom converter is registered.
278///
279/// Types that implement this trait (via the blanket impl below) can be
280/// converted to/from `DocValue` using only safe code.
281trait FallbackConvert: Sized {
282    fn to_doc_value_from_ref(&self) -> DocValue;
283    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self>;
284}
285
286// Direct implementations for common types.
287impl FallbackConvert for String {
288    fn to_doc_value_from_ref(&self) -> DocValue {
289        DocValue::String(self.clone())
290    }
291    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
292        match value {
293            DocValue::String(s) => Ok(s.clone()),
294            DocValue::Int(n) => Ok(n.to_string()),
295            DocValue::Float(n) => Ok(n.to_string()),
296            DocValue::Bool(b) => Ok(b.to_string()),
297            DocValue::Empty => Ok(String::new()),
298            other => Err(DocError::Conversion {
299                field: column.field_name.clone(),
300                value: format!("{other:?}"),
301                message: "cannot convert to String".to_owned(),
302            }),
303        }
304    }
305}
306
307impl FallbackConvert for i64 {
308    fn to_doc_value_from_ref(&self) -> DocValue {
309        DocValue::Int(*self)
310    }
311    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
312        match value {
313            DocValue::Int(n) => Ok(*n),
314            DocValue::String(s) => s.parse().map_err(|_| DocError::Conversion {
315                field: column.field_name.clone(),
316                value: s.clone(),
317                message: "cannot parse as i64".to_owned(),
318            }),
319            other => Err(DocError::Conversion {
320                field: column.field_name.clone(),
321                value: format!("{other:?}"),
322                message: "cannot convert to i64".to_owned(),
323            }),
324        }
325    }
326}
327
328impl FallbackConvert for i32 {
329    fn to_doc_value_from_ref(&self) -> DocValue {
330        DocValue::Int(i64::from(*self))
331    }
332    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
333        match value {
334            DocValue::Int(n) => Ok(*n as i32),
335            DocValue::String(s) => s.parse().map_err(|_| DocError::Conversion {
336                field: column.field_name.clone(),
337                value: s.clone(),
338                message: "cannot parse as i32".to_owned(),
339            }),
340            other => Err(DocError::Conversion {
341                field: column.field_name.clone(),
342                value: format!("{other:?}"),
343                message: "cannot convert to i32".to_owned(),
344            }),
345        }
346    }
347}
348
349impl FallbackConvert for u32 {
350    fn to_doc_value_from_ref(&self) -> DocValue {
351        DocValue::Int(i64::from(*self))
352    }
353    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
354        match value {
355            DocValue::Int(n) => Ok(*n as u32),
356            DocValue::String(s) => s.parse().map_err(|_| DocError::Conversion {
357                field: column.field_name.clone(),
358                value: s.clone(),
359                message: "cannot parse as u32".to_owned(),
360            }),
361            other => Err(DocError::Conversion {
362                field: column.field_name.clone(),
363                value: format!("{other:?}"),
364                message: "cannot convert to u32".to_owned(),
365            }),
366        }
367    }
368}
369
370impl FallbackConvert for f64 {
371    fn to_doc_value_from_ref(&self) -> DocValue {
372        DocValue::Float(*self)
373    }
374    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
375        match value {
376            DocValue::Float(n) => Ok(*n),
377            DocValue::Int(n) => Ok(*n as f64),
378            DocValue::String(s) => s.parse().map_err(|_| DocError::Conversion {
379                field: column.field_name.clone(),
380                value: s.clone(),
381                message: "cannot parse as f64".to_owned(),
382            }),
383            other => Err(DocError::Conversion {
384                field: column.field_name.clone(),
385                value: format!("{other:?}"),
386                message: "cannot convert to f64".to_owned(),
387            }),
388        }
389    }
390}
391
392impl FallbackConvert for bool {
393    fn to_doc_value_from_ref(&self) -> DocValue {
394        DocValue::Bool(*self)
395    }
396    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
397        match value {
398            DocValue::Bool(b) => Ok(*b),
399            DocValue::String(s) => {
400                let lower = s.to_lowercase();
401                if lower == "true" || lower == "1" || lower == "yes" {
402                    Ok(true)
403                } else if lower == "false" || lower == "0" || lower == "no" {
404                    Ok(false)
405                } else {
406                    Err(DocError::Conversion {
407                        field: column.field_name.clone(),
408                        value: s.clone(),
409                        message: "cannot parse as bool".to_owned(),
410                    })
411                }
412            }
413            DocValue::Int(n) => Ok(*n != 0),
414            other => Err(DocError::Conversion {
415                field: column.field_name.clone(),
416                value: format!("{other:?}"),
417                message: "cannot convert to bool".to_owned(),
418            }),
419        }
420    }
421}
422
423impl FallbackConvert for DateTime<Utc> {
424    fn to_doc_value_from_ref(&self) -> DocValue {
425        DocValue::DateTime(*self)
426    }
427    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
428        match value {
429            DocValue::DateTime(dt) => Ok(*dt),
430            DocValue::String(s) => s.parse().map_err(|_| DocError::Conversion {
431                field: column.field_name.clone(),
432                value: s.clone(),
433                message: "cannot parse as DateTime<Utc>".to_owned(),
434            }),
435            other => Err(DocError::Conversion {
436                field: column.field_name.clone(),
437                value: format!("{other:?}"),
438                message: "cannot convert to DateTime<Utc>".to_owned(),
439            }),
440        }
441    }
442}
443
444impl FallbackConvert for NaiveDate {
445    fn to_doc_value_from_ref(&self) -> DocValue {
446        DocValue::Date(*self)
447    }
448    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
449        match value {
450            DocValue::Date(d) => Ok(*d),
451            DocValue::DateTime(dt) => Ok(dt.date_naive()),
452            DocValue::String(s) => {
453                NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| DocError::Conversion {
454                    field: column.field_name.clone(),
455                    value: s.clone(),
456                    message: "cannot parse as NaiveDate (expected YYYY-MM-DD)".to_owned(),
457                })
458            }
459            other => Err(DocError::Conversion {
460                field: column.field_name.clone(),
461                value: format!("{other:?}"),
462                message: "cannot convert to NaiveDate".to_owned(),
463            }),
464        }
465    }
466}
467
468impl FallbackConvert for NaiveDateTime {
469    fn to_doc_value_from_ref(&self) -> DocValue {
470        DocValue::NaiveDateTime(*self)
471    }
472    fn from_doc_value(value: &DocValue, column: &TableColumn) -> Result<Self> {
473        match value {
474            DocValue::NaiveDateTime(ndt) => Ok(*ndt),
475            DocValue::DateTime(dt) => Ok(dt.naive_utc()),
476            DocValue::String(s) => {
477                NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map_err(|_| {
478                    DocError::Conversion {
479                        field: column.field_name.clone(),
480                        value: s.clone(),
481                        message: "cannot parse as NaiveDateTime".to_owned(),
482                    }
483                })
484            }
485            other => Err(DocError::Conversion {
486                field: column.field_name.clone(),
487                value: format!("{other:?}"),
488                message: "cannot convert to NaiveDateTime".to_owned(),
489            }),
490        }
491    }
492}
493
494fn fallback_to_doc_value<V: 'static + std::fmt::Debug>(
495    value: &V,
496    _column: &TableColumn,
497) -> Result<DocValue> {
498    let type_id = TypeId::of::<V>();
499
500    if type_id == TypeId::of::<String>() {
501        // Use the FallbackConvert impl — but we need to get the value as &String
502        // Since TypeId matches, we can use Any::downcast_ref safely
503        let any_val = value as &dyn Any;
504        if let Some(s) = any_val.downcast_ref::<String>() {
505            return Ok(<String as FallbackConvert>::to_doc_value_from_ref(s));
506        }
507    }
508    if type_id == TypeId::of::<i64>() {
509        let any_val = value as &dyn Any;
510        if let Some(n) = any_val.downcast_ref::<i64>() {
511            return Ok(<i64 as FallbackConvert>::to_doc_value_from_ref(n));
512        }
513    }
514    if type_id == TypeId::of::<i32>() {
515        let any_val = value as &dyn Any;
516        if let Some(n) = any_val.downcast_ref::<i32>() {
517            return Ok(<i32 as FallbackConvert>::to_doc_value_from_ref(n));
518        }
519    }
520    if type_id == TypeId::of::<u32>() {
521        let any_val = value as &dyn Any;
522        if let Some(n) = any_val.downcast_ref::<u32>() {
523            return Ok(<u32 as FallbackConvert>::to_doc_value_from_ref(n));
524        }
525    }
526    if type_id == TypeId::of::<f64>() {
527        let any_val = value as &dyn Any;
528        if let Some(n) = any_val.downcast_ref::<f64>() {
529            return Ok(<f64 as FallbackConvert>::to_doc_value_from_ref(n));
530        }
531    }
532    if type_id == TypeId::of::<bool>() {
533        let any_val = value as &dyn Any;
534        if let Some(b) = any_val.downcast_ref::<bool>() {
535            return Ok(<bool as FallbackConvert>::to_doc_value_from_ref(b));
536        }
537    }
538    if type_id == TypeId::of::<DateTime<Utc>>() {
539        let any_val = value as &dyn Any;
540        if let Some(dt) = any_val.downcast_ref::<DateTime<Utc>>() {
541            return Ok(<DateTime<Utc> as FallbackConvert>::to_doc_value_from_ref(
542                dt,
543            ));
544        }
545    }
546    if type_id == TypeId::of::<NaiveDate>() {
547        let any_val = value as &dyn Any;
548        if let Some(d) = any_val.downcast_ref::<NaiveDate>() {
549            return Ok(<NaiveDate as FallbackConvert>::to_doc_value_from_ref(d));
550        }
551    }
552    if type_id == TypeId::of::<NaiveDateTime>() {
553        let any_val = value as &dyn Any;
554        if let Some(ndt) = any_val.downcast_ref::<NaiveDateTime>() {
555            return Ok(<NaiveDateTime as FallbackConvert>::to_doc_value_from_ref(
556                ndt,
557            ));
558        }
559    }
560
561    // Last resort: format via Debug
562    Ok(DocValue::String(format!("{value:?}")))
563}
564
565fn fallback_from_doc_value<V: 'static>(value: &DocValue, column: &TableColumn) -> Result<V> {
566    let type_id = TypeId::of::<V>();
567
568    let err = |msg: &str| -> Result<V> {
569        Err(DocError::Conversion {
570            field: column.field_name.clone(),
571            value: format!("{value:?}"),
572            message: msg.to_owned(),
573        })
574    };
575
576    if type_id == TypeId::of::<String>() {
577        let s: String = <String as FallbackConvert>::from_doc_value(value, column)?;
578        // Convert through Any — since TypeId matches, this is safe
579        let any_box: Box<dyn Any> = Box::new(s);
580        match any_box.downcast::<V>() {
581            Ok(boxed) => Ok(*boxed),
582            Err(_) => err("type mismatch for String"),
583        }
584    } else if type_id == TypeId::of::<i64>() {
585        let n: i64 = <i64 as FallbackConvert>::from_doc_value(value, column)?;
586        let any_box: Box<dyn Any> = Box::new(n);
587        match any_box.downcast::<V>() {
588            Ok(boxed) => Ok(*boxed),
589            Err(_) => err("type mismatch for i64"),
590        }
591    } else if type_id == TypeId::of::<i32>() {
592        let n: i32 = <i32 as FallbackConvert>::from_doc_value(value, column)?;
593        let any_box: Box<dyn Any> = Box::new(n);
594        match any_box.downcast::<V>() {
595            Ok(boxed) => Ok(*boxed),
596            Err(_) => err("type mismatch for i32"),
597        }
598    } else if type_id == TypeId::of::<u32>() {
599        let n: u32 = <u32 as FallbackConvert>::from_doc_value(value, column)?;
600        let any_box: Box<dyn Any> = Box::new(n);
601        match any_box.downcast::<V>() {
602            Ok(boxed) => Ok(*boxed),
603            Err(_) => err("type mismatch for u32"),
604        }
605    } else if type_id == TypeId::of::<f64>() {
606        let n: f64 = <f64 as FallbackConvert>::from_doc_value(value, column)?;
607        let any_box: Box<dyn Any> = Box::new(n);
608        match any_box.downcast::<V>() {
609            Ok(boxed) => Ok(*boxed),
610            Err(_) => err("type mismatch for f64"),
611        }
612    } else if type_id == TypeId::of::<bool>() {
613        let b: bool = <bool as FallbackConvert>::from_doc_value(value, column)?;
614        let any_box: Box<dyn Any> = Box::new(b);
615        match any_box.downcast::<V>() {
616            Ok(boxed) => Ok(*boxed),
617            Err(_) => err("type mismatch for bool"),
618        }
619    } else if type_id == TypeId::of::<DateTime<Utc>>() {
620        let dt: DateTime<Utc> = <DateTime<Utc> as FallbackConvert>::from_doc_value(value, column)?;
621        let any_box: Box<dyn Any> = Box::new(dt);
622        match any_box.downcast::<V>() {
623            Ok(boxed) => Ok(*boxed),
624            Err(_) => err("type mismatch for DateTime<Utc>"),
625        }
626    } else if type_id == TypeId::of::<NaiveDate>() {
627        let d: NaiveDate = <NaiveDate as FallbackConvert>::from_doc_value(value, column)?;
628        let any_box: Box<dyn Any> = Box::new(d);
629        match any_box.downcast::<V>() {
630            Ok(boxed) => Ok(*boxed),
631            Err(_) => err("type mismatch for NaiveDate"),
632        }
633    } else if type_id == TypeId::of::<NaiveDateTime>() {
634        let ndt: NaiveDateTime = <NaiveDateTime as FallbackConvert>::from_doc_value(value, column)?;
635        let any_box: Box<dyn Any> = Box::new(ndt);
636        match any_box.downcast::<V>() {
637            Ok(boxed) => Ok(*boxed),
638            Err(_) => err("type mismatch for NaiveDateTime"),
639        }
640    } else {
641        err(&format!(
642            "no converter registered for type {}",
643            std::any::type_name::<V>()
644        ))
645    }
646}
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::types::ImageData;
652    use chrono::{TimeZone, Utc};
653
654    fn test_column() -> TableColumn {
655        TableColumn::new("Test", "test", 0)
656    }
657
658    // -----------------------------------------------------------------------
659    // Fallback tests (unchanged from before — must keep passing)
660    // -----------------------------------------------------------------------
661
662    #[test]
663    fn empty_registry() {
664        let r = ConverterRegistry::new();
665        assert!(!r.contains::<String>());
666        let dbg = format!("{r:?}");
667        assert!(dbg.contains("count: 0"));
668    }
669
670    #[test]
671    fn fallback_string_from_string_value() {
672        let r = ConverterRegistry::new();
673        let v = DocValue::String("hello".into());
674        let result: String = r.from_doc_value(&v, &test_column()).unwrap();
675        assert_eq!(result, "hello");
676    }
677
678    #[test]
679    fn fallback_string_from_int_value() {
680        let r = ConverterRegistry::new();
681        let v = DocValue::Int(42);
682        let result: String = r.from_doc_value(&v, &test_column()).unwrap();
683        assert_eq!(result, "42");
684    }
685
686    #[test]
687    fn fallback_string_from_float_value() {
688        let r = ConverterRegistry::new();
689        let v = DocValue::Float(std::f64::consts::PI);
690        let result: String = r.from_doc_value(&v, &test_column()).unwrap();
691        assert_eq!(result, "3.141592653589793");
692    }
693
694    #[test]
695    fn fallback_string_from_bool_value() {
696        let r = ConverterRegistry::new();
697        let v = DocValue::Bool(true);
698        let result: String = r.from_doc_value(&v, &test_column()).unwrap();
699        assert_eq!(result, "true");
700    }
701
702    #[test]
703    fn fallback_string_from_empty() {
704        let r = ConverterRegistry::new();
705        let v = DocValue::Empty;
706        let result: String = r.from_doc_value(&v, &test_column()).unwrap();
707        assert_eq!(result, "");
708    }
709
710    #[test]
711    fn fallback_string_from_image_fails() {
712        let r = ConverterRegistry::new();
713        let v = DocValue::Image(ImageData {
714            bytes: vec![],
715            extension: "png".into(),
716            width: None,
717            height: None,
718            alt_text: None,
719        });
720        let result: std::result::Result<String, _> = r.from_doc_value(&v, &test_column());
721        assert!(result.is_err());
722    }
723
724    #[test]
725    fn fallback_i64_from_int() {
726        let r = ConverterRegistry::new();
727        let v = DocValue::Int(100);
728        let result: i64 = r.from_doc_value(&v, &test_column()).unwrap();
729        assert_eq!(result, 100);
730    }
731
732    #[test]
733    fn fallback_i64_from_string_parse() {
734        let r = ConverterRegistry::new();
735        let v = DocValue::String("256".into());
736        let result: i64 = r.from_doc_value(&v, &test_column()).unwrap();
737        assert_eq!(result, 256);
738    }
739
740    #[test]
741    fn fallback_i64_from_invalid_string() {
742        let r = ConverterRegistry::new();
743        let v = DocValue::String("abc".into());
744        let result: std::result::Result<i64, _> = r.from_doc_value(&v, &test_column());
745        assert!(result.is_err());
746    }
747
748    #[test]
749    fn fallback_f64_from_float() {
750        let r = ConverterRegistry::new();
751        let v = DocValue::Float(2.5);
752        let result: f64 = r.from_doc_value(&v, &test_column()).unwrap();
753        assert!((result - 2.5).abs() < f64::EPSILON);
754    }
755
756    #[test]
757    fn fallback_f64_from_string_parse() {
758        let r = ConverterRegistry::new();
759        let v = DocValue::String("1.5".into());
760        let result: f64 = r.from_doc_value(&v, &test_column()).unwrap();
761        assert!((result - 1.5).abs() < f64::EPSILON);
762    }
763
764    #[test]
765    fn fallback_bool_from_bool() {
766        let r = ConverterRegistry::new();
767        let v = DocValue::Bool(false);
768        let result: bool = r.from_doc_value(&v, &test_column()).unwrap();
769        assert!(!result);
770    }
771
772    #[test]
773    fn fallback_to_doc_value_string() {
774        let r = ConverterRegistry::new();
775        let val = String::from("test");
776        let result = r.to_doc_value(&val, &test_column()).unwrap();
777        assert!(matches!(result, DocValue::String(s) if s == "test"));
778    }
779
780    #[test]
781    fn fallback_to_doc_value_i64() {
782        let r = ConverterRegistry::new();
783        let val: i64 = 42;
784        let result = r.to_doc_value(&val, &test_column()).unwrap();
785        assert!(matches!(result, DocValue::Int(42)));
786    }
787
788    #[test]
789    fn fallback_to_doc_value_i32() {
790        let r = ConverterRegistry::new();
791        let val: i32 = 7;
792        let result = r.to_doc_value(&val, &test_column()).unwrap();
793        assert!(matches!(result, DocValue::Int(7)));
794    }
795
796    #[test]
797    fn fallback_to_doc_value_u32() {
798        let r = ConverterRegistry::new();
799        let val: u32 = 99;
800        let result = r.to_doc_value(&val, &test_column()).unwrap();
801        assert!(matches!(result, DocValue::Int(99)));
802    }
803
804    #[test]
805    fn fallback_to_doc_value_f64() {
806        let r = ConverterRegistry::new();
807        let val: f64 = 1.5;
808        let result = r.to_doc_value(&val, &test_column()).unwrap();
809        assert!(matches!(result, DocValue::Float(f) if (f - 1.5).abs() < f64::EPSILON));
810    }
811
812    #[test]
813    fn fallback_to_doc_value_bool() {
814        let r = ConverterRegistry::new();
815        let val = true;
816        let result = r.to_doc_value(&val, &test_column()).unwrap();
817        assert!(matches!(result, DocValue::Bool(true)));
818    }
819
820    #[test]
821    fn fallback_to_doc_value_datetime() {
822        let r = ConverterRegistry::new();
823        let val = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
824        let result = r.to_doc_value(&val, &test_column()).unwrap();
825        assert!(matches!(result, DocValue::DateTime(_)));
826    }
827
828    #[test]
829    fn fallback_to_doc_value_naive_date() {
830        let r = ConverterRegistry::new();
831        let val = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
832        let result = r.to_doc_value(&val, &test_column()).unwrap();
833        assert!(matches!(result, DocValue::Date(_)));
834    }
835
836    #[test]
837    fn fallback_to_doc_value_naive_datetime() {
838        let r = ConverterRegistry::new();
839        let val = NaiveDate::from_ymd_opt(2024, 6, 1)
840            .unwrap()
841            .and_hms_opt(12, 0, 0)
842            .unwrap();
843        let result = r.to_doc_value(&val, &test_column()).unwrap();
844        assert!(matches!(result, DocValue::NaiveDateTime(_)));
845    }
846
847    #[test]
848    fn fallback_i32_roundtrip() {
849        let r = ConverterRegistry::new();
850        let v = DocValue::Int(42);
851        let result: i32 = r.from_doc_value(&v, &test_column()).unwrap();
852        assert_eq!(result, 42);
853    }
854
855    #[test]
856    fn fallback_u32_roundtrip() {
857        let r = ConverterRegistry::new();
858        let v = DocValue::Int(42);
859        let result: u32 = r.from_doc_value(&v, &test_column()).unwrap();
860        assert_eq!(result, 42);
861    }
862
863    #[test]
864    fn fallback_datetime_from_string() {
865        let r = ConverterRegistry::new();
866        let v = DocValue::DateTime(Utc.timestamp_opt(1_700_000_000, 0).unwrap());
867        let result: DateTime<Utc> = r.from_doc_value(&v, &test_column()).unwrap();
868        assert_eq!(result.timestamp(), 1_700_000_000);
869    }
870
871    #[test]
872    fn fallback_naive_date_from_date() {
873        let r = ConverterRegistry::new();
874        let d = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
875        let v = DocValue::Date(d);
876        let result: NaiveDate = r.from_doc_value(&v, &test_column()).unwrap();
877        assert_eq!(result, d);
878    }
879
880    #[test]
881    fn fallback_naive_datetime_from_ndt() {
882        let r = ConverterRegistry::new();
883        let ndt = NaiveDate::from_ymd_opt(2024, 6, 1)
884            .unwrap()
885            .and_hms_opt(12, 0, 0)
886            .unwrap();
887        let v = DocValue::NaiveDateTime(ndt);
888        let result: NaiveDateTime = r.from_doc_value(&v, &test_column()).unwrap();
889        assert_eq!(result, ndt);
890    }
891
892    #[test]
893    fn fallback_i64_from_float_error() {
894        let r = ConverterRegistry::new();
895        let v = DocValue::Float(std::f64::consts::PI);
896        let result: std::result::Result<i64, _> = r.from_doc_value(&v, &test_column());
897        assert!(result.is_err());
898    }
899
900    #[test]
901    fn fallback_i64_from_bool_error() {
902        let r = ConverterRegistry::new();
903        let v = DocValue::Bool(true);
904        let result: std::result::Result<i64, _> = r.from_doc_value(&v, &test_column());
905        assert!(result.is_err());
906    }
907
908    #[test]
909    fn fallback_i64_from_empty_error() {
910        let r = ConverterRegistry::new();
911        let v = DocValue::Empty;
912        let result: std::result::Result<i64, _> = r.from_doc_value(&v, &test_column());
913        assert!(result.is_err());
914    }
915
916    #[test]
917    fn fallback_i32_from_string_parse() {
918        let r = ConverterRegistry::new();
919        let v = DocValue::String("42".into());
920        let result: i32 = r.from_doc_value(&v, &test_column()).unwrap();
921        assert_eq!(result, 42);
922    }
923
924    #[test]
925    fn fallback_i32_from_invalid_string() {
926        let r = ConverterRegistry::new();
927        let v = DocValue::String("abc".into());
928        let result: std::result::Result<i32, _> = r.from_doc_value(&v, &test_column());
929        assert!(result.is_err());
930    }
931
932    #[test]
933    fn fallback_i32_from_float_error() {
934        let r = ConverterRegistry::new();
935        let v = DocValue::Float(std::f64::consts::PI);
936        let result: std::result::Result<i32, _> = r.from_doc_value(&v, &test_column());
937        assert!(result.is_err());
938    }
939
940    #[test]
941    fn fallback_u32_from_string_parse() {
942        let r = ConverterRegistry::new();
943        let v = DocValue::String("42".into());
944        let result: u32 = r.from_doc_value(&v, &test_column()).unwrap();
945        assert_eq!(result, 42);
946    }
947
948    #[test]
949    fn fallback_u32_from_invalid_string() {
950        let r = ConverterRegistry::new();
951        let v = DocValue::String("abc".into());
952        let result: std::result::Result<u32, _> = r.from_doc_value(&v, &test_column());
953        assert!(result.is_err());
954    }
955
956    #[test]
957    fn fallback_u32_from_float_error() {
958        let r = ConverterRegistry::new();
959        let v = DocValue::Float(std::f64::consts::PI);
960        let result: std::result::Result<u32, _> = r.from_doc_value(&v, &test_column());
961        assert!(result.is_err());
962    }
963
964    #[test]
965    fn fallback_f64_from_int() {
966        let r = ConverterRegistry::new();
967        let v = DocValue::Int(42);
968        let result: f64 = r.from_doc_value(&v, &test_column()).unwrap();
969        assert!((result - 42.0).abs() < f64::EPSILON);
970    }
971
972    #[test]
973    fn fallback_f64_from_invalid_string() {
974        let r = ConverterRegistry::new();
975        let v = DocValue::String("abc".into());
976        let result: std::result::Result<f64, _> = r.from_doc_value(&v, &test_column());
977        assert!(result.is_err());
978    }
979
980    #[test]
981    fn fallback_f64_from_bool_error() {
982        let r = ConverterRegistry::new();
983        let v = DocValue::Bool(true);
984        let result: std::result::Result<f64, _> = r.from_doc_value(&v, &test_column());
985        assert!(result.is_err());
986    }
987
988    #[test]
989    fn fallback_bool_from_string_true() {
990        let r = ConverterRegistry::new();
991        for s in &["true", "True", "TRUE", "1", "yes", "Yes"] {
992            let v = DocValue::String(s.to_string());
993            let result: bool = r.from_doc_value(&v, &test_column()).unwrap();
994            assert!(result, "failed for {s}");
995        }
996    }
997
998    #[test]
999    fn fallback_bool_from_string_false() {
1000        let r = ConverterRegistry::new();
1001        for s in &["false", "False", "FALSE", "0", "no", "No"] {
1002            let v = DocValue::String(s.to_string());
1003            let result: bool = r.from_doc_value(&v, &test_column()).unwrap();
1004            assert!(!result, "failed for {s}");
1005        }
1006    }
1007
1008    #[test]
1009    fn fallback_bool_from_invalid_string() {
1010        let r = ConverterRegistry::new();
1011        let v = DocValue::String("maybe".into());
1012        let result: std::result::Result<bool, _> = r.from_doc_value(&v, &test_column());
1013        assert!(result.is_err());
1014    }
1015
1016    #[test]
1017    fn fallback_bool_from_int() {
1018        let r = ConverterRegistry::new();
1019        let v = DocValue::Int(1);
1020        let result: bool = r.from_doc_value(&v, &test_column()).unwrap();
1021        assert!(result);
1022        let v2 = DocValue::Int(0);
1023        let result2: bool = r.from_doc_value(&v2, &test_column()).unwrap();
1024        assert!(!result2);
1025    }
1026
1027    #[test]
1028    fn fallback_bool_from_float_error() {
1029        let r = ConverterRegistry::new();
1030        let v = DocValue::Float(1.0);
1031        let result: std::result::Result<bool, _> = r.from_doc_value(&v, &test_column());
1032        assert!(result.is_err());
1033    }
1034
1035    #[test]
1036    fn fallback_datetime_from_datetime() {
1037        let r = ConverterRegistry::new();
1038        let dt = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
1039        let v = DocValue::DateTime(dt);
1040        let result: DateTime<Utc> = r.from_doc_value(&v, &test_column()).unwrap();
1041        assert_eq!(result.timestamp(), 1_700_000_000);
1042    }
1043
1044    #[test]
1045    fn fallback_datetime_from_string_parse() {
1046        let r = ConverterRegistry::new();
1047        let v = DocValue::String("2023-11-14T22:13:20Z".into());
1048        let result: DateTime<Utc> = r.from_doc_value(&v, &test_column()).unwrap();
1049        assert_eq!(result.timestamp(), 1_700_000_000);
1050    }
1051
1052    #[test]
1053    fn fallback_datetime_from_invalid_string() {
1054        let r = ConverterRegistry::new();
1055        let v = DocValue::String("not-a-date".into());
1056        let result: std::result::Result<DateTime<Utc>, _> = r.from_doc_value(&v, &test_column());
1057        assert!(result.is_err());
1058    }
1059
1060    #[test]
1061    fn fallback_datetime_from_int_error() {
1062        let r = ConverterRegistry::new();
1063        let v = DocValue::Int(100);
1064        let result: std::result::Result<DateTime<Utc>, _> = r.from_doc_value(&v, &test_column());
1065        assert!(result.is_err());
1066    }
1067
1068    #[test]
1069    fn fallback_naive_date_from_string_parse() {
1070        let r = ConverterRegistry::new();
1071        let v = DocValue::String("2024-01-15".into());
1072        let result: NaiveDate = r.from_doc_value(&v, &test_column()).unwrap();
1073        assert_eq!(result, NaiveDate::from_ymd_opt(2024, 1, 15).unwrap());
1074    }
1075
1076    #[test]
1077    fn fallback_naive_date_from_datetime() {
1078        let r = ConverterRegistry::new();
1079        let dt = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
1080        let v = DocValue::DateTime(dt);
1081        let result: NaiveDate = r.from_doc_value(&v, &test_column()).unwrap();
1082        assert_eq!(result, dt.date_naive());
1083    }
1084
1085    #[test]
1086    fn fallback_naive_date_from_invalid_string() {
1087        let r = ConverterRegistry::new();
1088        let v = DocValue::String("not-a-date".into());
1089        let result: std::result::Result<NaiveDate, _> = r.from_doc_value(&v, &test_column());
1090        assert!(result.is_err());
1091    }
1092
1093    #[test]
1094    fn fallback_naive_date_from_int_error() {
1095        let r = ConverterRegistry::new();
1096        let v = DocValue::Int(100);
1097        let result: std::result::Result<NaiveDate, _> = r.from_doc_value(&v, &test_column());
1098        assert!(result.is_err());
1099    }
1100
1101    #[test]
1102    fn fallback_naive_datetime_from_string_parse() {
1103        let r = ConverterRegistry::new();
1104        let v = DocValue::String("2024-06-01 12:00:00".into());
1105        let result: NaiveDateTime = r.from_doc_value(&v, &test_column()).unwrap();
1106        let expected = NaiveDate::from_ymd_opt(2024, 6, 1)
1107            .unwrap()
1108            .and_hms_opt(12, 0, 0)
1109            .unwrap();
1110        assert_eq!(result, expected);
1111    }
1112
1113    #[test]
1114    fn fallback_naive_datetime_from_datetime() {
1115        let r = ConverterRegistry::new();
1116        let dt = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
1117        let v = DocValue::DateTime(dt);
1118        let result: NaiveDateTime = r.from_doc_value(&v, &test_column()).unwrap();
1119        assert_eq!(result, dt.naive_utc());
1120    }
1121
1122    #[test]
1123    fn fallback_naive_datetime_from_invalid_string() {
1124        let r = ConverterRegistry::new();
1125        let v = DocValue::String("not-a-datetime".into());
1126        let result: std::result::Result<NaiveDateTime, _> = r.from_doc_value(&v, &test_column());
1127        assert!(result.is_err());
1128    }
1129
1130    #[test]
1131    fn fallback_naive_datetime_from_int_error() {
1132        let r = ConverterRegistry::new();
1133        let v = DocValue::Int(100);
1134        let result: std::result::Result<NaiveDateTime, _> = r.from_doc_value(&v, &test_column());
1135        assert!(result.is_err());
1136    }
1137
1138    #[test]
1139    fn fallback_to_doc_value_debug_fallback() {
1140        let r = ConverterRegistry::new();
1141        // Vec<u8> has no FallbackConvert impl but implements Debug
1142        // so it falls back to Debug string representation
1143        let val: Vec<u8> = vec![1, 2, 3];
1144        let result = r.to_doc_value(&val, &test_column());
1145        assert!(result.is_ok());
1146        assert!(matches!(result.unwrap(), DocValue::String(s) if s.contains('1')));
1147    }
1148
1149    #[test]
1150    fn fallback_from_doc_value_unsupported_type() {
1151        let r = ConverterRegistry::new();
1152        let v = DocValue::String("test".into());
1153        // Try to convert to a type with no FallbackConvert
1154        let result: std::result::Result<Vec<u8>, _> = r.from_doc_value(&v, &test_column());
1155        assert!(result.is_err());
1156    }
1157
1158    #[test]
1159    fn registry_contains_after_register() {
1160        let r = ConverterRegistry::new();
1161        assert!(!r.contains::<String>());
1162        // Note: we can't easily register a converter without a concrete type
1163        // but we can test the contains method
1164    }
1165
1166    #[test]
1167    fn registry_debug_format() {
1168        let r = ConverterRegistry::new();
1169        let dbg = format!("{r:?}");
1170        assert!(dbg.contains("ConverterRegistry"));
1171        assert!(dbg.contains("count: 0"));
1172    }
1173
1174    #[test]
1175    fn fallback_string_from_richtext() {
1176        let r = ConverterRegistry::new();
1177        let v = DocValue::RichText(vec![]);
1178        let result: std::result::Result<String, _> = r.from_doc_value(&v, &test_column());
1179        assert!(result.is_err());
1180    }
1181
1182    #[test]
1183    fn fallback_string_from_datetime() {
1184        let r = ConverterRegistry::new();
1185        let dt = Utc.timestamp_opt(1_700_000_000, 0).unwrap();
1186        let v = DocValue::DateTime(dt);
1187        let result: std::result::Result<String, _> = r.from_doc_value(&v, &test_column());
1188        assert!(result.is_err());
1189    }
1190
1191    // -----------------------------------------------------------------------
1192    // Custom converter tests — validates the type-erased lookup pattern
1193    // -----------------------------------------------------------------------
1194
1195    /// A simple Status enum for testing custom converters.
1196    #[derive(Debug, Clone, PartialEq, Eq)]
1197    enum Status {
1198        Active,
1199        Inactive,
1200    }
1201
1202    /// Converts `Status` to/from `DocValue::String`.
1203    struct StatusConverter;
1204
1205    impl DocConverter<Status> for StatusConverter {
1206        fn support_type() -> TypeId {
1207            TypeId::of::<Status>()
1208        }
1209
1210        fn to_doc_value(&self, value: &Status, _column: &TableColumn) -> Result<DocValue> {
1211            match value {
1212                Status::Active => Ok(DocValue::String("ACTIVE".into())),
1213                Status::Inactive => Ok(DocValue::String("INACTIVE".into())),
1214            }
1215        }
1216
1217        fn from_doc_value(&self, value: &DocValue, column: &TableColumn) -> Result<Status> {
1218            match value {
1219                DocValue::String(s) => match s.as_str() {
1220                    "ACTIVE" => Ok(Status::Active),
1221                    "INACTIVE" => Ok(Status::Inactive),
1222                    _ => Err(DocError::Conversion {
1223                        field: column.field_name.clone(),
1224                        value: s.clone(),
1225                        message: "unknown status value".to_owned(),
1226                    }),
1227                },
1228                _ => Err(DocError::Conversion {
1229                    field: column.field_name.clone(),
1230                    value: format!("{value:?}"),
1231                    message: "expected string for Status".to_owned(),
1232                }),
1233            }
1234        }
1235    }
1236
1237    /// A custom i32 converter that multiplies by 10 on write and divides by 10 on read.
1238    struct TimesTenConverter;
1239
1240    impl DocConverter<i32> for TimesTenConverter {
1241        fn support_type() -> TypeId {
1242            TypeId::of::<i32>()
1243        }
1244
1245        fn to_doc_value(&self, value: &i32, _column: &TableColumn) -> Result<DocValue> {
1246            Ok(DocValue::Int(i64::from(*value * 10)))
1247        }
1248
1249        fn from_doc_value(&self, value: &DocValue, column: &TableColumn) -> Result<i32> {
1250            match value {
1251                DocValue::Int(n) => Ok((*n / 10) as i32),
1252                _ => Err(DocError::Conversion {
1253                    field: column.field_name.clone(),
1254                    value: format!("{value:?}"),
1255                    message: "expected int for i32".to_owned(),
1256                }),
1257            }
1258        }
1259    }
1260
1261    #[test]
1262    fn register_and_find_status_converter() {
1263        let mut r = ConverterRegistry::new();
1264        let is_new = r.register::<Status, _>(StatusConverter);
1265        assert!(is_new, "first registration should return true");
1266        assert!(r.contains::<Status>());
1267        assert!(r.find_converter::<Status>().is_some());
1268    }
1269
1270    #[test]
1271    fn register_returns_false_on_overwrite() {
1272        let mut r = ConverterRegistry::new();
1273        let first = r.register::<Status, _>(StatusConverter);
1274        assert!(first);
1275        let second = r.register::<Status, _>(StatusConverter);
1276        assert!(
1277            !second,
1278            "second registration should return false (overwrite)"
1279        );
1280    }
1281
1282    #[test]
1283    fn find_converter_unregistered_returns_none() {
1284        let r = ConverterRegistry::new();
1285        assert!(r.find_converter::<Status>().is_none());
1286        assert!(r.find_converter::<i64>().is_none());
1287    }
1288
1289    #[test]
1290    fn custom_converter_to_doc_value_roundtrip() {
1291        let mut r = ConverterRegistry::new();
1292        r.register::<Status, _>(StatusConverter);
1293
1294        let col = test_column();
1295
1296        // to_doc_value uses the custom converter
1297        let active_val = r.to_doc_value(&Status::Active, &col).unwrap();
1298        assert!(matches!(active_val, DocValue::String(ref s) if s == "ACTIVE"));
1299
1300        let inactive_val = r.to_doc_value(&Status::Inactive, &col).unwrap();
1301        assert!(matches!(inactive_val, DocValue::String(ref s) if s == "INACTIVE"));
1302
1303        // from_doc_value uses the custom converter
1304        let active_back: Status = r.from_doc_value(&active_val, &col).unwrap();
1305        assert_eq!(active_back, Status::Active);
1306
1307        let inactive_back: Status = r.from_doc_value(&inactive_val, &col).unwrap();
1308        assert_eq!(inactive_back, Status::Inactive);
1309    }
1310
1311    #[test]
1312    fn custom_converter_from_doc_value_error() {
1313        let mut r = ConverterRegistry::new();
1314        r.register::<Status, _>(StatusConverter);
1315
1316        let col = test_column();
1317        let bad_val = DocValue::String("UNKNOWN".into());
1318        let result: std::result::Result<Status, _> = r.from_doc_value(&bad_val, &col);
1319        assert!(result.is_err());
1320    }
1321
1322    #[test]
1323    fn custom_converter_overrides_fallback() {
1324        // Register a custom i32 converter that multiplies by 10
1325        let mut r = ConverterRegistry::new();
1326        r.register::<i32, _>(TimesTenConverter);
1327
1328        let col = test_column();
1329
1330        // to_doc_value: 7 -> Int(70) via custom converter, not Int(7) via fallback
1331        let val = r.to_doc_value(&7i32, &col).unwrap();
1332        assert!(matches!(val, DocValue::Int(70)));
1333
1334        // from_doc_value: Int(70) -> 7 via custom converter
1335        let back: i32 = r.from_doc_value(&val, &col).unwrap();
1336        assert_eq!(back, 7);
1337    }
1338
1339    #[test]
1340    fn find_converter_by_name_registered() {
1341        let mut r = ConverterRegistry::new();
1342        r.register_named::<Status, _>("StatusConverter", StatusConverter);
1343
1344        assert!(r.find_converter::<Status>().is_some());
1345        assert!(r.find_converter_by_name("StatusConverter").is_some());
1346        assert!(r.find_converter_by_name("NonExistent").is_none());
1347    }
1348
1349    #[test]
1350    fn find_converter_by_name_unregistered() {
1351        let r = ConverterRegistry::new();
1352        assert!(r.find_converter_by_name("Anything").is_none());
1353    }
1354
1355    #[test]
1356    fn register_named_returns_correct_bool() {
1357        let mut r = ConverterRegistry::new();
1358        let first = r.register_named::<Status, _>("StatusConverter", StatusConverter);
1359        assert!(first);
1360        let second = r.register_named::<Status, _>("StatusConverter", StatusConverter);
1361        assert!(!second);
1362    }
1363
1364    #[test]
1365    fn multiple_types_in_same_registry() {
1366        let mut r = ConverterRegistry::new();
1367        r.register::<Status, _>(StatusConverter);
1368        r.register_named::<i32, _>("TimesTen", TimesTenConverter);
1369
1370        assert!(r.contains::<Status>());
1371        assert!(r.contains::<i32>());
1372        assert!(!r.contains::<String>());
1373        assert!(r.find_converter_by_name("TimesTen").is_some());
1374        assert!(r.find_converter_by_name("StatusConverter").is_none()); // not named
1375    }
1376
1377    #[test]
1378    fn registry_debug_shows_count() {
1379        let mut r = ConverterRegistry::new();
1380        r.register::<Status, _>(StatusConverter);
1381        r.register::<i32, _>(TimesTenConverter);
1382
1383        let dbg = format!("{r:?}");
1384        assert!(dbg.contains("count: 2"));
1385    }
1386
1387    #[test]
1388    fn find_converter_erased_to_doc_value() {
1389        // Verify that find_converter returns a usable ErasedConverter
1390        let mut r = ConverterRegistry::new();
1391        r.register::<Status, _>(StatusConverter);
1392
1393        let col = test_column();
1394        let converter = r.find_converter::<Status>().unwrap();
1395        let val = converter
1396            .to_doc_value_erased(&Status::Active as &dyn Any, &col)
1397            .unwrap();
1398        assert!(matches!(val, DocValue::String(ref s) if s == "ACTIVE"));
1399    }
1400
1401    #[test]
1402    fn find_converter_erased_from_doc_value() {
1403        let mut r = ConverterRegistry::new();
1404        r.register::<Status, _>(StatusConverter);
1405
1406        let col = test_column();
1407        let converter = r.find_converter::<Status>().unwrap();
1408        let val = DocValue::String("INACTIVE".into());
1409        let boxed = converter.from_doc_value_erased(&val, &col).unwrap();
1410        let status = boxed.downcast::<Status>().unwrap();
1411        assert_eq!(*status, Status::Inactive);
1412    }
1413
1414    #[test]
1415    fn find_converter_by_name_roundtrip() {
1416        let mut r = ConverterRegistry::new();
1417        r.register_named::<Status, _>("StatusConverter", StatusConverter);
1418
1419        let col = test_column();
1420        let converter = r.find_converter_by_name("StatusConverter").unwrap();
1421
1422        let val = converter
1423            .to_doc_value_erased(&Status::Active as &dyn Any, &col)
1424            .unwrap();
1425        assert!(matches!(val, DocValue::String(ref s) if s == "ACTIVE"));
1426
1427        let boxed = converter.from_doc_value_erased(&val, &col).unwrap();
1428        let status = boxed.downcast::<Status>().unwrap();
1429        assert_eq!(*status, Status::Active);
1430    }
1431}