use crate::formatting::RichText;
#[cfg(feature = "fp-bindgen")]
use fp_bindgen::prelude::Serializable;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use thiserror::Error;
use typed_builder::TypedBuilder;
const MIN_LENGTH: usize = 4;
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TypedBuilder)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct TableCell {
#[builder(setter(into))]
pub id: String,
#[builder(default, setter(strip_option))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
#[builder(default, setter(into))]
pub column_defs: Vec<TableColumnDefinition>,
#[builder(default, setter(into))]
pub rows: Vec<TableRow>,
}
impl TableCell {
pub fn column_def(&self, id: &TableColumnId) -> Option<&TableColumnDefinition> {
self.column_defs.iter().find(|def| &def.id == id)
}
pub fn row(&self, id: &TableRowId) -> Option<&TableRow> {
self.rows.iter().find(|row| &row.id == id)
}
pub fn row_value(&self, id: &TableRowValueId) -> Option<&TableRowValue> {
let row = self.row(id.row_id());
let value_index = self
.column_defs
.iter()
.position(|def| &def.id == id.column_id());
match (row, value_index) {
(Some(row), Some(value_index)) => row.values.get(value_index),
_ => None,
}
}
pub fn with_row_value(&self, field: &str, mut updated_value: TableRowValue) -> Self {
let Ok(id) = TableRowValueId::from_str(field) else {
return self.clone();
};
let Some(column_index) = self
.column_defs
.iter()
.position(|column_def| &column_def.id == id.column_id())
else {
return self.clone();
};
let rows = self
.rows
.iter()
.map(|row| match &row.id == id.row_id() {
true => TableRow {
id: row.id.clone(),
values: row
.values
.iter()
.enumerate()
.map(|(i, value)| match i == column_index {
true => std::mem::replace(
&mut updated_value,
TableRowValue::Text(RichText::default()),
),
false => value.clone(),
})
.collect(),
},
false => row.clone(),
})
.collect();
Self {
id: self.id.clone(),
column_defs: self.column_defs.clone(),
read_only: self.read_only,
rows,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct TableColumnDefinition {
pub id: TableColumnId,
#[builder(setter(into))]
pub title: String,
}
#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
pub struct TableColumnId(String);
impl std::fmt::Display for TableColumnId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for TableColumnId {
type Err = InvalidTableId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_id(s).map(|_| Self(s.to_owned()))
}
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize, TypedBuilder)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
#[non_exhaustive]
pub struct TableRow {
pub id: TableRowId,
#[builder(setter(into))]
pub values: Vec<TableRowValue>,
}
#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
pub struct TableRowId(String);
impl std::fmt::Display for TableRowId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for TableRowId {
type Err = InvalidTableId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
validate_id(s).map(|_| Self(s.to_owned()))
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
#[non_exhaustive]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TableRowValue {
Text(RichText),
}
#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
pub struct TableRowValueId(TableRowId, TableColumnId);
impl TableRowValueId {
pub fn new(row_id: TableRowId, column_id: TableColumnId) -> Self {
Self(row_id, column_id)
}
pub fn row_id(&self) -> &TableRowId {
&self.0
}
pub fn column_id(&self) -> &TableColumnId {
&self.1
}
}
impl std::fmt::Display for TableRowValueId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{};{}", self.0, self.1)
}
}
impl FromStr for TableRowValueId {
type Err = InvalidTableId;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once(';') {
Some((before, after)) => Ok(Self::new(
TableRowId::from_str(before)?,
TableColumnId::from_str(after)?,
)),
None => Err(InvalidTableId::MissingSeparator),
}
}
}
#[derive(Debug, Error, PartialEq, Eq)]
#[cfg_attr(
feature = "fp-bindgen",
derive(Serializable),
fp(rust_module = "fiberplane_models::notebooks")
)]
#[non_exhaustive]
pub enum InvalidTableId {
#[error("table IDs must be at least 6 characters in length")]
TooShort,
#[error("table IDs may only contain alpha-numeric characters")]
InvalidCharacters,
#[error("row ID and column ID must be separated by a semicolon")]
MissingSeparator,
}
fn validate_id(id: &str) -> Result<(), InvalidTableId> {
if id.len() < MIN_LENGTH {
return Err(InvalidTableId::TooShort);
}
if id.chars().any(|c| !c.is_ascii_alphanumeric()) {
return Err(InvalidTableId::InvalidCharacters);
}
Ok(())
}