Skip to content

Commit f53bde2

Browse files
committed
Avoid coupling of the form fields to the API. Make the API more real-world by making it return error codes.
1 parent f08b425 commit f53bde2

File tree

1 file changed

+79
-29
lines changed

1 file changed

+79
-29
lines changed

examples/forms-signup.rs

+79-29
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ enum NewUserState {
5252
}
5353

5454
#[derive(Default)]
55-
struct SignupFormFields {
55+
struct SignupFormFieldState {
5656
username: Dynamic::<String>,
5757
password: Dynamic::<MaskedString>,
5858
}
5959

60-
impl SignupFormFields {
60+
impl SignupFormFieldState {
6161
pub fn result(&self) -> LoginArgs {
6262
LoginArgs {
6363
username: self.username.get(),
@@ -75,9 +75,14 @@ struct LoginArgs {
7575
#[derive(Default)]
7676
struct SignupForm {
7777
state: Dynamic::<NewUserState>,
78-
fields: SignupFormFields,
78+
fields: SignupFormFieldState,
7979
}
8080

81+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
82+
enum SignupFormField {
83+
Username,
84+
Password,
85+
}
8186

8287
impl SignupForm {
8388
fn build(self,
@@ -94,20 +99,21 @@ impl SignupForm {
9499
// A network request can take time, so rather than waiting on the API call
95100
// once we are ready to submit the form, we delegate the login process to a
96101
// background task using a channel.
97-
let api_errors = Dynamic::default();
102+
let field_errors: Dynamic<Map<SignupFormField, String>> = Dynamic::default();
103+
98104
let login_handler = channel::build()
99105
.on_receive({
100106
let form_state = self.state.clone();
101107
let app_state = app_state.clone();
102108
let api = api.clone();
103-
let api_errors = api_errors.clone();
109+
let form_errors = field_errors.clone();
104110
move |login_args: LoginArgs| {
105111
handle_login(
106112
login_args,
107113
&api,
108114
&app_state,
109115
&form_state,
110-
&api_errors,
116+
&form_errors,
111117
);
112118
}
113119
})
@@ -137,9 +143,9 @@ impl SignupForm {
137143
// callback and any error returned from the API for this field.
138144
let username_field = "Username"
139145
.and(
140-
validated_field(SignupField::Username, form_fields.username
146+
validated_field(SignupFormField::Username, form_fields.username
141147
.to_input()
142-
.placeholder("Username"), &form_fields.username, &validations, &api_errors, |username| {
148+
.placeholder("Username"), &form_fields.username, &validations, &field_errors, |username| {
143149
if username.is_empty() {
144150
Err(String::from(
145151
"usernames must contain at least one character",
@@ -161,11 +167,11 @@ impl SignupForm {
161167
let password_field = "Password"
162168
.and(
163169
validated_field(
164-
SignupField::Password,
170+
SignupFormField::Password,
165171
form_fields.password.to_input().placeholder("Password"),
166172
&form_fields.password,
167173
&validations,
168-
&api_errors,
174+
&field_errors,
169175
|password| {
170176
if password.len() < 8 {
171177
Err(String::from("passwords must be at least 8 characters long"))
@@ -236,24 +242,23 @@ impl SignupForm {
236242
.scroll()
237243
.centered()
238244
}
239-
240245
}
241246

242247

243248
/// Returns `widget` that is validated using `validate` and `api_errors`.
244249
fn validated_field<T>(
245-
field: SignupField,
250+
form_field: SignupFormField,
246251
widget: impl MakeWidget,
247252
value: &Dynamic<T>,
248253
validations: &Validations,
249-
api_errors: &Dynamic<Map<SignupField, String>>,
254+
form_errors: &Dynamic<Map<SignupFormField, String>>,
250255
mut validate: impl FnMut(&T) -> Result<(), String> + Send + 'static,
251256
) -> Validated
252257
where
253258
T: Send + 'static,
254259
{
255260
// Create a dynamic that contains the error for this field, or None.
256-
let api_error = api_errors.map_each(move |errors| errors.get(&field).cloned());
261+
let api_error = form_errors.map_each(move |errors| errors.get(&form_field).cloned());
257262
// When the underlying value has been changed, we should invalidate the API
258263
// error since the edited value needs to be re-checked by the API.
259264
value
@@ -295,7 +300,7 @@ fn handle_login(
295300
api: &channel::Sender<FakeApiRequest>,
296301
app_state: &Dynamic<AppState>,
297302
form_state: &Dynamic<NewUserState>,
298-
api_errors: &Dynamic<Map<SignupField, String>>,
303+
form_errors: &Dynamic<Map<SignupFormField, String>>,
299304
) {
300305
let request = FakeApiRequestKind::SignUp {
301306
username: login_args.username.clone(),
@@ -310,9 +315,30 @@ fn handle_login(
310315
app_state.set(AppState::LoggedIn { username: login_args.username });
311316
form_state.set(NewUserState::Done);
312317
}
313-
FakeApiResponse::SignUpFailure(errors) => {
318+
FakeApiResponse::SignUpFailure(mut errors) => {
314319
form_state.set(NewUserState::FormEntry);
315-
api_errors.set(errors);
320+
321+
// match up the API errors to form errors, there may not be a 1:1 relationship with form fields and api errors
322+
let mut mapped_errors: Map<SignupFormField, String> = Default::default();
323+
324+
for code in errors.drain(..).into_iter() {
325+
match code.try_into() {
326+
Ok(FakeApiSignupErrorCode::UsernameReserved) |
327+
Ok(FakeApiSignupErrorCode::UsernameUnavailable)
328+
=> {
329+
// handle the two cases with the same error message
330+
mapped_errors.insert(SignupFormField::Username, String::from("Username is a unavailable"));
331+
},
332+
Ok(FakeApiSignupErrorCode::PasswordInsecure) => {
333+
mapped_errors.insert(SignupFormField::Password, String::from("Password is insecure"));
334+
},
335+
Err(_) => {
336+
// another error occurred with the API, but this implementation doesn't know how to handle it
337+
}
338+
}
339+
}
340+
341+
form_errors.set(mapped_errors);
316342
}
317343
}
318344
}
@@ -345,14 +371,35 @@ struct FakeApiRequest {
345371

346372
#[derive(Debug)]
347373
enum FakeApiResponse {
348-
SignUpFailure(Map<SignupField, String>),
374+
// the API returns numbers, which needs to be mapped to a specific error message
375+
SignUpFailure(Vec<u32>),
349376
SignUpSuccess,
350377
}
351378

352-
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
353-
enum SignupField {
354-
Username,
355-
Password,
379+
#[repr(u32)]
380+
enum FakeApiSignupErrorCode {
381+
UsernameReserved = 42,
382+
UsernameUnavailable = 3,
383+
PasswordInsecure = 69,
384+
}
385+
386+
impl TryFrom<u32> for FakeApiSignupErrorCode {
387+
type Error = ();
388+
389+
fn try_from(value: u32) -> Result<Self, Self::Error> {
390+
match value {
391+
42 => Ok(FakeApiSignupErrorCode::UsernameReserved),
392+
3 => Ok(FakeApiSignupErrorCode::UsernameUnavailable),
393+
69 => Ok(FakeApiSignupErrorCode::PasswordInsecure),
394+
_ => Err(()),
395+
}
396+
}
397+
}
398+
399+
impl Into<u32> for FakeApiSignupErrorCode {
400+
fn into(self) -> u32 {
401+
self as u32
402+
}
356403
}
357404

358405
fn fake_service(request: FakeApiRequest) {
@@ -361,17 +408,20 @@ fn fake_service(request: FakeApiRequest) {
361408
// Simulate this api taking a while
362409
thread::sleep(Duration::from_secs(1));
363410

364-
let mut errors = Map::new();
411+
let mut errors: Vec<u32> = Vec::default();
365412
if username == "admin" {
366-
errors.insert(
367-
SignupField::Username,
368-
String::from("admin is a reserved username"),
413+
errors.push(
414+
FakeApiSignupErrorCode::UsernameReserved.into(),
415+
);
416+
}
417+
if username == "user" {
418+
errors.push(
419+
FakeApiSignupErrorCode::UsernameUnavailable.into(),
369420
);
370421
}
371422
if *password == "password" {
372-
errors.insert(
373-
SignupField::Password,
374-
String::from("'password' is not a strong password"),
423+
errors.push(
424+
FakeApiSignupErrorCode::PasswordInsecure.into(),
375425
);
376426
}
377427

0 commit comments

Comments
 (0)