Skip to main content

tablo_test/protocol/
import.rs

1//! Reads a CSV import page: the alert refusing the whole file names why in
2//! `data-import-refusal`, each refused value's row carries its row number in
3//! `data-import-error` and its form key in `data-import-key`, and each column the import takes
4//! carries its key in `data-import-column`.
5
6use tablo_core::dom::import;
7
8use super::dom::{attr_value, tags_where};
9
10/// Why an import refused a whole file, as its alert's `data-import-refusal` names it.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum ImportRefusal {
14    /// The submission carries no file.
15    NoFile,
16    /// The file is not UTF-8 text.
17    Encoding,
18    /// A quoted cell is never closed.
19    Unterminated,
20    /// The file holds no header row.
21    NoHeader,
22    /// The file holds a header but no row.
23    NoRows,
24    /// The file holds more rows than one import creates.
25    TooManyRows,
26    /// A row of the file holds more cells than one import reads.
27    TooManyCells,
28    /// The header names a column no field the request may change takes.
29    UnknownColumn,
30    /// The header names a column by a label more than one field carries.
31    AmbiguousColumn,
32    /// The header names a single-value column twice.
33    RepeatedColumn,
34}
35
36/// One value an import refused.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ImportError {
39    /// The row's number as a spreadsheet shows it, counting the header and blank rows.
40    pub row: usize,
41    /// The form key refused, `None` for the row as a whole or a field the user may not see.
42    pub key: Option<String>,
43}
44
45/// Why the import page of `html` refused the whole file, or `None` when it did not.
46///
47/// # Panics
48///
49/// When the alert names no refusal this version knows: a renderer that drops
50/// `data-import-refusal`'s value would otherwise read as any refusal.
51pub fn import_refusal(html: &str) -> Option<ImportRefusal> {
52    let (_, attrs, _) = tags_where(html, |_, attrs| {
53        attr_value(attrs, "data-import-refusal").is_some()
54    })
55    .into_iter()
56    .next()?;
57    Some(match attr_value(&attrs, "data-import-refusal").as_deref() {
58        Some(import::NO_FILE) => ImportRefusal::NoFile,
59        Some(import::ENCODING) => ImportRefusal::Encoding,
60        Some(import::UNTERMINATED) => ImportRefusal::Unterminated,
61        Some(import::NO_HEADER) => ImportRefusal::NoHeader,
62        Some(import::NO_ROWS) => ImportRefusal::NoRows,
63        Some(import::TOO_MANY_ROWS) => ImportRefusal::TooManyRows,
64        Some(import::TOO_MANY_CELLS) => ImportRefusal::TooManyCells,
65        Some(import::UNKNOWN_COLUMN) => ImportRefusal::UnknownColumn,
66        Some(import::AMBIGUOUS_COLUMN) => ImportRefusal::AmbiguousColumn,
67        Some(import::REPEATED_COLUMN) => ImportRefusal::RepeatedColumn,
68        other => panic!("the import alert names no known refusal: {other:?}"),
69    })
70}
71
72/// The values the import page of `html` lists as refused, in its order; empty when it lists
73/// none.
74///
75/// # Panics
76///
77/// When an entry's row number does not parse.
78pub fn import_errors(html: &str) -> Vec<ImportError> {
79    tags_where(html, |_, attrs| {
80        attr_value(attrs, "data-import-error").is_some()
81    })
82    .into_iter()
83    .map(|(_, attrs, _)| ImportError {
84        row: attr_value(&attrs, "data-import-error")
85            .and_then(|row| row.parse().ok())
86            .expect("an import error names its row"),
87        key: attr_value(&attrs, "data-import-key").filter(|key| !key.is_empty()),
88    })
89    .collect()
90}
91
92/// The form keys the import page of `html` lists as the columns a file may name, in order.
93pub fn import_columns(html: &str) -> Vec<String> {
94    tags_where(html, |_, attrs| {
95        attr_value(attrs, "data-import-column").is_some()
96    })
97    .into_iter()
98    .filter_map(|(_, attrs, _)| attr_value(&attrs, "data-import-column"))
99    .collect()
100}