Rebol Programming/Language Features/Parse/Screen Validation

Let's build an example of how parsing REBOL values might be used to implement some screen entry validation routines for a database application.

Our design aims are:

  • validate the screen when we click a submit button
  • validate each field without having to explicitly write code to check each field
  • somehow reuse the code we write for other data entry screens.

First off, let's make a start by constructing an entry screen with a few representative fields

lo: layout [
   across
   title "Customer Registration Screen" return
   label "First Name: " fname: field return
   label "Surname: " sname: field return
   label "DOB: " dob: field return
   label "Age: " age: field return
   label "Email: " email: field return
   label "Home Page: " home-page: field return
   button "Register" [ if validate-screen [ register ] ]
]

which looks like this

 ... picture goes here ....
 

When we click on the "Register" button, we want the program to validate all the fields to ensure that they have been all filled in, and with values that are probably appropriate. Our validate-screen function will do this.

First off, we have to collect the values from all the fields. If we knew enough about the internals of VID, we could probably build a function that collects them from the layout lo, but since we don't expect this degree of familiarity with VID internals from our readers we will do this manually.

fields: [ fname sname dob age email home-page ]

At this point, they are just word! values inside a block. We have to force REBOL to evaluate the block so it recognises that they refer to the field objects that were created with the layout function.

fields: reduce [ fname sname dob age email home-page ]

The data from each of the fields happens to be in the "text" attribute of each field. To collect the data in a block, we write a function that iterates through our list of fields, and returns a block containing the data. The function is designed to return a value. If we complete the task we return a block of evaluated fields. Otherwise we return an empty block.

build-data: func [ fields [block!] /local bl ][
   bl: copy []
   foreach field fields [
       append bl field/text
   ]
   bl
]

But this is not quite good enough as it results in a block of text strings. We really want a block of REBOL values to allow us to parse the data by value. So, we force the data into REBOL values by loading them.

build-data: func [ fields [block!] /local bl ][
   bl: copy []
   foreach field fields [
       append bl load field/text
   ]
   bl
]

Although REBOL is quite good at recognising values, sometimes with load we can get an error. Therefore, we should put in an error trap to stop our routines from crashing. We also want to know where the error occurred so we can give feedback to the user.

build-data: func [ fields [block!] /local bl ][
   bl: copy []
   foreach field fields [
       if error? try [
           append bl load field/text
       ][
           focus field
           alert join "Error converting this value " field/text    
           return [] 
       ]
   ]
   bl
]

Now, at this point, we are expecting to have build-data return us a block of REBOL values with the following sequence of datatypes:

[ string! string! date! integer! email! url! ]

Anything that does not conform to this indicates a data entry error.

We can now build a validation rule that checks this, and we will also maintain a counter so that we know where the rule stopped due to bad data.

field-no: 1
incr: func ['word][set word 1 + get word]

validation-rule: [ (field-no: 1)
   set first-name string! ( incr field-no )
   set last-name string! ( incr field-no )
   set date-of-birth date! ( incr field-no )
   set customer-age integer! ( incr field-no )
   set customer-email email! ( incr field-no )
   set customer-web url! ( incr field-no )
]

Our final validate-screen function grabs the data that has been entered, applies the validation rule, and returns a logic! value.

validate-screen: has [ bad-field] [
   either parse build-data fields validation-rule [
       true
   ][
       bad-field: pick fields field-no
       focus bad-field
       show bad-field
       alert "Incorrect data at this field"
       false
   ]
]

So, if build-data does not manage to complete, it returns an empty block which causes our validation-rule to fail. If it does return a block, we put it through our validation-rule and again return the success or failure. If it fails, it puts the cursor in the field with the bad data, and pops up an alert to advise the user.

Although the above satisfies our wish to validate the screen in one go semi-automatically, a consequence of the implementation is that if we change the screen layout by adding or removing fields, we have to change our fields block, and we have to change our validation-rule. That is, we are setting ourselves up for a future failure by creating dependencies elsewhere in our code.

What we want to do is create a data structure where all this information is defined only in one place. Perhaps dialects can help here as well? Maybe we can create a mini data entry dialect that builds the VID code, and validation rule for us?

We need to know 4 essential things about the VID code. We need to know the label text values, the variables associated with each field, a field type for validation purposes, and we need a variable name. So, we need a minimum quartet of values to specify each field. Let us see if we can automate the rest.

So, if we just specify the screen definition as a block of quartets, we get:

screen-definition: [
   "First Name: " fname: string! first-name
   "Surname: " sname: string! last-name
   "DOB: " dob: date! date-of-birth
   "Age: " age: integer! customer-age
   "Email: " email: email! customer-email
   "Home Page: " home-page: url! customer-web
]

We now define a rule that will be used to create the VID code.

screen-def: []

field-rule: [ 
   set prompt string!
   set field-name set-word!
   set field-type word! 
   skip
   ( repend screen-def [ 'label prompt field-name 'field 'return ] )
]

We create the VID block as follows:

if not parse screen-definition [ some field-rule end ][
   print "error in screen definition"
   halt
]

What we do here is to apply the field-rule iteratively until the rule fails. The parser then goes to the next part of the rule, and should match 'end resulting in a successful parse. The block screen-def now holds the VID code that defines the data entry fields.

However, our data entry field also has a button, and a title. So, we add these manually.

sprucedup-def: compose [
   title "Customer Entry Screen"
   across
   ( screen-def )
   button "Register" [ if validate-screen screen-definition [ register ] ]
]

lo: layout copy/deep sprucedup-def

We now write a function that iterates over the screen definition, and grabs the values from the fields. It returns a block of values, and if there are errors on datatype conversion, it inserts a none value where the error occurs.

get-values: func [ sdef [block!] /local bl value] [
  bl: copy []
  foreach [ label field type variable ] sdef [
      field: do to-word :field
      if error? try [
           value: load get in field 'text
           case [
               word! = type? value [ value: form value ]
               block! = type? value [ value: none ]
           ]
          append bl value
      ][ append bl none ]
  ]
  bl
]

We now need to generate the rule that will be used to validate the values. Our function create-validation-rule again iterates over the screen definition block.

create-validation-rule: func [ def [block!] /local rule ][
   rule: copy []
   foreach [ label field type variable ] def [
       repend rule [ 'set variable type ( to-paren [incr field-no] ) ]
   ]
   rule
]

and our final validate-screen function uses these to get:

validate-screen: func [ sdef [block!]
   /local values rule bad-field
][
   values: get-values sdef
   rule: create-validation-rule sdef
   either parse values [ (field-no: 1) some rule end ][
       true
   ][
       bad-field: get pick sdef ( 4 * field-no - 2 )
       focus bad-field
       alert "Bad data entered in this field"
       false
   ]
]

Our validate-screen function gets the values from the data entry, creates the rule that will be used to validate the values, and then applies the rule returning a logic! result.

We reached this stage by creating a sub dialect that allowed us to generate some VID code, and also created a data retrieval function, and auto-generated a validation rule that worked with the same dialect block. It is possible to further optimise this code since we can see that we have traversed the screen-definition three times when we could do this perhaps in one pass. However, one preferred way to write code is to create small functions that are easily testable, and so in this example we sacrifice efficiency for readability and maintenance issues.

register: func [
][
	; <--- register the field values here
	alert "Registered field values."
]
view lo

And for reference, here is the final script complete with REBOL header and license.

Rebol [
   file: %screen-validator.r
   author: "Graham Chiu"
   rights: 'BSD
   date: 12-Sep-2005
   purpose: {
       To construct a data entry screen from a simple dialect
       To generate data validation rule from the same dialect
   
   }
]

screen-definition: [
  "First Name: " fname: string! first-name
  "Surname: " sname: string! last-name
  "DOB: " dob: date! date-of-birth
  "Age: " age: integer! customer-age
  "Email: " email: email! customer-email
  "Home Page: " home-page: url! customer-web
]

incr: func ['word][set word 1 + get word]

screen-def: []

field-rule: [ 
  set prompt string!
  set field-name set-word!
  set field-type word! 
  skip
  ( repend screen-def [ 'label prompt field-name 'field 'return ] )
]

if not parse screen-definition [ some field-rule end ][
  print "error in screen definition"
  halt
]

sprucedup-def: compose [
  title "Customer Entry Screen"
  across
  ( screen-def )
  button "Register" [ if validate-screen screen-definition [ register ] ]
]

lo: layout copy/deep sprucedup-def

get-values: func [ sdef [block!] /local bl value] [
  bl: copy []
  foreach [ label field type variable ] sdef [
      field: do to-word :field
      if error? try [
           value: load get in field 'text
           case [
               word? value [ value: form value ]
               block? value [ value: none ]
           ]
          append bl value
      ][ append bl none ]
  ]
  bl
]

create-validation-rule: func [ def [block!] /local rule ][
  rule: copy []
  foreach [ label field type variable ] def [
      repend rule [ 'set variable type ( to-paren [incr field-no] ) ]
  ]
  rule
]

validate-screen: func [ sdef [block!]
  /local values rule bad-field
][
  values: get-values sdef
  rule: create-validation-rule sdef
  either parse values [ (field-no: 1) some rule end ][
      true
  ][
      bad-field: get pick sdef ( 4 * field-no - 2 )
      focus bad-field
      alert "Bad data entered in this field"
      false
  ]
]

register: func [
][
   ; <--- register the field values here
   alert "Data validated ok!"
]

view lo