1use std::{collections::HashMap, hash::Hash};
9
10use serde::{Deserialize, Serialize};
11
12pub trait FormField: Copy + Hash + PartialEq + Eq + Serialize + for<'de> Deserialize<'de> {
14 fn keep(&self) -> bool;
17}
18
19#[derive(Debug, Serialize)]
21#[serde(rename_all = "snake_case", tag = "kind")]
22pub enum FieldError {
23 Required,
25
26 Unspecified,
28
29 Invalid,
31
32 PasswordMismatch,
34
35 Exists,
37
38 Mismatch,
40
41 Policy {
43 code: Option<&'static str>,
45
46 message: String,
48 },
49}
50
51#[derive(Debug, Serialize)]
53#[serde(rename_all = "snake_case", tag = "kind")]
54pub enum FormError {
55 InvalidCredentials,
57
58 PasswordMismatch,
60
61 Internal,
63
64 RateLimitExceeded,
66
67 Policy {
69 code: Option<&'static str>,
71
72 message: String,
74 },
75
76 Captcha,
78}
79
80#[derive(Debug, Default, Serialize)]
81struct FieldState {
82 value: Option<String>,
83 errors: Vec<FieldError>,
84}
85
86#[derive(Debug, Serialize)]
88pub struct FormState<K: Hash + Eq> {
89 fields: HashMap<K, FieldState>,
90 errors: Vec<FormError>,
91
92 #[serde(skip)]
93 has_errors: bool,
94}
95
96impl<K: Hash + Eq> Default for FormState<K> {
97 fn default() -> Self {
98 FormState {
99 fields: HashMap::default(),
100 errors: Vec::default(),
101 has_errors: false,
102 }
103 }
104}
105
106#[derive(Deserialize, PartialEq, Eq, Hash)]
107#[serde(untagged)]
108enum KeyOrOther<K> {
109 Key(K),
110 Other(String),
111}
112
113impl<K> KeyOrOther<K> {
114 fn key(self) -> Option<K> {
115 match self {
116 Self::Key(key) => Some(key),
117 Self::Other(_) => None,
118 }
119 }
120}
121
122impl<K: FormField> FormState<K> {
123 pub fn from_form<F: Serialize>(form: &F) -> Self {
130 let form = serde_json::to_value(form).unwrap();
131 let fields: HashMap<KeyOrOther<K>, Option<String>> = serde_json::from_value(form).unwrap();
132
133 let fields = fields
134 .into_iter()
135 .filter_map(|(key, value)| {
136 let key = key.key()?;
137 let value = key.keep().then_some(value).flatten();
138 let field = FieldState {
139 value,
140 errors: Vec::new(),
141 };
142 Some((key, field))
143 })
144 .collect();
145
146 FormState {
147 fields,
148 errors: Vec::new(),
149 has_errors: false,
150 }
151 }
152
153 pub fn add_error_on_field(&mut self, field: K, error: FieldError) {
155 self.fields.entry(field).or_default().errors.push(error);
156 self.has_errors = true;
157 }
158
159 #[must_use]
161 pub fn with_error_on_field(mut self, field: K, error: FieldError) -> Self {
162 self.add_error_on_field(field, error);
163 self
164 }
165
166 pub fn add_error_on_form(&mut self, error: FormError) {
168 self.errors.push(error);
169 self.has_errors = true;
170 }
171
172 #[must_use]
174 pub fn with_error_on_form(mut self, error: FormError) -> Self {
175 self.add_error_on_form(error);
176 self
177 }
178
179 pub fn set_value(&mut self, field: K, value: Option<String>) {
181 self.fields.entry(field).or_default().value = value;
182 }
183
184 pub fn has_value(&self, field: K) -> bool {
186 self.fields.get(&field).is_some_and(|f| f.value.is_some())
187 }
188
189 #[must_use]
191 pub fn is_valid(&self) -> bool {
192 !self.has_errors
193 }
194}
195
196pub trait ToFormState: Serialize {
198 type Field: FormField;
200
201 fn to_form_state(&self) -> FormState<Self::Field> {
207 FormState::from_form(&self)
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[derive(Serialize)]
216 struct TestForm {
217 foo: String,
218 bar: String,
219 }
220
221 #[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
222 #[serde(rename_all = "snake_case")]
223 enum TestFormField {
224 Foo,
225 Bar,
226 }
227
228 impl FormField for TestFormField {
229 fn keep(&self) -> bool {
230 match self {
231 Self::Foo => true,
232 Self::Bar => false,
233 }
234 }
235 }
236
237 impl ToFormState for TestForm {
238 type Field = TestFormField;
239 }
240
241 #[test]
242 fn form_state_serialization() {
243 let form = TestForm {
244 foo: "john".to_owned(),
245 bar: "hunter2".to_owned(),
246 };
247
248 let state = form.to_form_state();
249 let state = serde_json::to_value(state).unwrap();
250 assert_eq!(
251 state,
252 serde_json::json!({
253 "errors": [],
254 "fields": {
255 "foo": {
256 "errors": [],
257 "value": "john",
258 },
259 "bar": {
260 "errors": [],
261 "value": null
262 },
263 }
264 })
265 );
266
267 let form = TestForm {
268 foo: String::new(),
269 bar: String::new(),
270 };
271 let state = form
272 .to_form_state()
273 .with_error_on_field(TestFormField::Foo, FieldError::Required)
274 .with_error_on_field(TestFormField::Bar, FieldError::Required)
275 .with_error_on_form(FormError::InvalidCredentials);
276
277 let state = serde_json::to_value(state).unwrap();
278 assert_eq!(
279 state,
280 serde_json::json!({
281 "errors": [{"kind": "invalid_credentials"}],
282 "fields": {
283 "foo": {
284 "errors": [{"kind": "required"}],
285 "value": "",
286 },
287 "bar": {
288 "errors": [{"kind": "required"}],
289 "value": null
290 },
291 }
292 })
293 );
294 }
295}