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