Skip to main content

rango_store/
spec.rs

1use std::sync::Arc;
2
3use crate::{BoxFuture, Gather, Reader, Show, Storable, Store, StoreError, Value, Widget, Writer};
4
5/// A table name.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct Table(pub &'static str);
8
9/// A column name.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct Name(pub &'static str);
12
13impl Table {
14    /// The underlying `&'static str`.
15    pub fn as_str(&self) -> &'static str {
16        self.0
17    }
18}
19
20impl Name {
21    /// The underlying `&'static str`.
22    pub fn as_str(&self) -> &'static str {
23        self.0
24    }
25}
26
27impl std::fmt::Display for Table {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.write_str(self.0)
30    }
31}
32
33impl std::fmt::Display for Name {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.write_str(self.0)
36    }
37}
38
39/// A fixed set of (value, label) choices for a field.
40#[derive(Clone, Debug, PartialEq)]
41pub struct Pick {
42    /// Allowed choices; first element is the stored value.
43    pub options: &'static [(&'static str, &'static str)],
44}
45
46/// A named checkbox constraint.
47#[derive(Clone, Debug, PartialEq)]
48pub struct Check(pub &'static str);
49
50/// Reference between tables.
51#[derive(Clone, Debug, PartialEq)]
52pub enum Link {
53    /// Direct reference to a column of another table.
54    To(Table, Name),
55    /// Many-to-many through a join table: table, our column, their column.
56    Via(Table, Name, Name),
57}
58
59/// Repository-level constraint attached to a [`Schema`].
60#[derive(Clone, Debug, PartialEq)]
61pub enum Rule {
62    /// The named columns must hold equal values across related rows.
63    Same(Vec<Name>),
64    /// The named column keeps a reference shared by linked rows.
65    Hold(Name),
66    /// A named checkbox constraint must hold.
67    Said(Check),
68}
69
70/// Comparison operator for a [`Filter`].
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub enum Op {
73    /// Equal.
74    Eq,
75    /// Not equal.
76    Ne,
77    /// Strictly greater.
78    More,
79    /// Strictly less.
80    Less,
81    /// Within the given calendar day.
82    At,
83    /// Case-insensitive SQL `LIKE` match.
84    Like,
85    /// `IS NULL` on the field.
86    Bare,
87}
88
89/// A leaf condition on one field.
90#[derive(Clone, Debug, PartialEq)]
91pub struct Filter {
92    /// Field to compare.
93    pub field: Name,
94    /// Comparison operator.
95    pub op: Op,
96    /// Comparison value.
97    pub value: Value,
98}
99
100/// Nested query condition.
101#[derive(Clone, Debug, PartialEq)]
102pub enum Tree {
103    /// A single leaf filter.
104    Leaf(Filter),
105    /// All branches must hold.
106    And(Vec<Tree>),
107    /// At least one branch must hold.
108    Or(Vec<Tree>),
109    /// Negation of the wrapped branch.
110    Cut(Box<Tree>),
111}
112
113/// Sort direction.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum Order {
116    /// Ascending.
117    Asc,
118    /// Descending.
119    Desc,
120}
121
122/// A sort on one field.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub struct Sort {
125    /// Field to sort by.
126    pub field: Name,
127    /// Sort direction.
128    pub order: Order,
129}
130
131impl Sort {
132    /// Parses `-name`/`name` into a [`Sort`]; unknown fields fall back to the schema key.
133    pub fn parse(raw: &str, schema: &Schema) -> Self {
134        let (name, order) = match raw.strip_prefix('-') {
135            Some(name) => (name, Order::Desc),
136            None => (raw, Order::Asc),
137        };
138        let known = schema
139            .fields
140            .iter()
141            .any(|field| field.name.as_str() == name);
142        if known {
143            for field in &schema.fields {
144                if field.name.as_str() == name {
145                    return Sort {
146                        field: field.name,
147                        order,
148                    };
149                }
150            }
151        }
152        Sort {
153            field: schema.key(),
154            order: Order::Asc,
155        }
156    }
157}
158
159/// Paging window; `count` 0 means no limit.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub struct Page {
162    /// Maximum rows, 0 for all.
163    pub count: usize,
164    /// Rows to skip.
165    pub offset: usize,
166}
167
168impl Page {
169    /// The full page: no limit, no offset.
170    pub fn all() -> Self {
171        Self {
172            count: 0,
173            offset: 0,
174        }
175    }
176}
177
178/// Which columns a query returns.
179#[derive(Clone, Debug, PartialEq, Eq)]
180pub enum Only {
181    /// Every physical column.
182    All,
183    /// Only the named columns.
184    Some(Vec<Name>),
185    /// First matching row, all columns.
186    Lone,
187}
188
189/// Aggregate over matching rows.
190#[derive(Clone, Copy, Debug, PartialEq, Eq)]
191pub enum Mass {
192    /// Row count.
193    Count,
194    /// Sum of a column.
195    Sum(Name),
196    /// Average of a column.
197    Mean(Name),
198    /// Minimum of a column.
199    Low(Name),
200    /// Maximum of a column.
201    High(Name),
202}
203
204/// A full read: condition, sort, page, columns, optional aggregate.
205#[derive(Clone, Debug, PartialEq)]
206pub struct Query {
207    /// Root condition.
208    pub tree: Tree,
209    /// Ordering.
210    pub sort: Vec<Sort>,
211    /// Paging window.
212    pub page: Page,
213    /// Columns to return.
214    pub only: Only,
215    /// Optional aggregate.
216    pub mass: Option<Mass>,
217}
218
219/// A row key: autoincrement id or text key.
220#[derive(Clone, Debug, PartialEq)]
221pub enum Key {
222    /// Autoincrement `id` key.
223    Int(i64),
224    /// Custom text key.
225    Text(String),
226}
227
228impl Key {
229    /// Converts a [`Value`] into a key, or [`StoreError::Value`].
230    pub fn of(value: &Value) -> Result<Self, StoreError> {
231        match value {
232            Value::Int(id) => Ok(Key::Int(*id)),
233            Value::Str(text) => Ok(Key::Text(text.clone())),
234            _ => Err(StoreError::Value("bad key".into())),
235        }
236    }
237
238    /// Parses raw text into a key: text when the schema is keyed, else int-or-text.
239    pub fn parse(raw: &str, schema: &Schema) -> Self {
240        let keyed = schema
241            .fields
242            .iter()
243            .any(|field| field.keyed && !field.id && field.name == schema.key());
244        if keyed {
245            return Key::Text(raw.into());
246        }
247        match raw.parse::<i64>() {
248            Ok(id) => Key::Int(id),
249            Err(_) => Key::Text(raw.into()),
250        }
251    }
252
253    /// This key as a [`Value`].
254    pub fn value(&self) -> Value {
255        match self {
256            Key::Int(id) => Value::int(*id),
257            Key::Text(text) => Value::str(text),
258        }
259    }
260}
261
262/// Spec for one column of a schema.
263#[derive(Clone)]
264pub struct Field {
265    /// Column name.
266    pub name: Name,
267    /// SQL type name (`INTEGER`, `REAL`, or `TEXT`).
268    pub dtype: &'static str,
269    /// Autoincrement primary key column.
270    pub id: bool,
271    /// Primary key with a caller-supplied value.
272    pub keyed: bool,
273    /// Virtual many-to-many column, not stored inline.
274    pub many: bool,
275    /// Nullable column.
276    pub optional: bool,
277    /// Unique constraint.
278    pub unique: bool,
279    /// Indexed column.
280    pub index: bool,
281    /// Fixed choice list, if any.
282    pub pick: Option<Pick>,
283    /// Default value factory, if any.
284    pub default: Option<DefaultFn>,
285    /// Reference to another table, if any.
286    pub link: Option<Link>,
287    /// Widget for this field.
288    pub widget: fn() -> Widget,
289    /// Parses raw text into the field's value.
290    pub parse: fn(&str, &mut dyn Writer) -> Result<(), StoreError>,
291    /// Renders the field's value as text.
292    pub text: fn(&mut dyn Reader) -> String,
293}
294
295/// Default value factory writing into a [`Writer`].
296pub type DefaultFn = Arc<dyn Fn(&mut dyn Writer) + Send + Sync + 'static>;
297
298impl Field {
299    /// A plain column storing any [`Storable`] + [`Show`] type.
300    pub fn cell<T: Storable + Show>(name: &'static str) -> Self {
301        Self {
302            name: Name(name),
303            dtype: T::dtype(),
304            id: false,
305            keyed: false,
306            many: false,
307            optional: false,
308            unique: false,
309            index: false,
310            pick: None,
311            default: None,
312            link: None,
313            widget: widget_of::<T>,
314            parse: parse_of::<T>,
315            text: text_of::<T>,
316        }
317    }
318
319    /// An autoincrement `id` primary key column.
320    pub fn id() -> Self {
321        let field = Self::cell::<i64>("id");
322        Self {
323            id: true,
324            keyed: true,
325            ..field
326        }
327    }
328
329    /// A caller-supplied primary key column.
330    pub fn key<T: Storable + Show>(name: &'static str) -> Self {
331        let field = Self::cell::<T>(name);
332        Self {
333            keyed: true,
334            ..field
335        }
336    }
337
338    /// A string column.
339    pub fn str(name: &'static str) -> Self {
340        Self::cell::<String>(name)
341    }
342
343    /// A boolean column.
344    pub fn check(name: &'static str) -> Self {
345        Self::cell::<bool>(name)
346    }
347
348    /// A virtual many-to-many column.
349    pub fn many(name: &'static str) -> Self {
350        Self {
351            name: Name(name),
352            dtype: "TEXT",
353            id: false,
354            keyed: false,
355            many: true,
356            optional: false,
357            unique: false,
358            index: false,
359            pick: None,
360            default: None,
361            link: None,
362            widget: nop_widget,
363            parse: nop_parse,
364            text: nop_text,
365        }
366    }
367
368    /// Marks the column nullable.
369    pub fn optional(mut self) -> Self {
370        self.optional = true;
371        self
372    }
373
374    /// Adds a unique constraint.
375    pub fn unique(mut self) -> Self {
376        self.unique = true;
377        self
378    }
379
380    /// Marks the column indexed.
381    pub fn indexed(mut self) -> Self {
382        self.index = true;
383        self
384    }
385
386    /// Sets a static default [`Storable`] value.
387    pub fn default_value(mut self, value: impl Storable) -> Self {
388        self.default = Some(Arc::new(move |w| value.put(w)));
389        self
390    }
391
392    /// Sets a default value factory.
393    pub fn default(mut self, make: impl Fn(&mut dyn Writer) + Send + Sync + 'static) -> Self {
394        self.default = Some(Arc::new(make));
395        self
396    }
397
398    /// Links to another table's column (`"table.column"` or just `"table"`).
399    pub fn references(mut self, target: &'static str) -> Self {
400        let (table, column) = match target.split_once('.') {
401            Some((table, column)) => (table, column),
402            None => (target, "id"),
403        };
404        self.link = Some(Link::To(Table(table), Name(column)));
405        self
406    }
407
408    /// Sets a [`Link`] to another table.
409    pub fn link(mut self, link: Link) -> Self {
410        self.link = Some(link);
411        self
412    }
413
414    /// The referenced (table, column) for a direct link, if any.
415    pub fn reference(&self) -> Option<(Table, Name)> {
416        match self.link {
417            Some(Link::To(table, name)) => Some((table, name)),
418            _ => None,
419        }
420    }
421
422    /// The field's [`Widget`].
423    pub fn load(&self) -> Widget {
424        (self.widget)()
425    }
426
427    /// The field's default as a [`Value`], or [`Value::Null`].
428    pub fn initial(&self) -> Value {
429        match &self.default {
430            Some(make) => {
431                let mut gather = Gather::new();
432                make(&mut gather);
433                gather.value()
434            }
435            None => Value::Null,
436        }
437    }
438}
439
440fn widget_of<T: Show>() -> Widget {
441    T::widget()
442}
443
444fn parse_of<T: Storable + Show>(raw: &str, w: &mut dyn Writer) -> Result<(), StoreError> {
445    let value = T::parse(raw)?;
446    T::put(&value, w);
447    Ok(())
448}
449
450fn text_of<T: Storable + Show>(r: &mut dyn Reader) -> String {
451    match T::take(r) {
452        Ok(value) => T::text(&value),
453        Err(_) => String::new(),
454    }
455}
456
457fn nop_widget() -> Widget {
458    Widget::Text
459}
460
461fn nop_parse(_raw: &str, _w: &mut dyn Writer) -> Result<(), StoreError> {
462    Ok(())
463}
464
465fn nop_text(_r: &mut dyn Reader) -> String {
466    String::new()
467}
468
469/// A table definition: name, fields, and rules.
470#[derive(Clone)]
471pub struct Schema {
472    /// Table name.
473    pub table: Table,
474    /// Column specs.
475    pub fields: Vec<Field>,
476    /// Constraints.
477    pub rules: Vec<Rule>,
478}
479
480impl Schema {
481    /// The primary key column name.
482    pub fn key(&self) -> Name {
483        self.fields
484            .iter()
485            .find(|field| field.id || field.keyed)
486            .map(|field| field.name)
487            .unwrap_or(Name("id"))
488    }
489}
490
491/// A bulk row action with a name, title, and handler.
492#[derive(Clone, Copy, Debug)]
493pub struct Action {
494    /// Machine name.
495    pub name: Name,
496    /// Display title.
497    pub title: &'static str,
498    /// Handler producing a result message.
499    pub run: Run,
500    /// Whether the action is logged.
501    pub logged: bool,
502    /// Whether the action applies to selected rows.
503    pub row: bool,
504}
505
506/// Action handler: store, schema, and target keys to a result message.
507pub type Run =
508    fn(Arc<dyn Store>, Schema, Vec<Key>) -> BoxFuture<'static, Result<String, StoreError>>;
509
510impl Action {
511    /// The built-in delete action.
512    pub fn wipe() -> Self {
513        Self {
514            name: Name("wipe"),
515            title: "Delete",
516            run: wipe,
517            logged: true,
518            row: true,
519        }
520    }
521}
522
523fn wipe(
524    store: Arc<dyn Store>,
525    schema: Schema,
526    keys: Vec<Key>,
527) -> BoxFuture<'static, Result<String, StoreError>> {
528    Box::pin(async move {
529        for key in &keys {
530            store.remove(&schema, key).await?;
531        }
532        if keys.len() == 1 {
533            Ok("Deleted 1 row.".into())
534        } else {
535            Ok(format!("Deleted {} rows.", keys.len()))
536        }
537    })
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use rust_decimal::Decimal;
544
545    fn schema() -> Schema {
546        Schema {
547            table: Table("posts"),
548            fields: vec![Field::id(), Field::str("title")],
549            rules: Vec::new(),
550        }
551    }
552
553    fn keyed() -> Schema {
554        Schema {
555            table: Table("products"),
556            fields: vec![Field::key::<String>("sku"), Field::cell::<Decimal>("price")],
557            rules: Vec::new(),
558        }
559    }
560
561    #[test]
562    fn keys() {
563        assert_eq!(schema().key(), Name("id"));
564        assert_eq!(keyed().key(), Name("sku"));
565        assert_eq!(Key::parse("7", &schema()).value(), Value::int(7));
566        assert_eq!(Key::parse("7", &keyed()).value(), Value::str("7"));
567        assert_eq!(Key::parse("x", &schema()).value(), Value::str("x"));
568    }
569
570    #[test]
571    fn key_of() {
572        assert_eq!(Key::of(&Value::int(3)).unwrap(), Key::Int(3));
573        assert_eq!(Key::of(&Value::str("x")).unwrap(), Key::Text("x".into()));
574        assert!(Key::of(&Value::Null).is_err());
575    }
576
577    #[test]
578    fn sorts() {
579        let schema = schema();
580        assert_eq!(
581            Sort::parse("title", &schema),
582            Sort {
583                field: Name("title"),
584                order: Order::Asc,
585            }
586        );
587        assert_eq!(
588            Sort::parse("-title", &schema),
589            Sort {
590                field: Name("title"),
591                order: Order::Desc,
592            }
593        );
594        assert_eq!(
595            Sort::parse("junk", &schema),
596            Sort {
597                field: Name("id"),
598                order: Order::Asc,
599            }
600        );
601    }
602}