use crate::{Columns, Row, SerializableColumns, TableColumn, TableContext, TableData};
use dioxus::prelude::*;
use serde::Serialize;
pub trait SerializableColumn<R: Row>: TableColumn<R> {
fn header(&self) -> String {
self.column_name()
}
fn serialize_cell(&self, row: &R) -> impl Serialize + '_;
fn include_in_export(&self) -> bool {
true
}
}
pub trait Exporter {
type Error;
fn serialize_header(&mut self, col: usize, header: &str) -> Result<(), Self::Error>;
fn serialize_cell<'a>(
&mut self,
row: usize,
col: usize,
cell: impl Serialize + 'a,
) -> Result<(), Self::Error>;
}
impl<C: Columns<R> + SerializableColumns<R>, R: Row> TableData<C, R> {
pub fn serialize<E: Exporter>(&self, exporter: &mut E) -> Result<(), E::Error> {
self.context.serialize(self.rows, exporter)
}
}
impl<C> TableContext<C> {
pub fn serialize<R, E: Exporter>(
&self,
rows: ReadSignal<Vec<R>>,
exporter: &mut E,
) -> Result<(), E::Error>
where
C: Columns<R> + SerializableColumns<R>,
R: Row,
{
let binding = self.columns.read();
let all_headers = binding.serialize_headers();
let all_cells = binding.serialize_cell();
let mut export_col = 0;
for header_data in self.headers() {
let col_index = header_data.column_index;
if all_headers[col_index].include_in_export {
exporter.serialize_header(export_col, &(all_headers[col_index].header_fn)())?;
export_col += 1;
}
}
let sorted_rows: Vec<_> = self.rows(rows).collect();
for (row_index, row_data) in sorted_rows.into_iter().enumerate() {
let row = &row_data.rows.read()[row_data.index];
let mut export_col = 0;
for cell_data in row_data.cells() {
let col_index = cell_data.column_index;
if all_cells[col_index].include_in_export {
(all_cells[col_index].cell_fn)(row_index, export_col, row, exporter)?;
export_col += 1;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_suite::{test_hook, test_hook_simple};
use crate::{ColumnContext, Sort, SortDirection, SortGesture};
#[derive(Debug, Clone, PartialEq)]
struct Person {
name: String,
age: u32,
}
impl Row for Person {
fn key(&self) -> impl Into<String> {
format!("{}_{}", self.name, self.age)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
enum Priority {
High,
}
#[derive(Clone, PartialEq)]
struct NameColumn;
impl TableColumn<Person> for NameColumn {
fn column_name(&self) -> String {
"Name".to_string()
}
fn render_header(&self, _context: ColumnContext, _attributes: Vec<Attribute>) -> Element {
rsx! {
th {}
}
}
fn render_cell(
&self,
_context: ColumnContext,
_row: &Person,
_attributes: Vec<Attribute>,
) -> Element {
rsx! {
td {}
}
}
fn compare(&self, a: &Person, b: &Person) -> std::cmp::Ordering {
a.name.cmp(&b.name)
}
}
impl SerializableColumn<Person> for NameColumn {
fn serialize_cell(&self, row: &Person) -> impl Serialize + '_ {
row.name.clone()
}
}
#[derive(Clone, PartialEq)]
struct AgeColumn;
impl TableColumn<Person> for AgeColumn {
fn column_name(&self) -> String {
"Age".to_string()
}
fn render_header(&self, _context: ColumnContext, _attributes: Vec<Attribute>) -> Element {
rsx! {
th {}
}
}
fn render_cell(
&self,
_context: ColumnContext,
_row: &Person,
_attributes: Vec<Attribute>,
) -> Element {
rsx! {
td {}
}
}
fn compare(&self, a: &Person, b: &Person) -> std::cmp::Ordering {
a.age.cmp(&b.age)
}
}
impl SerializableColumn<Person> for AgeColumn {
fn serialize_cell(&self, row: &Person) -> impl Serialize + '_ {
row.age
}
}
#[derive(Clone, PartialEq)]
struct PriorityColumn;
impl TableColumn<Person> for PriorityColumn {
fn column_name(&self) -> String {
"Priority".to_string()
}
fn render_header(&self, _context: ColumnContext, _attributes: Vec<Attribute>) -> Element {
rsx! {
th {}
}
}
fn render_cell(
&self,
_context: ColumnContext,
_row: &Person,
_attributes: Vec<Attribute>,
) -> Element {
rsx! {
td {}
}
}
}
impl SerializableColumn<Person> for PriorityColumn {
fn header(&self) -> String {
"Custom Priority Header".to_string()
}
fn serialize_cell(&self, _row: &Person) -> impl Serialize + '_ {
Priority::High
}
}
struct MockExporter {
headers: Vec<(usize, String)>,
cells: Vec<(usize, usize, String)>,
}
impl MockExporter {
fn new() -> Self {
Self {
headers: Vec::new(),
cells: Vec::new(),
}
}
}
impl Exporter for MockExporter {
type Error = ();
fn serialize_header(&mut self, col: usize, header: &str) -> Result<(), Self::Error> {
self.headers.push((col, header.to_string()));
Ok(())
}
fn serialize_cell<'a>(
&mut self,
row: usize,
col: usize,
cell: impl Serialize + 'a,
) -> Result<(), Self::Error> {
let json = serde_json::to_string(&cell).unwrap();
self.cells.push((row, col, json));
Ok(())
}
}
#[test]
fn test_export_empty_table() {
test_hook_simple(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(Vec::<Person>::new());
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
exporter
},
|exporter| {
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Name".to_string()), (1, "Age".to_string())]
);
assert_eq!(exporter.cells.as_slice(), &[]);
},
);
}
#[test]
fn test_export_multiple_rows_and_columns() {
test_hook(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(vec![
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
Person {
name: "Charlie".to_string(),
age: 35,
},
]);
(context, rows)
},
|(context, rows), _| {
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Name".to_string()), (1, "Age".to_string())]
);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"Alice\"".to_string()),
(0, 1, "30".to_string()),
(1, 0, "\"Bob\"".to_string()),
(1, 1, "25".to_string()),
(2, 0, "\"Charlie\"".to_string()),
(2, 1, "35".to_string()),
]
);
},
|_| {},
);
}
#[test]
fn test_export_with_custom_header() {
test_hook(
|| {
let context =
TableContext::use_table_context((NameColumn, AgeColumn, PriorityColumn));
let rows = Signal::new(vec![Person {
name: "Alice".to_string(),
age: 30,
}]);
(context, rows)
},
|(context, rows), _| {
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[
(0, "Name".to_string()),
(1, "Age".to_string()),
(2, "Custom Priority Header".to_string()) ]
);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"Alice\"".to_string()),
(0, 1, "30".to_string()),
(0, 2, "\"High\"".to_string()),
]
);
},
|_| {},
);
}
#[test]
fn test_export_with_column_reordering() {
test_hook(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(vec![Person {
name: "Alice".to_string(),
age: 30,
}]);
(context, rows)
},
|(context, rows), _| {
context.data.swap_columns(0, 1);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Age".to_string()), (1, "Name".to_string())]
);
assert_eq!(
exporter.cells.as_slice(),
&[(0, 0, "30".to_string()), (0, 1, "\"Alice\"".to_string())]
);
},
|_| {},
);
}
#[test]
fn test_export_with_hidden_columns() {
test_hook(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(vec![Person {
name: "Alice".to_string(),
age: 30,
}]);
(context, rows)
},
|(context, rows), _| {
context.data.hide_column(1);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(exporter.headers.as_slice(), &[(0, "Name".to_string())]);
assert_eq!(
exporter.cells.as_slice(),
&[(0, 0, "\"Alice\"".to_string())]
);
},
|_| {},
);
}
#[test]
fn test_export_with_sorted_rows() {
test_hook_simple(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(vec![
Person {
name: "Charlie".to_string(),
age: 35,
},
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
]);
context.data.request_sort(
1,
SortGesture::AddFirst(Sort {
direction: SortDirection::Ascending,
}),
);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
exporter
},
|exporter| {
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Name".to_string()), (1, "Age".to_string())]
);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"Bob\"".to_string()),
(0, 1, "25".to_string()),
(1, 0, "\"Alice\"".to_string()),
(1, 1, "30".to_string()),
(2, 0, "\"Charlie\"".to_string()),
(2, 1, "35".to_string()),
]
);
},
);
}
#[test]
fn test_export_with_all_columns_hidden() {
test_hook(
|| {
let context = TableContext::use_table_context((NameColumn, AgeColumn));
let rows = Signal::new(vec![Person {
name: "Alice".to_string(),
age: 30,
}]);
(context, rows)
},
|(context, rows), _| {
context.data.hide_column(0);
context.data.hide_column(1);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(exporter.headers.as_slice(), &[]);
assert_eq!(exporter.cells.as_slice(), &[]);
},
|_| {},
);
}
#[test]
fn test_export_with_combined_features() {
test_hook(
|| {
let context =
TableContext::use_table_context((NameColumn, AgeColumn, PriorityColumn));
let rows = Signal::new(vec![
Person {
name: "Charlie".to_string(),
age: 35,
},
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
]);
(context, rows)
},
|(context, rows), _| {
context.data.hide_column(2);
context.data.swap_columns(0, 1);
context.data.request_sort(
1,
SortGesture::AddFirst(Sort {
direction: SortDirection::Ascending,
}),
);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Age".to_string()), (1, "Name".to_string())]
);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "25".to_string()),
(0, 1, "\"Bob\"".to_string()),
(1, 0, "30".to_string()),
(1, 1, "\"Alice\"".to_string()),
(2, 0, "35".to_string()),
(2, 1, "\"Charlie\"".to_string()),
]
);
},
|_| {},
);
}
#[derive(Clone, PartialEq)]
struct ExcludedColumn;
impl TableColumn<Person> for ExcludedColumn {
fn column_name(&self) -> String {
"Excluded".to_string()
}
fn render_header(&self, _context: ColumnContext, _attributes: Vec<Attribute>) -> Element {
rsx! {
th {}
}
}
fn render_cell(
&self,
_context: ColumnContext,
_row: &Person,
_attributes: Vec<Attribute>,
) -> Element {
rsx! {
td {}
}
}
}
impl SerializableColumn<Person> for ExcludedColumn {
fn serialize_cell(&self, _row: &Person) -> impl Serialize + '_ {
"excluded_value"
}
fn include_in_export(&self) -> bool {
false
}
}
#[test]
fn test_export_with_include_in_export_false() {
test_hook(
|| {
let context = TableContext::use_table_context((NameColumn, ExcludedColumn));
let rows = Signal::new(vec![
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
]);
(context, rows)
},
|(context, rows), _| {
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(exporter.headers.as_slice(), &[(0, "Name".to_string())]);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"Alice\"".to_string()),
(1, 0, "\"Bob\"".to_string())
]
);
},
|_| {},
);
}
#[test]
fn test_export_with_include_in_export_false_and_hidden_columns() {
test_hook(
|| {
let context =
TableContext::use_table_context((NameColumn, AgeColumn, ExcludedColumn));
let rows = Signal::new(vec![
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
]);
(context, rows)
},
|(context, rows), _| {
context.data.hide_column(1);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(exporter.headers.as_slice(), &[(0, "Name".to_string())]);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"Alice\"".to_string()),
(1, 0, "\"Bob\"".to_string())
]
);
},
|_| {},
);
}
#[test]
fn test_export_with_include_in_export_false_and_column_reordering() {
test_hook(
|| {
let context =
TableContext::use_table_context((NameColumn, ExcludedColumn, AgeColumn));
let rows = Signal::new(vec![Person {
name: "Alice".to_string(),
age: 30,
}]);
(context, rows)
},
|(context, rows), _| {
context.data.swap_columns(0, 2);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[(0, "Age".to_string()), (1, "Name".to_string())]
);
assert_eq!(
exporter.cells.as_slice(),
&[(0, 0, "30".to_string()), (0, 1, "\"Alice\"".to_string())]
);
},
|_| {},
);
}
#[test]
fn test_export_with_include_in_export_false_and_combined_features() {
test_hook(
|| {
let context = TableContext::use_table_context((
NameColumn,
AgeColumn,
ExcludedColumn,
PriorityColumn,
));
let rows = Signal::new(vec![
Person {
name: "Charlie".to_string(),
age: 35,
},
Person {
name: "Alice".to_string(),
age: 30,
},
Person {
name: "Bob".to_string(),
age: 25,
},
]);
(context, rows)
},
|(context, rows), _| {
context.data.hide_column(0);
context.data.swap_columns(1, 3);
context.data.request_sort(
1,
SortGesture::AddFirst(Sort {
direction: SortDirection::Ascending,
}),
);
let mut exporter = MockExporter::new();
context.serialize(rows.into(), &mut exporter).unwrap();
assert_eq!(
exporter.headers.as_slice(),
&[
(0, "Custom Priority Header".to_string()),
(1, "Age".to_string())
]
);
assert_eq!(
exporter.cells.as_slice(),
&[
(0, 0, "\"High\"".to_string()),
(0, 1, "25".to_string()),
(1, 0, "\"High\"".to_string()),
(1, 1, "30".to_string()),
(2, 0, "\"High\"".to_string()),
(2, 1, "35".to_string()),
]
);
},
|_| {},
);
}
}