Skip to main content

tablo_core/schema/
read.rs

1//! Reading a posted body into the values a schema's fields hold.
2//!
3//! A body posts flat `(key, value)` pairs. Its schema reads each field's key in the shape the
4//! field's control posts: a scalar control's last value, a multiple choice's every value, and a
5//! repeater's rows. Every later step, from completing an edit to parsing, then reads one value per
6//! key.
7
8use std::collections::{BTreeSet, HashMap, HashSet};
9
10use topcoat::Result;
11
12use super::{Schema, fields::ChoiceControl};
13use crate::form::{FormValue, FormValues};
14
15/// A posted body's pairs, indexed by key, recording which ones a field claimed.
16pub(super) struct Pairs<'p> {
17    pairs: &'p [(String, String)],
18    at: HashMap<&'p str, Vec<usize>>,
19    claimed: Vec<bool>,
20}
21
22impl<'p> Pairs<'p> {
23    fn new(pairs: &'p [(String, String)]) -> Self {
24        let mut at: HashMap<&str, Vec<usize>> = HashMap::new();
25        for (index, (key, _)) in pairs.iter().enumerate() {
26            at.entry(key.as_str()).or_default().push(index);
27        }
28        Self {
29            pairs,
30            at,
31            claimed: vec![false; pairs.len()],
32        }
33    }
34
35    /// Claims every value posted under `key`, in the order the body posts them.
36    pub(super) fn claim(&mut self, key: &str) -> Vec<&'p str> {
37        let Some(indices) = self.at.get(key) else {
38            return Vec::new();
39        };
40        indices
41            .iter()
42            .map(|&index| {
43                self.claimed[index] = true;
44                self.pairs[index].1.as_str()
45            })
46            .collect()
47    }
48
49    /// The keys no field claimed, sorted, each once.
50    fn unclaimed(&self) -> Vec<String> {
51        self.pairs
52            .iter()
53            .zip(&self.claimed)
54            .filter(|(_, claimed)| !**claimed)
55            .map(|((key, _), _)| key.clone())
56            .collect::<BTreeSet<_>>()
57            .into_iter()
58            .collect()
59    }
60}
61
62impl<F> Schema<F> {
63    /// Reads a posted body's `(key, value)` pairs, in the order it posts them, into the values
64    /// the schema's fields hold: a scalar control's last value, a multiple choice's values without
65    /// blanks or repeats, and a repeater's rows in the order its key lists them. A field that
66    /// posts nothing holds nothing, so an edit keeps its stored value. A key no field declares is
67    /// left out: a page reads its own, such as the CSRF token, from the pairs.
68    ///
69    /// A resource's form and an action's input read their submissions; a page handling its own
70    /// post reads it before parsing it.
71    ///
72    /// # Errors
73    ///
74    /// A repeater's key lists something other than its rows, a row twice, a row posting nothing,
75    /// or more than [`MAX_ROWS`](super::MAX_ROWS) rows: no browser posts these, so a handler
76    /// answers 400.
77    pub fn read(&self, posted: &[(String, String)]) -> std::result::Result<FormValues, String> {
78        self.read_claiming(&mut Pairs::new(posted))
79    }
80
81    /// [`read`](Self::read), with the keys no field claims, sorted and each once.
82    pub(crate) fn read_unclaimed(
83        &self,
84        posted: &[(String, String)],
85    ) -> std::result::Result<(FormValues, Vec<String>), String> {
86        let mut pairs = Pairs::new(posted);
87        let values = self.read_claiming(&mut pairs)?;
88        Ok((values, pairs.unclaimed()))
89    }
90
91    fn read_claiming(&self, pairs: &mut Pairs<'_>) -> std::result::Result<FormValues, String> {
92        let mut values = FormValues::new();
93        for field in &self.fields {
94            let name = field.name();
95            if let Some(repeater) = field.as_repeater() {
96                if let Some(rows) = repeater.read(name, pairs)? {
97                    values.insert(name, rows);
98                }
99                continue;
100            }
101            let posted = pairs.claim(name);
102            if field.as_choice().is_some_and(ChoiceControl::is_multiple) {
103                if !posted.is_empty() {
104                    values.insert(name, distinct(&posted));
105                }
106            } else if let Some(last) = posted.last() {
107                values.insert(name, *last);
108            }
109        }
110        Ok(values)
111    }
112
113    /// A posted input as its parse reads it: [read](Self::read), with a condition-hidden field's
114    /// key removed, since the browser posts nothing for it.
115    ///
116    /// # Errors
117    ///
118    /// 400 for a malformed repeater and for a key that neither a field declares nor `reserved`
119    /// names: the keys the POST carries for itself.
120    pub(crate) fn read_input(
121        &self,
122        posted: &[(String, String)],
123        reserved: &[&str],
124    ) -> Result<FormValues> {
125        use topcoat::router::error::bad_request;
126
127        let (mut input, unclaimed) = self.read_unclaimed(posted).map_err(bad_request)?;
128        let unknown: Vec<String> = unclaimed
129            .into_iter()
130            .filter(|key| !reserved.contains(&key.as_str()))
131            .collect();
132        if !unknown.is_empty() {
133            return Err(bad_request(format!("unknown field(s): {}", unknown.join(", "))).into());
134        }
135        // A disabled field's default first, which a condition reads as the browser did.
136        self.drop_disabled(&mut input, false);
137        for key in self.condition_hidden(&input) {
138            input.remove(&key);
139        }
140        Ok(input)
141    }
142}
143
144/// A multiple choice's posted values, trimmed, each once and none blank: its hidden empty value,
145/// which posts the key when no box is checked, drops here.
146fn distinct(posted: &[&str]) -> FormValue {
147    let mut seen = HashSet::new();
148    FormValue::List(
149        posted
150            .iter()
151            .map(|value| value.trim())
152            .filter(|value| !value.is_empty() && seen.insert(*value))
153            .map(str::to_string)
154            .collect(),
155    )
156}
157
158#[cfg(test)]
159mod tests;