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, and a repeater's rows in the order its key lists them. A field that
70    /// posts nothing holds nothing, so an edit keeps its stored value. A key no field declares is
71    /// left out: a page reads its own, such as the CSRF token, from the pairs.
72    ///
73    /// A text control's value is read through its type in the request's
74    /// [`time_zone`](crate::time_zone): a timestamp's control posts a wall-clock time, which reads
75    /// as the instant it names there.
76    ///
77    /// A resource's form and an action's input read their submissions; a page handling its own
78    /// post reads it before parsing it.
79    ///
80    /// # Errors
81    ///
82    /// A repeater's key lists something other than its rows, a row twice, a row posting nothing,
83    /// or more than [`MAX_REPEATER_ROWS`](super::MAX_REPEATER_ROWS) rows: no browser posts these,
84    /// so a handler answers 400.
85    pub fn read(
86        &self,
87        cx: &Cx,
88        posted: &[(String, String)],
89    ) -> std::result::Result<FormValues, String> {
90        self.read_in(posted, crate::time_zone(cx))
91    }
92
93    /// [`read`](Self::read) in `zone`.
94    pub(super) fn read_in(
95        &self,
96        posted: &[(String, String)],
97        zone: TimeZone,
98    ) -> std::result::Result<FormValues, String> {
99        self.read_claiming(&mut Pairs::new(posted, zone))
100    }
101
102    /// [`read`](Self::read), with the keys no field claims, sorted and each once.
103    pub(crate) fn read_unclaimed(
104        &self,
105        cx: &Cx,
106        posted: &[(String, String)],
107    ) -> std::result::Result<(FormValues, Vec<String>), String> {
108        let mut pairs = Pairs::new(posted, crate::time_zone(cx));
109        let values = self.read_claiming(&mut pairs)?;
110        Ok((values, pairs.unclaimed()))
111    }
112
113    fn read_claiming(&self, pairs: &mut Pairs<'_>) -> std::result::Result<FormValues, String> {
114        let mut values = FormValues::new();
115        for field in &self.fields {
116            let name = field.name();
117            if let Some(repeater) = field.as_repeater() {
118                if let Some(rows) = repeater.read(name, pairs)? {
119                    values.insert(name, rows);
120                }
121                continue;
122            }
123            let posted = pairs.claim(name);
124            if field.as_choice().is_some_and(ChoiceControl::is_multiple) {
125                if !posted.is_empty() {
126                    values.insert(name, distinct(&posted));
127                }
128            } else if let Some(last) = posted.last() {
129                values.insert(name, field.read_posted(last, &pairs.zone));
130            }
131        }
132        Ok(values)
133    }
134
135    /// A posted input as its parse reads it: [read](Self::read), with a condition-hidden field's
136    /// key removed, since the browser posts nothing for it.
137    ///
138    /// # Errors
139    ///
140    /// 400 for a malformed repeater and for a key that neither a field declares nor `reserved`
141    /// names: the keys the POST carries for itself.
142    pub(crate) fn read_input(
143        &self,
144        cx: &Cx,
145        posted: &[(String, String)],
146        reserved: &[&str],
147    ) -> Result<FormValues> {
148        use topcoat::router::error::bad_request;
149
150        let (mut input, unclaimed) = self.read_unclaimed(cx, posted).map_err(bad_request)?;
151        let unknown: Vec<String> = unclaimed
152            .into_iter()
153            .filter(|key| !reserved.contains(&key.as_str()))
154            .collect();
155        if !unknown.is_empty() {
156            return Err(bad_request(format!("unknown field(s): {}", unknown.join(", "))).into());
157        }
158        // A disabled field's default first, which a condition reads as the browser did.
159        self.drop_disabled(&mut input, false);
160        for key in self.condition_hidden(&input) {
161            input.remove(&key);
162        }
163        Ok(input)
164    }
165}
166
167/// A multiple choice's posted values, trimmed, each once and none blank: its hidden empty value,
168/// which posts the key when no box is checked, drops here.
169fn distinct(posted: &[&str]) -> FormValue {
170    let mut seen = HashSet::new();
171    FormValue::List(
172        posted
173            .iter()
174            .map(|value| value.trim())
175            .filter(|value| !value.is_empty() && seen.insert(*value))
176            .map(str::to_string)
177            .collect(),
178    )
179}
180
181#[cfg(test)]
182mod tests;