Skip to main content

tabnas_alchemy/shared/
table.rs

1//! `TableRows/1`: one schema, ordered finite rows, completion.
2//!
3//! The protocol is flat and budgeted: a row is a finite vector of cells,
4//! already projected into schema order by the transducer, and a renderer
5//! never sees a source path. Public columns carry a label; the source
6//! binding (`BoundColumn`) stays on the transducer's side of the boundary.
7
8use std::fmt;
9
10use crate::shared::datum::Datum;
11use crate::shared::error::Fail;
12use crate::shared::selector::{Segment, Selector};
13use crate::shared::sink::Flow;
14
15/// One projected value.
16#[derive(Clone, Debug, PartialEq)]
17pub enum Cell {
18    Null,
19    Bool(bool),
20    Number {
21        value: f64,
22        lexeme: Option<Box<str>>,
23    },
24    String(Box<str>),
25    /// The source had no value at the column's path. Not null, not the
26    /// empty string, not zero: a policy maps or rejects it later.
27    Missing,
28}
29
30impl Cell {
31    /// From a retained value. A container is not a cell; it is
32    /// serialized as compact JSON text, which is the lossy but
33    /// unambiguous choice the standard binding makes and documents.
34    pub fn from_datum(d: &Datum) -> Cell {
35        match d {
36            Datum::Null => Cell::Null,
37            Datum::Bool(b) => Cell::Bool(*b),
38            Datum::Number { value, lexeme } => Cell::Number {
39                value: *value,
40                lexeme: lexeme.clone(),
41            },
42            Datum::String(s) => Cell::String(s.clone()),
43            Datum::Array(_) | Datum::Object(_) => Cell::String(d.to_string().into()),
44        }
45    }
46
47    /// [`Cell::from_datum`] for a value the caller owns: the text moves
48    /// instead of being copied.
49    pub fn from_owned(d: Datum) -> Cell {
50        match d {
51            Datum::Null => Cell::Null,
52            Datum::Bool(b) => Cell::Bool(b),
53            Datum::Number { value, lexeme } => Cell::Number { value, lexeme },
54            Datum::String(s) => Cell::String(s),
55            Datum::Array(_) | Datum::Object(_) => Cell::String(d.to_string().into()),
56        }
57    }
58
59    pub fn is_missing(&self) -> bool {
60        matches!(self, Cell::Missing)
61    }
62
63    /// The bytes this cell retains, on the same basis as `Datum::byte_size`.
64    pub fn byte_size(&self) -> usize {
65        match self {
66            Cell::Null | Cell::Bool(_) | Cell::Missing => crate::shared::limits::NODE_BYTES,
67            Cell::Number { lexeme, .. } => {
68                crate::shared::limits::NODE_BYTES + lexeme.as_ref().map_or(8, |l| l.len())
69            }
70            Cell::String(s) => crate::shared::limits::NODE_BYTES + s.len(),
71        }
72    }
73}
74
75impl fmt::Display for Cell {
76    /// The cell as JSON text; `Missing` prints as `missing`.
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Cell::Null => f.write_str("null"),
80            Cell::Bool(b) => write!(f, "{b}"),
81            Cell::Number { value, lexeme } => match lexeme {
82                Some(l) => f.write_str(l),
83                None => write!(f, "{value}"),
84            },
85            Cell::String(s) => {
86                let mut out = String::new();
87                crate::shared::datum::write_json_string(s, &mut out);
88                f.write_str(&out)
89            }
90            Cell::Missing => f.write_str("missing"),
91        }
92    }
93}
94
95/// What a renderer knows about a column.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct PublicColumn {
98    pub label: Box<str>,
99}
100
101impl PublicColumn {
102    pub fn new(label: impl Into<Box<str>>) -> PublicColumn {
103        PublicColumn {
104            label: label.into(),
105        }
106    }
107}
108
109/// One event of `TableRows/1`.
110#[derive(Clone, Copy, Debug, PartialEq)]
111pub enum TableEvent<'a> {
112    /// Exactly one, first.
113    Schema(&'a [PublicColumn]),
114    /// As many as there are rows, each exactly as wide as the schema.
115    Row(&'a [Cell]),
116    /// Exactly one, last, and only after the source validated to its end.
117    End,
118}
119
120/// A consumer of `TableRows/1`.
121pub trait TableSink {
122    fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail>;
123}
124
125impl<S: TableSink + ?Sized> TableSink for &mut S {
126    fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
127        (**self).table_event(ev)
128    }
129}
130
131impl<S: TableSink + ?Sized> TableSink for Box<S> {
132    fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
133        (**self).table_event(ev)
134    }
135}
136
137/// An owned recording of a table, for tests and small results.
138#[derive(Clone, Debug, Default, PartialEq)]
139pub struct Table {
140    pub columns: Vec<PublicColumn>,
141    pub rows: Vec<Vec<Cell>>,
142    pub ended: bool,
143}
144
145impl TableSink for Table {
146    fn table_event(&mut self, ev: TableEvent<'_>) -> Result<Flow, Fail> {
147        match ev {
148            TableEvent::Schema(c) => self.columns = c.to_vec(),
149            TableEvent::Row(r) => self.rows.push(r.to_vec()),
150            TableEvent::End => self.ended = true,
151        }
152        Ok(Flow::Continue)
153    }
154}
155
156/// What to do when a row has no value at a column's path.
157#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
158pub enum MissingPolicy {
159    /// Deliver [`Cell::Missing`]; the renderer's policy decides.
160    #[default]
161    Missing,
162    /// Deliver `null`.
163    Null,
164    /// Fail the run with `MISSING_VALUE`.
165    Error,
166}
167
168/// A column as the transducer binds it: the public label, and the source
169/// path projected from each row. Never crosses into a renderer.
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct BoundColumn {
172    pub label: Box<str>,
173    pub source: Vec<Segment>,
174    pub missing: MissingPolicy,
175}
176
177impl BoundColumn {
178    pub fn new(label: impl Into<Box<str>>, source: Vec<Segment>) -> BoundColumn {
179        BoundColumn {
180            label: label.into(),
181            source,
182            missing: MissingPolicy::Missing,
183        }
184    }
185
186    pub fn public(&self) -> PublicColumn {
187        PublicColumn {
188            label: self.label.clone(),
189        }
190    }
191}
192
193/// Maps one metadata descriptor to a bound column.
194pub type ColumnMapper = Box<dyn Fn(&Datum) -> Result<BoundColumn, Fail> + Send + Sync>;
195
196/// Where a table's columns come from.
197pub enum Schema {
198    /// Declared by the caller; no metadata is read from the source.
199    Static(Vec<BoundColumn>),
200    /// Selected from the source and mapped one descriptor at a time. The
201    /// metadata must complete before the first row begins.
202    FromMetadata {
203        columns: Selector,
204        column: ColumnMapper,
205    },
206    /// The first row's member names, in its order. Data-dependent, and
207    /// documented as such: a later row's extra members are dropped, its
208    /// absent ones are `Missing`.
209    Infer,
210}
211
212impl fmt::Debug for Schema {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            Schema::Static(c) => f.debug_tuple("Static").field(c).finish(),
216            Schema::FromMetadata { columns, .. } => f
217                .debug_struct("FromMetadata")
218                .field("columns", columns)
219                .finish_non_exhaustive(),
220            Schema::Infer => f.write_str("Infer"),
221        }
222    }
223}
224
225/// A table transducer's source binding.
226#[derive(Debug)]
227pub struct TableBinding {
228    pub schema: Schema,
229    /// Each location this names is one row.
230    pub rows: Selector,
231}
232
233/// The standard mapping from a metadata descriptor to a column: the
234/// spec's `column-from-meta`, reading `title` and a `path` of segments.
235pub fn column_from_meta(meta: &Datum) -> Result<BoundColumn, Fail> {
236    let obj = meta
237        .as_object()
238        .ok_or_else(|| Fail::input("a column descriptor is not an object"))?;
239    let label = obj
240        .get("title")
241        .and_then(Datum::as_str)
242        .ok_or_else(|| Fail::input("a column descriptor has no string \"title\""))?;
243    let path = obj
244        .get("path")
245        .and_then(Datum::as_array)
246        .ok_or_else(|| Fail::input(format!("column {label:?} has no \"path\" array")))?;
247    let source = path
248        .iter()
249        .map(|seg| match seg {
250            Datum::String(s) => Ok(Segment::Key(s.clone())),
251            Datum::Number { value, .. }
252                if *value >= 0.0 && value.fract() == 0.0 && *value <= u32::MAX as f64 =>
253            {
254                Ok(Segment::Index(*value as usize))
255            }
256            other => Err(Fail::input(format!(
257                "column {label:?} has a path segment that is neither a string nor a non-negative integer: {other}"
258            ))),
259        })
260        .collect::<Result<Vec<_>, _>>()?;
261    Ok(BoundColumn::new(label, source))
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn column_from_meta_reads_title_and_path() {
270        let meta = Datum::from_json(
271            &serde_json::json!({"title": "Balance", "path": ["account", "balance"]}),
272        );
273        let c = column_from_meta(&meta).unwrap();
274        assert_eq!(&*c.label, "Balance");
275        assert_eq!(
276            c.source,
277            vec![Segment::key("account"), Segment::key("balance")]
278        );
279        let meta = Datum::from_json(&serde_json::json!({"title": "First", "path": ["tags", 0]}));
280        assert_eq!(
281            column_from_meta(&meta).unwrap().source[1],
282            Segment::Index(0)
283        );
284        let bad = Datum::from_json(&serde_json::json!({"title": "x", "path": ["a", -1]}));
285        assert_eq!(
286            column_from_meta(&bad).unwrap_err().code,
287            crate::shared::Code::InputInvalid
288        );
289        let bad = Datum::from_json(&serde_json::json!({"path": ["a"]}));
290        assert!(column_from_meta(&bad).is_err());
291    }
292
293    #[test]
294    fn cells_print_as_json() {
295        assert_eq!(
296            Cell::from_datum(&Datum::from_json(&serde_json::json!(50.25))).to_string(),
297            "50.25"
298        );
299        assert_eq!(
300            Cell::from_datum(&Datum::from_json(&serde_json::json!([1, 2]))).to_string(),
301            "\"[1,2]\""
302        );
303        assert_eq!(Cell::Missing.to_string(), "missing");
304        assert_eq!(Cell::from_datum(&Datum::Null), Cell::Null);
305    }
306
307    #[test]
308    fn a_table_records() {
309        let mut t = Table::default();
310        let cols = [PublicColumn::new("a")];
311        t.table_event(TableEvent::Schema(&cols)).unwrap();
312        t.table_event(TableEvent::Row(&[Cell::Bool(true)])).unwrap();
313        t.table_event(TableEvent::End).unwrap();
314        assert_eq!(t.columns.len(), 1);
315        assert_eq!(t.rows, vec![vec![Cell::Bool(true)]]);
316        assert!(t.ended);
317    }
318}