sift_query_core/query/
summary.rs1use std::marker::PhantomData;
2
3use crate::{ConfigError, FieldId, FieldRef, ParamError, SearchEntity, SummableKind, ValueType};
4
5#[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}