Skip to main content

sift_query_core/codecs/
mod.rs

1//! Input/result conversion contracts, registry and built-in codecs.
2use crate::{FieldMeta, RowValue, ScalarValue};
3use sift_query_algorithm::StableHasher64;
4use std::fmt;
5use std::sync::Arc;
6
7mod business;
8mod list;
9#[cfg(feature = "chrono")]
10mod temporal;
11#[cfg(feature = "chrono")]
12pub use business::DateCodec;
13#[cfg(feature = "json")]
14pub use business::JsonValueCodec;
15pub use business::{BooleanCodec, EnumCodec, ScalarTextCodec, StringCodec};
16pub use list::ListResultCodec;
17#[cfg(feature = "chrono")]
18pub use temporal::{DateTimeCodec, TimeCodec, TimestampCodec};
19
20/// A server-owned input/result conversion codec.
21pub trait Codec: Send + Sync + fmt::Debug {
22    /// Stable name used in schema and policy fingerprints.
23    fn name(&self) -> &'static str;
24
25    /// Whether this codec handles the field.
26    fn supports(&self, field: &FieldMeta) -> bool;
27
28    /// Convert one external value into the field's validated query value.
29    fn parse(&self, field: &FieldMeta, input: &str) -> Result<ScalarValue, String>;
30
31    /// Canonical RPC representation. Encoders verify that parsing this text
32    /// recovers the original database value; lossy codecs must override it.
33    fn format_input(&self, field: &FieldMeta, value: &ScalarValue) -> Result<String, String> {
34        self.format_result(field, value.clone().into())
35            .map(|value| value.to_external_text())
36    }
37
38    /// Convert one decoded result value into its public representation.
39    fn format_result(&self, _field: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
40        Ok(value)
41    }
42
43    /// Stable identity including codec configuration.
44    fn fingerprint(&self) -> u64 {
45        let mut hash = StableHasher64::default();
46        hash.write_delimited(self.name());
47        hash.finish()
48    }
49}
50
51/// Compatibility name for reflection-based field converters. Implementations
52/// use the typed [`Codec`] contract and cannot bypass bind typing.
53pub trait FieldConvertor: Codec {}
54impl<T: Codec + ?Sized> FieldConvertor for T {}
55
56/// A small, ordered collection of codecs.
57#[derive(Clone, Default)]
58pub struct CodecRegistry {
59    codecs: Vec<Arc<dyn Codec>>,
60}
61
62impl fmt::Debug for CodecRegistry {
63    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64        formatter
65            .debug_list()
66            .entries(self.codecs.iter().map(|codec| codec.name()))
67            .finish()
68    }
69}
70
71impl CodecRegistry {
72    #[must_use]
73    pub fn with_codec<C: Codec + 'static>(mut self, codec: C) -> Self {
74        self.codecs.push(Arc::new(codec));
75        self
76    }
77
78    pub fn push<C: Codec + 'static>(&mut self, codec: C) {
79        self.codecs.push(Arc::new(codec));
80    }
81
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.codecs.is_empty()
85    }
86
87    pub fn iter(&self) -> impl Iterator<Item = &Arc<dyn Codec>> {
88        self.codecs.iter()
89    }
90
91    pub(crate) fn find(&self, field: &FieldMeta) -> Option<&dyn Codec> {
92        self.codecs
93            .iter()
94            .find(|codec| codec.supports(field))
95            .map(Arc::as_ref)
96    }
97
98    pub(crate) fn find_arc(&self, field: &FieldMeta) -> Option<Arc<dyn Codec>> {
99        self.codecs
100            .iter()
101            .find(|codec| codec.supports(field))
102            .cloned()
103    }
104
105    pub(crate) fn fingerprint(&self) -> u64 {
106        let mut hash = StableHasher64::default();
107        for codec in &self.codecs {
108            hash.write(codec.fingerprint().to_le_bytes());
109        }
110        hash.finish()
111    }
112}