Skip to main content

sift_query_core/codecs/
list.rs

1use crate::{Codec, ConfigError, FieldMeta, RowValue, ScalarValue, ValueType};
2use sift_query_algorithm::StableHasher64;
3
4/// Literal separated database text -> typed presentation list. This does not
5/// add PostgreSQL array-column operators or change the database bind type.
6#[derive(Debug, Clone)]
7pub struct ListResultCodec {
8    name: &'static str,
9    field: &'static str,
10    separator: &'static str,
11    item_type: ValueType,
12    max_items: usize,
13}
14impl ListResultCodec {
15    pub fn new(
16        name: &'static str,
17        field: &'static str,
18        separator: &'static str,
19        item_type: ValueType,
20        max_items: usize,
21    ) -> Result<Self, ConfigError> {
22        if separator.is_empty() || separator.len() > 32 || max_items == 0 || max_items > 16384 {
23            return Err(ConfigError::Invalid(
24                "invalid list separator or item budget".to_owned(),
25            ));
26        }
27        #[cfg(feature = "json")]
28        if item_type == ValueType::Json {
29            return Err(ConfigError::Invalid(
30                "list items must be scalar values".to_owned(),
31            ));
32        }
33        Ok(Self {
34            name,
35            field,
36            separator,
37            item_type,
38            max_items,
39        })
40    }
41}
42impl Codec for ListResultCodec {
43    fn name(&self) -> &'static str {
44        self.name
45    }
46    fn supports(&self, field: &FieldMeta) -> bool {
47        field.public_name == self.field && field.value_type == ValueType::Text
48    }
49    fn parse(&self, _: &FieldMeta, input: &str) -> Result<ScalarValue, String> {
50        Ok(ScalarValue::Text(input.to_owned()))
51    }
52    fn format_input(&self, _: &FieldMeta, value: &ScalarValue) -> Result<String, String> {
53        Ok(value.to_external_text())
54    }
55    fn format_result(&self, _: &FieldMeta, value: RowValue) -> Result<RowValue, String> {
56        let text = match value {
57            RowValue::Null => return Ok(RowValue::Null),
58            RowValue::Text(text) => text,
59            _ => return Err("list result needs a text field".to_owned()),
60        };
61        let mut items = Vec::new();
62        if !text.trim().is_empty() {
63            for part in text.split(self.separator) {
64                if items.len() >= self.max_items {
65                    return Err("list item budget exceeded".to_owned());
66                }
67                items.push(
68                    ScalarValue::parse(self.item_type, part, self.field)
69                        .map(RowValue::from)
70                        .map_err(|_| "invalid list item".to_owned())?,
71                );
72            }
73        }
74        Ok(RowValue::List(items))
75    }
76    fn fingerprint(&self) -> u64 {
77        let mut hash = StableHasher64::default();
78        for part in [self.name, self.field, self.separator] {
79            hash.write_delimited(part);
80        }
81        hash.write_delimited(format!("{:?}", self.item_type));
82        hash.write_delimited(self.max_items.to_string());
83        hash.finish()
84    }
85}