fiberplane_models/notebooks/cells/
table_cell.rs1use 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#[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 #[builder(default, setter(into))]
49 pub column_defs: Vec<TableColumnDefinition>,
50
51 #[builder(default, setter(into))]
53 pub rows: Vec<TableRow>,
54}
55
56impl TableCell {
57 pub fn column_def(&self, id: &TableColumnId) -> Option<&TableColumnDefinition> {
59 self.column_defs.iter().find(|def| &def.id == id)
60 }
61
62 pub fn row(&self, id: &TableRowId) -> Option<&TableRow> {
64 self.rows.iter().find(|row| &row.id == id)
65 }
66
67 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 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 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 pub id: TableColumnId,
142
143 #[builder(setter(into))]
145 pub title: String,
146}
147
148#[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 pub id: TableRowId,
188
189 #[builder(setter(into))]
194 pub values: Vec<TableRowValue>,
195}
196
197#[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#[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#[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 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}