@@ -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 ) ]
7676struct 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
8287impl 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`.
244249fn 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
252257where
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 ) ]
347373enum 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
358405fn 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