Skip to main content

fiberplane_models/notebooks/cells/
table_cell.rs

1use crate::formatting::RichText;
2#[cfg(feature = "fp-bindgen")]
3use fp_bindgen::prelude::Serializable;
4use serde::{Deserialize, Serialize};
5use std::str::FromStr;
6use thiserror::Error;
7use typed_builder::TypedBuilder;
8
9const MIN_LENGTH: usize = 4;
10
11/// Cell used for displaying tables in a notebook.
12///
13/// Tables have columns, which are tracked using [TableColumnDefinition]. The
14/// column definition may specify a specific schema to be used for all values
15/// in that column.
16///
17/// Tables also have [rows](TableRow), which are used for tracking all the data
18/// in the table. Each row has multiple "row values" (we intentionally avoid the
19/// term "cell" here, because it would be too confusing with the table cell
20/// itself).
21///
22/// Row values have a specific data type, which should correspond to the type
23/// specified in the [TableColumnDefinition].
24///
25/// Every row and every column inside a table has a unique ID. Those IDs can be
26/// combined to create a [TableRowValueId]. [TableRowValueId] can be serialized
27/// to be used inside the `field` of
28/// [some operations](crate::notebooks::operations::ReplaceTextOperation::field)
29/// as well as [focus types](crate::realtime::FocusPosition::field).
30#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TypedBuilder)]
31#[cfg_attr(
32    feature = "fp-bindgen",
33    derive(Serializable),
34    fp(rust_module = "fiberplane_models::notebooks")
35)]
36#[non_exhaustive]
37#[serde(rename_all = "camelCase")]
38pub struct TableCell {
39    #[builder(setter(into))]
40    pub id: String,
41
42    #[builder(default, setter(strip_option))]
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub read_only: Option<bool>,
45
46    /// Describes the types used for the columns and the order they should be
47    /// rendered in.
48    #[builder(default, setter(into))]
49    pub column_defs: Vec<TableColumnDefinition>,
50
51    /// Holds the table rows and their values.
52    #[builder(default, setter(into))]
53    pub rows: Vec<TableRow>,
54}
55
56impl TableCell {
57    /// Returns a reference to a column definition by [TableColumnId].
58    pub fn column_def(&self, id: &TableColumnId) -> Option<&TableColumnDefinition> {
59        self.column_defs.iter().find(|def| &def.id == id)
60    }
61
62    /// Returns a reference to a row by [TableRowId].
63    pub fn row(&self, id: &TableRowId) -> Option<&TableRow> {
64        self.rows.iter().find(|row| &row.id == id)
65    }
66
67    /// Returns a reference to a row value.
68    pub fn row_value(&self, id: &TableRowValueId) -> Option<&TableRowValue> {
69        let row = self.row(id.row_id());
70        let value_index = self
71            .column_defs
72            .iter()
73            .position(|def| &def.id == id.column_id());
74        match (row, value_index) {
75            (Some(row), Some(value_index)) => row.values.get(value_index),
76            _ => None,
77        }
78    }
79
80    /// Returns the table cell with an updated row value for the given field.
81    pub fn with_row_value(&self, field: &str, mut updated_value: TableRowValue) -> Self {
82        let Ok(id) = TableRowValueId::from_str(field) else {
83            return self.clone();
84        };
85
86        let Some(column_index) = self
87            .column_defs
88            .iter()
89            .position(|column_def| &column_def.id == id.column_id())
90        else {
91            return self.clone();
92        };
93
94        let rows = self
95            .rows
96            .iter()
97            .map(|row| match &row.id == id.row_id() {
98                true => TableRow {
99                    id: row.id.clone(),
100                    values: row
101                        .values
102                        .iter()
103                        .enumerate()
104                        .map(|(i, value)| match i == column_index {
105                            // We use `mem::replace()` to avoid cloning, because
106                            // the borrow checker thinks we might move from
107                            // `updated_value` multiple times, even though we
108                            // know it'll happen only once since `field`
109                            // identifies a unique table row value.
110                            true => std::mem::replace(
111                                &mut updated_value,
112                                TableRowValue::Text(RichText::default()),
113                            ),
114                            false => value.clone(),
115                        })
116                        .collect(),
117                },
118                false => row.clone(),
119            })
120            .collect();
121
122        Self {
123            id: self.id.clone(),
124            column_defs: self.column_defs.clone(),
125            read_only: self.read_only,
126            rows,
127        }
128    }
129}
130
131#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
132#[cfg_attr(
133    feature = "fp-bindgen",
134    derive(Serializable),
135    fp(rust_module = "fiberplane_models::notebooks")
136)]
137#[non_exhaustive]
138#[serde(rename_all = "camelCase")]
139pub struct TableColumnDefinition {
140    /// ID of the column.
141    pub id: TableColumnId,
142
143    /// Heading text to be displayed at the top of the column.
144    #[builder(setter(into))]
145    pub title: String,
146}
147
148/// This is an automatically generated ID that is added to every column in a
149/// table cell.
150///
151/// Table column IDs are used to refer to column definitions in the table.
152/// They can also be combined with a [TableRowId] to create a [TableRowItemId].
153///
154/// Column IDs may only contain alphanumeric characters and must be unique
155/// within a table.
156#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
157#[cfg_attr(
158    feature = "fp-bindgen",
159    derive(Serializable),
160    fp(rust_module = "fiberplane_models::notebooks")
161)]
162pub struct TableColumnId(String);
163
164impl std::fmt::Display for TableColumnId {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        write!(f, "{}", self.0)
167    }
168}
169
170impl FromStr for TableColumnId {
171    type Err = InvalidTableId;
172
173    fn from_str(s: &str) -> Result<Self, Self::Err> {
174        validate_id(s).map(|_| Self(s.to_owned()))
175    }
176}
177
178#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
179#[cfg_attr(
180    feature = "fp-bindgen",
181    derive(Serializable),
182    fp(rust_module = "fiberplane_models::notebooks")
183)]
184#[non_exhaustive]
185pub struct TableRow {
186    /// ID of the row.
187    pub id: TableRowId,
188
189    /// The values inside this row.
190    ///
191    /// The types, order, and amount of the values should match the table's
192    /// [column definitions](TableCell::column_defs).
193    #[builder(setter(into))]
194    pub values: Vec<TableRowValue>,
195}
196
197/// This is an automatically generated ID that is added to every row in a table
198/// cell.
199///
200/// Table row IDs are used to refer to rows in the table. They can also be
201/// combined with a [TableColumnId] to create a [TableRowValueId].
202///
203/// Row IDs may only contain alphanumeric characters and must be unique within a
204/// table.
205#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
206#[cfg_attr(
207    feature = "fp-bindgen",
208    derive(Serializable),
209    fp(rust_module = "fiberplane_models::notebooks")
210)]
211pub struct TableRowId(String);
212
213impl std::fmt::Display for TableRowId {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        write!(f, "{}", self.0)
216    }
217}
218
219impl FromStr for TableRowId {
220    type Err = InvalidTableId;
221
222    fn from_str(s: &str) -> Result<Self, Self::Err> {
223        validate_id(s).map(|_| Self(s.to_owned()))
224    }
225}
226
227/// One of the values stored in a [TableRow].
228///
229/// We intentionally avoid the term "cell" here, because it would be too
230/// confusing with the table cell itself.
231///
232/// Row values can be looked up by [ID](TableRowValueId), although the ID is not
233/// stored in the row value itself. Instead, it can be created from the row ID
234/// and column ID.
235#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
236#[cfg_attr(
237    feature = "fp-bindgen",
238    derive(Serializable),
239    fp(rust_module = "fiberplane_models::notebooks")
240)]
241#[non_exhaustive]
242#[serde(tag = "type", rename_all = "snake_case")]
243pub enum TableRowValue {
244    Text(RichText),
245}
246
247/// This is a compound ID based on the row ID and column ID that together
248/// identify a row value.
249///
250/// Table row value IDs are used to refer to [row values](TableRowValue). They
251/// are not stored in the [TableCell] data structure, but are used in serialized
252/// form to refer to row values inside the `field` of
253/// [some operations](crate::notebooks::operations::ReplaceTextOperation::field)
254/// as well as [focus types](crate::realtime::FocusPosition::field).
255#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
256#[cfg_attr(
257    feature = "fp-bindgen",
258    derive(Serializable),
259    fp(rust_module = "fiberplane_models::notebooks")
260)]
261pub struct TableRowValueId(TableRowId, TableColumnId);
262
263impl TableRowValueId {
264    /// Creates a new value ID from a [TableRowId] and [TableColumnId].
265    pub fn new(row_id: TableRowId, column_id: TableColumnId) -> Self {
266        Self(row_id, column_id)
267    }
268
269    pub fn row_id(&self) -> &TableRowId {
270        &self.0
271    }
272
273    pub fn column_id(&self) -> &TableColumnId {
274        &self.1
275    }
276}
277
278impl std::fmt::Display for TableRowValueId {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        write!(f, "{};{}", self.0, self.1)
281    }
282}
283
284impl FromStr for TableRowValueId {
285    type Err = InvalidTableId;
286
287    fn from_str(s: &str) -> Result<Self, Self::Err> {
288        match s.split_once(';') {
289            Some((before, after)) => Ok(Self::new(
290                TableRowId::from_str(before)?,
291                TableColumnId::from_str(after)?,
292            )),
293            None => Err(InvalidTableId::MissingSeparator),
294        }
295    }
296}
297
298#[derive(Debug, Error, PartialEq, Eq)]
299#[cfg_attr(
300    feature = "fp-bindgen",
301    derive(Serializable),
302    fp(rust_module = "fiberplane_models::notebooks")
303)]
304#[non_exhaustive]
305pub enum InvalidTableId {
306    #[error("table IDs must be at least 6 characters in length")]
307    TooShort,
308    #[error("table IDs may only contain alpha-numeric characters")]
309    InvalidCharacters,
310    #[error("row ID and column ID must be separated by a semicolon")]
311    MissingSeparator,
312}
313
314fn validate_id(id: &str) -> Result<(), InvalidTableId> {
315    if id.len() < MIN_LENGTH {
316        return Err(InvalidTableId::TooShort);
317    }
318
319    if id.chars().any(|c| !c.is_ascii_alphanumeric()) {
320        return Err(InvalidTableId::InvalidCharacters);
321    }
322
323    Ok(())
324}