1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum SortOrder {
14 #[default]
16 None,
17 Ascending,
19 Descending,
21}
22
23impl std::fmt::Display for SortOrder {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::None => write!(f, ""),
27 Self::Ascending => write!(f, "ascending"),
28 Self::Descending => write!(f, "descending"),
29 }
30 }
31}
32
33impl SortOrder {
34 pub const fn toggled(self) -> Self {
37 match self {
38 Self::None => Self::Ascending,
39 Self::Ascending => Self::Descending,
40 Self::Descending => Self::Ascending,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct QbeField {
48 pub name: String,
52 pub shown: bool,
54 pub sort: SortOrder,
56 pub criteria: Vec<String>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct QueryDef {
63 pub name: String,
65 pub table: String,
67 pub fields: Vec<QbeField>,
72}
73
74impl QueryDef {
75 pub fn new(name: impl Into<String>, table: impl Into<String>) -> Self {
77 Self {
78 name: name.into(),
79 table: table.into(),
80 fields: Vec::new(),
81 }
82 }
83
84 pub fn set_cell(&mut self, row: QbeRow, col: usize, value: QbeValue) -> Result<(), QueryError> {
86 match row {
87 QbeRow::Table => match value {
88 QbeValue::Text(table) => {
89 self.table = table;
90 Ok(())
91 }
92 QbeValue::Bool(_) => Err(QueryError::InvalidCell {
93 detail: "the Table row takes a name".to_owned(),
94 }),
95 },
96 QbeRow::Field => match value {
97 QbeValue::Text(name) => {
98 if col == self.fields.len() {
99 if self
100 .fields
101 .iter()
102 .any(|f| f.name.eq_ignore_ascii_case(&name))
103 {
104 return Err(QueryError::DuplicateField { field: name });
105 }
106 self.fields.push(QbeField {
107 name,
108 shown: true,
109 sort: SortOrder::None,
110 criteria: Vec::new(),
111 });
112 return Ok(());
113 }
114 if self
115 .fields
116 .iter()
117 .enumerate()
118 .any(|(i, f)| i != col && f.name.eq_ignore_ascii_case(&name))
119 {
120 return Err(QueryError::DuplicateField { field: name });
121 }
122 let field = self.fields.get_mut(col).ok_or(QueryError::InvalidCell {
123 detail: "columns extend one at a time".to_owned(),
124 })?;
125 field.name = name;
126 Ok(())
127 }
128 QbeValue::Bool(_) => Err(QueryError::InvalidCell {
129 detail: "the Field row takes a name".to_owned(),
130 }),
131 },
132 QbeRow::Show => {
133 let field = self.fields.get_mut(col).ok_or(QueryError::InvalidCell {
134 detail: "no column at that position".to_owned(),
135 })?;
136 match value {
137 QbeValue::Bool(shown) => {
138 field.shown = shown;
139 Ok(())
140 }
141 QbeValue::Text(_) => Err(QueryError::InvalidCell {
142 detail: "the Show row takes a checkbox".to_owned(),
143 }),
144 }
145 }
146 QbeRow::Sort => {
147 let field = self.fields.get_mut(col).ok_or(QueryError::InvalidCell {
148 detail: "no column at that position".to_owned(),
149 })?;
150 match value {
151 QbeValue::Text(text) => {
152 field.sort = match text.as_str() {
153 "" => SortOrder::None,
154 "ascending" => SortOrder::Ascending,
155 "descending" => SortOrder::Descending,
156 other => {
157 return Err(QueryError::InvalidCell {
158 detail: format!(
159 "sort must be ascending, descending or empty, found {other:?}"
160 ),
161 });
162 }
163 };
164 Ok(())
165 }
166 QbeValue::Bool(_) => Err(QueryError::InvalidCell {
167 detail: "the Sort row takes text".to_owned(),
168 }),
169 }
170 }
171 QbeRow::Criteria(or_row) => {
172 let field = self.fields.get_mut(col).ok_or(QueryError::InvalidCell {
173 detail: "no column at that position".to_owned(),
174 })?;
175 let QbeValue::Text(text) = value else {
176 return Err(QueryError::InvalidCell {
177 detail: "the Criteria row takes text".to_owned(),
178 });
179 };
180 if text.is_empty() {
181 return Err(QueryError::InvalidCell {
182 detail: "criteria must not be empty".to_owned(),
183 });
184 }
185 if or_row == field.criteria.len() {
186 field.criteria.push(text);
187 } else if let Some(slot) = field.criteria.get_mut(or_row) {
188 *slot = text;
189 } else {
190 return Err(QueryError::InvalidCell {
191 detail: "criteria rows fill in order".to_owned(),
192 });
193 }
194 Ok(())
195 }
196 }
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum QbeRow {
204 Table,
206 Field,
208 Show,
210 Sort,
212 Criteria(usize),
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
219pub enum QbeValue {
220 Text(String),
222 Bool(bool),
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
229pub enum QueryError {
230 DuplicateField {
232 field: String,
234 },
235 InvalidCell {
237 detail: String,
239 },
240}
241
242impl std::fmt::Display for QueryError {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 match self {
245 Self::DuplicateField { field } => {
246 write!(
247 f,
248 "duplicate query column {field:?}: columns are case-insensitive"
249 )
250 }
251 Self::InvalidCell { detail } => write!(f, "invalid cell: {detail}"),
252 }
253 }
254}
255
256impl std::error::Error for QueryError {}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn field(name: &str) -> QbeField {
263 QbeField {
264 name: name.to_owned(),
265 shown: true,
266 sort: SortOrder::None,
267 criteria: Vec::new(),
268 }
269 }
270
271 #[test]
273 fn cells_reject_the_wrong_value_type() {
274 let mut def = QueryDef::new("q", "t");
275 def.fields.push(field("a"));
276 let text = QbeValue::Text("x".into());
277 let bool_ = QbeValue::Bool(true);
278
279 assert!(def.set_cell(QbeRow::Table, 0, bool_.clone()).is_err());
280 assert!(def.set_cell(QbeRow::Field, 0, bool_.clone()).is_err());
281 assert!(def.set_cell(QbeRow::Show, 0, text).is_err());
282 assert!(def.set_cell(QbeRow::Sort, 0, bool_.clone()).is_err());
283 assert!(def.set_cell(QbeRow::Criteria(0), 0, bool_).is_err());
284 }
285
286 #[test]
289 fn cells_beyond_the_grid_are_errors() {
290 let mut def = QueryDef::new("q", "t");
291 assert!(
292 def.set_cell(QbeRow::Field, 1, QbeValue::Text("a".into()))
293 .is_err()
294 );
295 assert!(def.set_cell(QbeRow::Show, 0, QbeValue::Bool(true)).is_err());
296 assert!(
297 def.set_cell(QbeRow::Sort, 0, QbeValue::Text("".into()))
298 .is_err()
299 );
300 assert!(
301 def.set_cell(QbeRow::Criteria(0), 0, QbeValue::Text("x".into()))
302 .is_err()
303 );
304 }
305
306 #[test]
308 fn sort_texts_parse_or_reject() {
309 let mut def = QueryDef::new("q", "t");
310 def.fields.push(field("a"));
311 for (text, expected) in [
312 ("ascending", SortOrder::Ascending),
313 ("descending", SortOrder::Descending),
314 ("", SortOrder::None),
315 ] {
316 def.set_cell(QbeRow::Sort, 0, QbeValue::Text(text.into()))
317 .expect("valid sort text");
318 assert_eq!(def.fields[0].sort, expected);
319 }
320 assert!(
321 def.set_cell(QbeRow::Sort, 0, QbeValue::Text("x".into()))
322 .is_err()
323 );
324 assert_eq!(def.fields[0].sort.to_string(), "", "none renders empty");
325 }
326
327 #[test]
329 fn criteria_rows_fill_in_order_and_overwrite() {
330 let mut def = QueryDef::new("q", "t");
331 def.fields.push(field("a"));
332 def.set_cell(QbeRow::Criteria(0), 0, QbeValue::Text("1".into()))
333 .unwrap();
334 def.set_cell(QbeRow::Criteria(1), 0, QbeValue::Text("2".into()))
335 .unwrap();
336 assert!(
337 def.set_cell(QbeRow::Criteria(3), 0, QbeValue::Text("4".into()))
338 .is_err()
339 );
340 assert_eq!(def.fields[0].criteria, ["1", "2"]);
341 assert!(
342 def.set_cell(QbeRow::Criteria(0), 0, QbeValue::Text("9".into()))
343 .is_ok()
344 );
345 assert_eq!(def.fields[0].criteria, ["9", "2"]);
346 }
347
348 #[test]
350 fn query_error_displays_are_precise() {
351 let dup = QueryError::DuplicateField { field: "a".into() };
352 assert!(dup.to_string().contains("a"));
353 assert!(dup.to_string().contains("duplicate"));
354 let invalid = QueryError::InvalidCell {
355 detail: "no column".into(),
356 };
357 assert!(invalid.to_string().contains("no column"));
358 }
359
360 #[test]
363 fn sort_toggle_cycles_ascending_and_descending() {
364 assert_eq!(SortOrder::None.toggled(), SortOrder::Ascending);
365 assert_eq!(SortOrder::Ascending.toggled(), SortOrder::Descending);
366 assert_eq!(SortOrder::Descending.toggled(), SortOrder::Ascending);
367 }
368
369 #[test]
371 fn sort_display_and_wrong_cell_types_are_precise() {
372 assert_eq!(SortOrder::None.to_string(), "");
373 assert_eq!(SortOrder::Ascending.to_string(), "ascending");
374 assert_eq!(SortOrder::Descending.to_string(), "descending");
375 let mut def = QueryDef::new("q", "t");
376 def.fields.push(field("a"));
377 assert!(
378 def.set_cell(QbeRow::Show, 0, QbeValue::Text("x".into()))
379 .is_err()
380 );
381 assert!(def.set_cell(QbeRow::Sort, 0, QbeValue::Bool(true)).is_err());
382 }
383
384 #[test]
388 fn field_row_rejects_duplicate_names_and_out_of_range_columns() {
389 let mut def = QueryDef::new("q", "t");
390 def.fields.push(field("a"));
391 def.fields.push(field("b"));
392 assert_eq!(
393 def.set_cell(QbeRow::Field, 0, QbeValue::Text("B".into())),
394 Err(QueryError::DuplicateField { field: "B".into() })
395 );
396 assert_eq!(
397 def.set_cell(QbeRow::Field, 5, QbeValue::Text("c".into())),
398 Err(QueryError::InvalidCell {
399 detail: "columns extend one at a time".to_owned(),
400 })
401 );
402 assert_eq!(
403 def.set_cell(QbeRow::Field, 0, QbeValue::Text("A".into())),
404 Ok(()),
405 "renaming a column to its own name (case-insensitive) is not a duplicate"
406 );
407 assert_eq!(def.fields[0].name, "A");
408 assert_eq!(def.fields[1].name, "b");
409 }
410}