Skip to main content

sift_query_core/query/
summary.rs

1use std::marker::PhantomData;
2
3use crate::{ConfigError, FieldId, FieldRef, ParamError, SearchEntity, SummableKind, ValueType};
4
5/// Trusted server-side aggregate expression containing a `{field}` placeholder.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize))]
8pub struct CustomAggregate {
9    name: &'static str,
10    template: &'static str,
11    output_type: ValueType,
12}
13
14impl CustomAggregate {
15    pub fn new(
16        name: &'static str,
17        template: &'static str,
18        output_type: ValueType,
19    ) -> Result<Self, ConfigError> {
20        if name.is_empty()
21            || !name
22                .bytes()
23                .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
24        {
25            return Err(ConfigError::Invalid(
26                "custom aggregate name must be ASCII alphanumeric or underscore".to_owned(),
27            ));
28        }
29        let remainder = template.replace("{field}", "");
30        if !template.contains("{field}")
31            || remainder.contains(['{', '}'])
32            || template.contains(';')
33            || template.contains("--")
34            || template.contains("/*")
35            || template.contains('$')
36        {
37            return Err(ConfigError::Invalid(
38                "custom aggregate template contains a forbidden token".to_owned(),
39            ));
40        }
41        Ok(Self {
42            name,
43            template,
44            output_type,
45        })
46    }
47
48    #[must_use]
49    pub const fn name(self) -> &'static str {
50        self.name
51    }
52
53    #[must_use]
54    pub const fn template(self) -> &'static str {
55        self.template
56    }
57
58    #[must_use]
59    pub const fn output_type(self) -> ValueType {
60        self.output_type
61    }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize))]
66pub enum AggregateFn {
67    Sum,
68    Avg,
69    Min,
70    Max,
71    Custom(CustomAggregate),
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct SummaryRequest {
76    pub field: FieldId,
77    pub function: AggregateFn,
78    pub input_type: ValueType,
79}
80
81#[derive(Debug)]
82pub struct SearchOptions<E: SearchEntity> {
83    requests: Vec<SummaryRequest>,
84    error: Option<ParamError>,
85    _entity: PhantomData<fn() -> E>,
86}
87
88impl<E: SearchEntity> SearchOptions<E> {
89    #[must_use]
90    pub fn builder() -> Self {
91        Self {
92            requests: Vec::new(),
93            error: None,
94            _entity: PhantomData,
95        }
96    }
97
98    #[must_use]
99    pub fn sum<T, K: SummableKind>(self, field: FieldRef<E, T, K>) -> Self {
100        self.push(field.id(), AggregateFn::Sum)
101    }
102
103    #[must_use]
104    pub fn avg<T, K: SummableKind>(self, field: FieldRef<E, T, K>) -> Self {
105        self.push(field.id(), AggregateFn::Avg)
106    }
107
108    #[must_use]
109    pub fn min<T, K: SummableKind>(self, field: FieldRef<E, T, K>) -> Self {
110        self.push(field.id(), AggregateFn::Min)
111    }
112
113    #[must_use]
114    pub fn max<T, K: SummableKind>(self, field: FieldRef<E, T, K>) -> Self {
115        self.push(field.id(), AggregateFn::Max)
116    }
117
118    #[must_use]
119    pub fn custom<T, K: SummableKind>(
120        self,
121        field: FieldRef<E, T, K>,
122        aggregate: CustomAggregate,
123    ) -> Self {
124        self.push(field.id(), AggregateFn::Custom(aggregate))
125    }
126
127    fn push(mut self, field_id: FieldId, function: AggregateFn) -> Self {
128        if self.requests.len() >= 8 {
129            self.error = Some(ParamError::TooComplex("summary count"));
130        } else if self
131            .requests
132            .iter()
133            .any(|request| request.field == field_id && request.function == function)
134        {
135            self.error = Some(ParamError::DuplicateParameter("summary".to_owned()));
136        } else if let Some(meta) = E::META.field(field_id) {
137            self.requests.push(SummaryRequest {
138                field: field_id,
139                function,
140                input_type: meta.value_type,
141            });
142        } else {
143            self.error = Some(ParamError::UnknownField(format!("#{}", field_id.0)));
144        }
145        self
146    }
147
148    pub fn build(self) -> Result<Self, ParamError> {
149        if let Some(error) = self.error {
150            Err(error)
151        } else {
152            Ok(self)
153        }
154    }
155
156    #[must_use]
157    pub fn requests(&self) -> &[SummaryRequest] {
158        &self.requests
159    }
160}