Skip to main content

sift_query_core/codecs/
business.rs

1use super::Codec;
2use crate::{ConfigError, FieldMeta, RowValue, ScalarValue, ValueType};
3use sift_query_algorithm::StableHasher64;
4
5/// A strict text field codec with optional whitespace normalization. It is
6/// useful when an API's representation must be explicit even though the
7/// underlying value is already a SQL text column.
8#[derive(Debug, Clone, Copy)]
9pub struct StringCodec {
10    pub name: &'static str,
11    pub field: &'static str,
12    pub trim: bool,
13}
14
15impl Codec for StringCodec {
16    fn name(&self) -> &'static str {
17        self.name
18    }
19    fn supports(&self, field: &FieldMeta) -> bool {
20        field.public_name == self.field && field.value_type == ValueType::Text
21    }
22    fn parse(&self, _: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
23        Ok(ScalarValue::Text(
24            if self.trim { input.trim() } else { input }.to_owned(),
25        ))
26    }
27    fn format_result(&self, _: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
28        match value {
29            RowValue::Null => Ok(RowValue::Null),
30            RowValue::Text(value) => Ok(RowValue::Text(
31                if self.trim { value.trim() } else { &value }.to_owned(),
32            )),
33            _ => Err("string result has an incompatible type".to_owned()),
34        }
35    }
36    fn fingerprint(&self) -> u64 {
37        let mut hash = StableHasher64::default();
38        hash.write_delimited(self.name);
39        hash.write_delimited(self.field);
40        hash.write([u8::from(self.trim)]);
41        hash.finish()
42    }
43}
44
45/// Configurable text representation for a scalar field. Parsing and result
46/// conversion use the same logical type, so cursor and database binds remain
47/// typed while the public text format is centralized in one codec.
48#[derive(Debug, Clone, Copy)]
49pub struct ScalarTextCodec {
50    pub name: &'static str,
51    pub field: &'static str,
52    pub value_type: ValueType,
53    pub trim: bool,
54}
55
56impl Codec for ScalarTextCodec {
57    fn name(&self) -> &'static str {
58        self.name
59    }
60    fn supports(&self, field: &FieldMeta) -> bool {
61        field.public_name == self.field && field.value_type == self.value_type
62    }
63    fn parse(&self, _: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
64        ScalarValue::parse(
65            self.value_type,
66            if self.trim { input.trim() } else { input },
67            self.field,
68        )
69        .map_err(|error| error.to_string())
70    }
71    fn format_result(&self, _: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
72        if matches!(value, RowValue::Null) {
73            return Ok(RowValue::Null);
74        }
75        if value.value_type() != Some(self.value_type) {
76            return Err("scalar result has an incompatible type".to_owned());
77        }
78        Ok(RowValue::Text(value.to_external_text()))
79    }
80    fn format_input(&self, _: &FieldMeta, value: &ScalarValue) -> Result<String, String> {
81        if value.value_type() != self.value_type {
82            return Err("scalar input has an incompatible type".to_owned());
83        }
84        Ok(value.to_external_text())
85    }
86    fn fingerprint(&self) -> u64 {
87        let mut hash = StableHasher64::default();
88        hash.write_delimited(self.name);
89        hash.write_delimited(self.field);
90        hash.write_delimited(format!("{:?}", self.value_type));
91        hash.write([u8::from(self.trim)]);
92        hash.finish()
93    }
94}
95
96#[cfg(feature = "json")]
97#[derive(Debug, Clone, Copy)]
98pub struct JsonValueCodec {
99    pub name: &'static str,
100    pub field: &'static str,
101    pub max_bytes: usize,
102}
103
104#[cfg(feature = "json")]
105impl Codec for JsonValueCodec {
106    fn name(&self) -> &'static str {
107        self.name
108    }
109    fn supports(&self, field: &FieldMeta) -> bool {
110        field.public_name == self.field && field.value_type == ValueType::Json
111    }
112    fn parse(&self, _: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
113        if self.max_bytes == 0 || input.len() > self.max_bytes {
114            return Err("JSON input exceeds codec budget".to_owned());
115        }
116        serde_json::from_str(input)
117            .map(|value| ScalarValue::Json(Box::new(value)))
118            .map_err(|error| format!("invalid JSON: {error}"))
119    }
120    fn format_result(&self, _: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
121        match value {
122            RowValue::Null => Ok(RowValue::Null),
123            RowValue::Json(value) => {
124                let text = value.to_string();
125                if self.max_bytes == 0 || text.len() > self.max_bytes {
126                    return Err("JSON result exceeds codec budget".to_owned());
127                }
128                Ok(RowValue::Text(text))
129            }
130            _ => Err("JSON result has an incompatible type".to_owned()),
131        }
132    }
133    fn format_input(&self, _: &FieldMeta, value: &ScalarValue) -> Result<String, String> {
134        match value {
135            ScalarValue::Json(value) => Ok(value.to_string()),
136            _ => Err("JSON input has an incompatible type".to_owned()),
137        }
138    }
139    fn fingerprint(&self) -> u64 {
140        let mut hash = StableHasher64::default();
141        hash.write_delimited(self.name);
142        hash.write_delimited(self.field);
143        hash.write_delimited(self.max_bytes.to_string());
144        hash.finish()
145    }
146}
147
148/// Strict enum names mapped to an integer database field. Empty and unknown
149/// names/codes are errors; results use the same names accepted by input.
150#[derive(Debug, Clone)]
151pub struct EnumCodec {
152    name: &'static str,
153    field: &'static str,
154    variants: Vec<(&'static str, i32)>,
155}
156
157impl EnumCodec {
158    pub fn new(
159        name: &'static str,
160        field: &'static str,
161        variants: impl IntoIterator<Item = (&'static str, i32)>,
162    ) -> Result<Self, ConfigError> {
163        let variants = variants.into_iter().collect::<Vec<_>>();
164        if variants.is_empty()
165            || variants.iter().enumerate().any(|(index, (name, code))| {
166                name.is_empty()
167                    || variants[..index]
168                        .iter()
169                        .any(|(other, value)| other == name || value == code)
170            })
171        {
172            return Err(ConfigError::Invalid(
173                "enum codec requires unique nonempty names and codes".to_owned(),
174            ));
175        }
176        Ok(Self {
177            name,
178            field,
179            variants,
180        })
181    }
182}
183
184impl Codec for EnumCodec {
185    fn name(&self) -> &'static str {
186        self.name
187    }
188    fn supports(&self, field: &FieldMeta) -> bool {
189        field.public_name == self.field && field.value_type == ValueType::I32
190    }
191    fn parse(&self, _field: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
192        self.variants
193            .iter()
194            .find(|(name, _)| *name == input)
195            .map(|(_, value)| ScalarValue::I32(*value))
196            .ok_or_else(|| "unknown enum name".to_owned())
197    }
198    fn format_result(&self, _field: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
199        match value {
200            RowValue::Null => Ok(RowValue::Null),
201            RowValue::I32(value) => self
202                .variants
203                .iter()
204                .find(|(_, code)| *code == value)
205                .map(|(name, _)| RowValue::Text((*name).to_owned()))
206                .ok_or_else(|| "unknown enum code".to_owned()),
207            _ => Err("enum result has an incompatible type".to_owned()),
208        }
209    }
210    fn fingerprint(&self) -> u64 {
211        let mut hash = StableHasher64::default();
212        hash.write_delimited(self.name);
213        hash.write_delimited(self.field);
214        for (name, code) in &self.variants {
215            hash.write_delimited(name);
216            hash.write(code.to_le_bytes());
217        }
218        hash.finish()
219    }
220}
221
222/// A strict pair of business boolean names (for example `enabled`/`disabled`).
223#[derive(Debug, Clone, Copy)]
224pub struct BooleanCodec {
225    pub name: &'static str,
226    pub field: &'static str,
227    pub true_name: &'static str,
228    pub false_name: &'static str,
229}
230
231impl Codec for BooleanCodec {
232    fn name(&self) -> &'static str {
233        self.name
234    }
235    fn supports(&self, field: &FieldMeta) -> bool {
236        field.public_name == self.field && field.value_type == ValueType::Bool
237    }
238    fn parse(&self, _field: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
239        if self.true_name == self.false_name
240            || self.true_name.is_empty()
241            || self.false_name.is_empty()
242        {
243            return Err("invalid boolean codec configuration".to_owned());
244        }
245        if input == self.true_name {
246            Ok(ScalarValue::Bool(true))
247        } else if input == self.false_name {
248            Ok(ScalarValue::Bool(false))
249        } else {
250            Err("unknown boolean name".to_owned())
251        }
252    }
253    fn format_result(&self, _field: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
254        match value {
255            RowValue::Null => Ok(RowValue::Null),
256            RowValue::Bool(value) => Ok(RowValue::Text(
257                if value {
258                    self.true_name
259                } else {
260                    self.false_name
261                }
262                .to_owned(),
263            )),
264            _ => Err("boolean result has an incompatible type".to_owned()),
265        }
266    }
267    fn fingerprint(&self) -> u64 {
268        let mut hash = StableHasher64::default();
269        for part in [self.name, self.field, self.true_name, self.false_name] {
270            hash.write_delimited(part);
271        }
272        hash.finish()
273    }
274}
275
276/// A configured date-only format. This codec has no implicit timezone;
277/// use `TimestampCodec` for configured timezone conversion.
278#[cfg(feature = "chrono")]
279#[derive(Debug, Clone, Copy)]
280pub struct DateCodec {
281    pub name: &'static str,
282    pub field: &'static str,
283    pub format: &'static str,
284}
285
286#[cfg(feature = "chrono")]
287impl Codec for DateCodec {
288    fn name(&self) -> &'static str {
289        self.name
290    }
291    fn supports(&self, field: &FieldMeta) -> bool {
292        field.public_name == self.field && field.value_type == ValueType::Date
293    }
294    fn parse(&self, _field: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
295        chrono::NaiveDate::parse_from_str(input, self.format)
296            .map(ScalarValue::Date)
297            .map_err(|_| "invalid date".to_owned())
298    }
299    fn format_result(&self, _field: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
300        match value {
301            RowValue::Null => Ok(RowValue::Null),
302            RowValue::Date(value) => {
303                use std::fmt::Write;
304                let mut output = String::new();
305                write!(&mut output, "{}", value.format(self.format))
306                    .map_err(|_| "invalid date format".to_owned())?;
307                Ok(RowValue::Text(output))
308            }
309            _ => Err("date result has an incompatible type".to_owned()),
310        }
311    }
312    fn fingerprint(&self) -> u64 {
313        let mut hash = StableHasher64::default();
314        for part in [self.name, self.field, self.format] {
315            hash.write_delimited(part);
316        }
317        hash.finish()
318    }
319}