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 jiff::tz::TimeZone;
11use topcoat::{Result, context::Cx};
12
13use super::{Schema, fields::ChoiceControl};
14use crate::values::{FormValue, FormValues};
15
16/// A posted body's pairs, indexed by key, recording which ones a field claimed, and the time zone
17/// its wall-clock times are read in.
18pub(super) struct Pairs<'p> {
19    pairs: &'p [(String, String)],
20    at: HashMap<&'p str, Vec<usize>>,
21    claimed: Vec<bool>,
22    pub(super) zone: TimeZone,
23}
24
25impl<'p> Pairs<'p> {
26    fn new(pairs: &'p [(String, String)], zone: TimeZone) -> Self {
27        let mut at: HashMap<&str, Vec<usize>> = HashMap::new();
28        for (index, (key, _)) in pairs.iter().enumerate() {
29            at.entry(key.as_str()).or_default().push(index);
30        }
31        Self {
32            pairs,
33            at,
34            claimed: vec![false; pairs.len()],
35            zone,
36        }
37    }
38
39    /// Claims every value posted under `key`, in the order the body posts them.
40    pub(super) fn claim(&mut self, key: &str) -> Vec<&'p str> {
41        let Some(indices) = self.at.get(key) else {
42            return Vec::new();
43        };
44        indices
45            .iter()
46            .map(|&index| {
47                self.claimed[index] = true;
48                self.pairs[index].1.as_str()
49            })
50            .collect()
51    }
52
53    /// The keys no field claimed, sorted, each once.
54    fn unclaimed(&self) -> Vec<String> {
55        self.pairs
56            .iter()
57            .zip(&self.claimed)
58            .filter(|(_, claimed)| !**claimed)
59            .map(|((key, _), _)| key.clone())
60            .collect::<BTreeSet<_>>()
61            .into_iter()
62            .collect()
63    }
64}
65
66impl<F> Schema<F> {
67    /// Reads a posted body's `(key, value)` pairs, in the order it posts them, into the values
68    /// the schema's fields hold: a scalar control's last value, a multiple choice's values without
69    /// blanks or repeats, a file list's chosen file names likewise, and a repeater's rows in the
70    /// order its key lists them. A field that posts nothing holds nothing, so an edit keeps its
71    /// stored value. A key no field declares is left out: a page reads its own, such as the
72    /// CSRF token, from the pairs.
73    ///
74    /// A text control's value is read through its type in the request's
75    /// [`time_zone`](crate::time_zone): a timestamp's control posts a wall-clock time, which reads
76    /// as the instant it names there.
77    ///
78    /// A resource's form and an action's input read their submissions; a page handling its own
79    /// post reads it before parsing it.
80    ///
81    /// # Errors
82    ///
83    /// A repeater's key lists something other than its rows, a row twice, a row posting nothing,
84    /// or more than [`MAX_REPEATER_ROWS`](super::MAX_REPEATER_ROWS) rows: no browser posts these,
85    /// so a handler answers 400.
86    pub fn read(
87        &self,
88        cx: &Cx,
89        posted: &[(String, String)],
90    ) -> std::result::Result<FormValues, String> {
91        self.read_in(posted, crate::time_zone(cx))
92    }
93
94    /// [`read`](Self::read) in `zone`.
95    pub(super) fn read_in(
96        &self,
97        posted: &[(String, String)],
98        zone: TimeZone,
99    ) -> std::result::Result<FormValues, String> {
100        self.read_claiming(&mut Pairs::new(posted, zone))
101    }
102
103    /// [`read`](Self::read), with the keys no field claims, sorted and each once.
104    pub(crate) fn read_unclaimed(
105        &self,
106        cx: &Cx,
107        posted: &[(String, String)],
108    ) -> std::result::Result<(FormValues, Vec<String>), String> {
109        let mut pairs = Pairs::new(posted, crate::time_zone(cx));
110        let values = self.read_claiming(&mut pairs)?;
111        Ok((values, pairs.unclaimed()))
112    }
113
114    fn read_claiming(&self, pairs: &mut Pairs<'_>) -> std::result::Result<FormValues, String> {
115        let mut values = FormValues::new();
116        for field in &self.fields {
117            let name = field.name();
118            if let Some(repeater) = field.as_repeater() {
119                if let Some(rows) = repeater.read(name, pairs)? {
120                    values.insert(name, rows);
121                }
122                continue;
123            }
124            let mut posted = pairs.claim(name);
125            if field.as_choice().is_some_and(ChoiceControl::is_tags) {
126                // The text input adds several tags at once, separated by commas.
127                posted = posted.iter().flat_map(|value| value.split(',')).collect();
128            }
129            if field.as_choice().is_some_and(ChoiceControl::is_multiple) || field.is_file_list() {
130                if !posted.is_empty() {
131                    values.insert(name, distinct(&posted));
132                }
133            } else if let Some(last) = posted.last() {
134                values.insert(name, field.read_posted(last, &pairs.zone));
135            }
136        }
137        Ok(values)
138    }
139
140    /// A posted input as its parse reads it: [read](Self::read), with a condition-hidden field's
141    /// key removed and a condition-disabled field's replaced by its default, since the browser
142    /// posts nothing for either.
143    ///
144    /// # Errors
145    ///
146    /// 400 for a malformed repeater and for a key that neither a field declares nor `reserved`
147    /// names: the keys the POST carries for itself.
148    pub(crate) fn read_input(
149        &self,
150        cx: &Cx,
151        posted: &[(String, String)],
152        reserved: &[&str],
153    ) -> Result<FormValues> {
154        use topcoat::router::error::bad_request;
155
156        let (mut input, unclaimed) = self.read_unclaimed(cx, posted).map_err(bad_request)?;
157        let unknown: Vec<String> = unclaimed
158            .into_iter()
159            .filter(|key| !reserved.contains(&key.as_str()))
160            .collect();
161        if !unknown.is_empty() {
162            return Err(bad_request(format!("unknown field(s): {}", unknown.join(", "))).into());
163        }
164        // A disabled field's default first, which a condition reads as the browser did.
165        self.drop_disabled(&mut input, false);
166        for key in self.condition_hidden(&input) {
167            input.remove(&key);
168        }
169        let disabled = self.condition_disabled(&input);
170        for key in &disabled {
171            input.remove(key);
172        }
173        input.extend(self.defaults_of(disabled.iter().map(String::as_str)));
174        Ok(input)
175    }
176}
177
178/// A multiple choice's posted values, trimmed, each once and none blank: its hidden empty value,
179/// which posts the key when no box is checked, drops here.
180fn distinct(posted: &[&str]) -> FormValue {
181    let mut seen = HashSet::new();
182    FormValue::List(
183        posted
184            .iter()
185            .map(|value| value.trim())
186            .filter(|value| !value.is_empty() && seen.insert(*value))
187            .map(str::to_string)
188            .collect(),
189    )
190}
191
192#[cfg(test)]
193mod tests;