@@ -52,12 +52,12 @@ enum NewUserState {
52
52
}
53
53
54
54
#[ derive( Default ) ]
55
- struct SignupFormFields {
55
+ struct SignupFormFieldState {
56
56
username : Dynamic :: < String > ,
57
57
password : Dynamic :: < MaskedString > ,
58
58
}
59
59
60
- impl SignupFormFields {
60
+ impl SignupFormFieldState {
61
61
pub fn result ( & self ) -> LoginArgs {
62
62
LoginArgs {
63
63
username : self . username . get ( ) ,
@@ -75,9 +75,14 @@ struct LoginArgs {
75
75
#[ derive( Default ) ]
76
76
struct SignupForm {
77
77
state : Dynamic :: < NewUserState > ,
78
- fields : SignupFormFields ,
78
+ fields : SignupFormFieldState ,
79
79
}
80
80
81
+ #[ derive( Debug , Clone , Copy , PartialEq , Eq , PartialOrd , Ord ) ]
82
+ enum SignupFormField {
83
+ Username ,
84
+ Password ,
85
+ }
81
86
82
87
impl SignupForm {
83
88
fn build ( self ,
@@ -94,20 +99,21 @@ impl SignupForm {
94
99
// A network request can take time, so rather than waiting on the API call
95
100
// once we are ready to submit the form, we delegate the login process to a
96
101
// background task using a channel.
97
- let api_errors = Dynamic :: default ( ) ;
102
+ let field_errors: Dynamic < Map < SignupFormField , String > > = Dynamic :: default ( ) ;
103
+
98
104
let login_handler = channel:: build ( )
99
105
. on_receive ( {
100
106
let form_state = self . state . clone ( ) ;
101
107
let app_state = app_state. clone ( ) ;
102
108
let api = api. clone ( ) ;
103
- let api_errors = api_errors . clone ( ) ;
109
+ let form_errors = field_errors . clone ( ) ;
104
110
move |login_args : LoginArgs | {
105
111
handle_login (
106
112
login_args,
107
113
& api,
108
114
& app_state,
109
115
& form_state,
110
- & api_errors ,
116
+ & form_errors ,
111
117
) ;
112
118
}
113
119
} )
@@ -137,9 +143,9 @@ impl SignupForm {
137
143
// callback and any error returned from the API for this field.
138
144
let username_field = "Username"
139
145
. and (
140
- validated_field ( SignupField :: Username , form_fields. username
146
+ validated_field ( SignupFormField :: Username , form_fields. username
141
147
. to_input ( )
142
- . placeholder ( "Username" ) , & form_fields. username , & validations, & api_errors , |username| {
148
+ . placeholder ( "Username" ) , & form_fields. username , & validations, & field_errors , |username| {
143
149
if username. is_empty ( ) {
144
150
Err ( String :: from (
145
151
"usernames must contain at least one character" ,
@@ -161,11 +167,11 @@ impl SignupForm {
161
167
let password_field = "Password"
162
168
. and (
163
169
validated_field (
164
- SignupField :: Password ,
170
+ SignupFormField :: Password ,
165
171
form_fields. password . to_input ( ) . placeholder ( "Password" ) ,
166
172
& form_fields. password ,
167
173
& validations,
168
- & api_errors ,
174
+ & field_errors ,
169
175
|password| {
170
176
if password. len ( ) < 8 {
171
177
Err ( String :: from ( "passwords must be at least 8 characters long" ) )
@@ -236,24 +242,23 @@ impl SignupForm {
236
242
. scroll ( )
237
243
. centered ( )
238
244
}
239
-
240
245
}
241
246
242
247
243
248
/// Returns `widget` that is validated using `validate` and `api_errors`.
244
249
fn validated_field < T > (
245
- field : SignupField ,
250
+ form_field : SignupFormField ,
246
251
widget : impl MakeWidget ,
247
252
value : & Dynamic < T > ,
248
253
validations : & Validations ,
249
- api_errors : & Dynamic < Map < SignupField , String > > ,
254
+ form_errors : & Dynamic < Map < SignupFormField , String > > ,
250
255
mut validate : impl FnMut ( & T ) -> Result < ( ) , String > + Send + ' static ,
251
256
) -> Validated
252
257
where
253
258
T : Send + ' static ,
254
259
{
255
260
// 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 ( ) ) ;
257
262
// When the underlying value has been changed, we should invalidate the API
258
263
// error since the edited value needs to be re-checked by the API.
259
264
value
@@ -295,7 +300,7 @@ fn handle_login(
295
300
api : & channel:: Sender < FakeApiRequest > ,
296
301
app_state : & Dynamic < AppState > ,
297
302
form_state : & Dynamic < NewUserState > ,
298
- api_errors : & Dynamic < Map < SignupField , String > > ,
303
+ form_errors : & Dynamic < Map < SignupFormField , String > > ,
299
304
) {
300
305
let request = FakeApiRequestKind :: SignUp {
301
306
username : login_args. username . clone ( ) ,
@@ -310,9 +315,30 @@ fn handle_login(
310
315
app_state. set ( AppState :: LoggedIn { username : login_args. username } ) ;
311
316
form_state. set ( NewUserState :: Done ) ;
312
317
}
313
- FakeApiResponse :: SignUpFailure ( errors) => {
318
+ FakeApiResponse :: SignUpFailure ( mut errors) => {
314
319
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) ;
316
342
}
317
343
}
318
344
}
@@ -345,14 +371,35 @@ struct FakeApiRequest {
345
371
346
372
#[ derive( Debug ) ]
347
373
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 > ) ,
349
376
SignUpSuccess ,
350
377
}
351
378
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
+ }
356
403
}
357
404
358
405
fn fake_service ( request : FakeApiRequest ) {
@@ -361,17 +408,20 @@ fn fake_service(request: FakeApiRequest) {
361
408
// Simulate this api taking a while
362
409
thread:: sleep ( Duration :: from_secs ( 1 ) ) ;
363
410
364
- let mut errors = Map :: new ( ) ;
411
+ let mut errors: Vec < u32 > = Vec :: default ( ) ;
365
412
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 ( ) ,
369
420
) ;
370
421
}
371
422
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 ( ) ,
375
425
) ;
376
426
}
377
427
0 commit comments