Skip to main content

appcore_filemaker/export/
csv.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: csv.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Streams bounded RFC-4180-style records without duplicating textual cells.
12
13use std::borrow::Cow;
14use std::collections::BTreeSet;
15use std::io::Write;
16
17use crate::{
18    Dataset, ErrorCode, ExportCapabilities, ExportLossReport, ExportOutcome, FileMakerError,
19    ResourceLimits, Result, TableSpec,
20};
21
22/// Streams a dataset as RFC-4180-style UTF-8 CSV in table column order.
23pub fn export_dataset_csv(
24    spec: &TableSpec,
25    dataset: &dyn Dataset,
26    limits: &ResourceLimits,
27    writer: &mut dyn Write,
28) -> Result<ExportOutcome> {
29    limits.validate()?;
30    spec.validate()?;
31    let mut bounded = BoundedWriter::new(writer, limits.max_output_bytes);
32    write_record(
33        &mut bounded,
34        spec.columns.iter().map(|column| column.header.as_str()),
35    )?;
36    spec.visit_bounded(dataset, &mut |_, row| {
37        write_record(
38            &mut bounded,
39            spec.columns.iter().map(|column| {
40                row.get(&column.field)
41                    .map_or(Cow::Borrowed(""), display_value)
42            }),
43        )
44    })?;
45    Ok(ExportOutcome {
46        bytes_written: bounded.written,
47        loss_report: ExportLossReport::default(),
48        capabilities: BTreeSet::from([ExportCapabilities::Semantic]),
49    })
50}
51
52/// Streams a dataset into a bounded in-memory CSV byte vector.
53pub fn export_dataset_csv_bytes(
54    spec: &TableSpec,
55    dataset: &dyn Dataset,
56    limits: &ResourceLimits,
57) -> Result<(Vec<u8>, ExportOutcome)> {
58    let mut bytes = Vec::new();
59    let outcome = export_dataset_csv(spec, dataset, limits, &mut bytes)?;
60    Ok((bytes, outcome))
61}
62
63fn write_record<T, S>(writer: &mut BoundedWriter<'_>, values: T) -> Result<()>
64where
65    T: IntoIterator<Item = S>,
66    S: AsRef<str>,
67{
68    let mut first = true;
69    for value in values {
70        if !first {
71            writer.write_all(b",")?;
72        }
73        first = false;
74        let value = value.as_ref();
75        write_field(writer, value)?;
76    }
77    writer.write_all(b"\r\n")
78}
79
80fn write_field(writer: &mut BoundedWriter<'_>, value: &str) -> Result<()> {
81    if !value.contains([',', '"', '\r', '\n']) {
82        return writer.write_all(value.as_bytes());
83    }
84    writer.write_all(b"\"")?;
85    let bytes = value.as_bytes();
86    let mut start = 0;
87    for (index, _) in value.match_indices('"') {
88        writer.write_all(&bytes[start..index])?;
89        writer.write_all(b"\"\"")?;
90        start = index + 1;
91    }
92    writer.write_all(&bytes[start..])?;
93    writer.write_all(b"\"")
94}
95
96fn display_value(value: &crate::DataValue) -> Cow<'_, str> {
97    match value {
98        crate::DataValue::String(value)
99        | crate::DataValue::Date(value)
100        | crate::DataValue::DateTime(value) => Cow::Borrowed(value),
101        crate::DataValue::Array(_) => Cow::Borrowed("[array]"),
102        crate::DataValue::Object(_) => Cow::Borrowed("[object]"),
103        crate::DataValue::Null => Cow::Borrowed(""),
104        _ => Cow::Owned(value.display()),
105    }
106}
107
108struct BoundedWriter<'a> {
109    inner: &'a mut dyn Write,
110    limit: usize,
111    written: usize,
112}
113
114impl<'a> BoundedWriter<'a> {
115    const fn new(inner: &'a mut dyn Write, limit: usize) -> Self {
116        Self {
117            inner,
118            limit,
119            written: 0,
120        }
121    }
122
123    fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
124        let next = self
125            .written
126            .checked_add(bytes.len())
127            .ok_or_else(|| limit_error("CSV byte accounting overflow"))?;
128        if next > self.limit {
129            return Err(limit_error("CSV exceeds configured output limit"));
130        }
131        self.inner
132            .write_all(bytes)
133            .map_err(|error| FileMakerError::new(ErrorCode::ExportWrite, error.to_string()))?;
134        self.written = next;
135        Ok(())
136    }
137}
138
139fn limit_error(message: impl Into<String>) -> FileMakerError {
140    FileMakerError::new(ErrorCode::LimitExceeded, message)
141}