-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlooper.lisp
276 lines (210 loc) · 9.54 KB
/
looper.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
;;;; -*- Mode: Lisp -*-
;;;; looper.lisp
(in-package "CL3270")
;;; Rules is a map of field names (strings) to FieldRules structs. Each
;;; field for which you wish validation to occur must appear in the
;;; map. Fields not in the map will not have any input validation
;;; performed.
(deftype rules ()
'hash-table ; map[string]FieldRules
)
;;; Validator is a type that represents a function which can perform
;;; field input validation. The function is passed a string, input, and
;;; returns true if the input is valid or false if the not.
(deftype validator ()
'(function (string) boolean))
;;; NonBlank is a Validator that returns true if, after spaces are
;;; trimmed from the beginning and end of the string, the value is not
;;; empty.
(defun non-blank-validator (input)
(declare (type string input))
(not (string= "" (string-trim '(#\Space) input))))
;;;;; var isIntegerRegexp = regexp.MustCompile(`^-?[0-9]+$`)
;;; is-integer-validator is a Validator that returns true if, after
;;; spaces are trimmed from the beginning and end if the string, the
;;; value is an integer (including negative numbers and 0).
(defun is-integer-validator (input)
(declare (type string input))
(ignore-errors
(integerp
(parse-integer (string-trim '(#\Space) input)
:junk-allowed nil))))
(defstruct (field-rules
(:constructor field-rules (field
&key
must-change
error-text
validator
reset)))
"The FIELD-RULES structure.
FIELD-RULES objects provide validation rules for a particular field."
(field "" :type string)
;; must-change, when true, indicates that the value of the field MUST be
;; altered by the user -- if applied to a field with no starting value,
;; this makes the field a required field. If true on a field with a
;; starting value (either in the field's Content attribute, or with an
;; override in the initial values map), then the user must change
;; the value from the default.
(must-change nil :type boolean)
;; error-text is the text displayed with the MustChange validation fails.
;; If ErrorText is the empty string, but MustValidation fails, an error
;; string will be constructed from the field name:
;; "Please enter a valid value for <fieldName>."
(error-text "" :type string)
;; validator is a function to validate the value the user input into
;; the field. It may be nil if no validation is required. The
;; Validator function is called *after* the MustChange logic, so if
;; you wish to fully handle validation, ensure MustChange is set to
;; false.
(validator (constantly t) :type (or symbol function validator)) ; Not quite right.
;; reset indicates that if the screen fails validation, this field
;; should always be reset to its original/default value, regardless
;; of what the user entered.
(reset nil :type boolean)
)
;;; make-rules
(defun make-rules (&rest frs &aux (rs (make-hash-table :test #'equal)))
(loop for fr in frs
do (setf (gethash (field-rules-field fr) rs) fr)
)
rs)
;;; handle-screen
(defun handle-screen (screen
rules
vals
pf-keys
exit-keys
error-field
curs-row
curs-col
conn)
"A higher-level interface to the SHOW-SCREEN function.
HANDLE-SCREEN will loop until all validation rules are satisfied, and only
return when an expected AID (i.e. PF) key is pressed.
HANDLE-SCREEN will return when the user: 1) presses a key in pfkeys AND all
fields pass validation, OR 2) the user presses a key in exitkeys. In all
other cases, HANDLE-SCREEN will re-present the screen to the user again,
possibly with an error message set in the errorField field.
Arguments and Values:
SCREEN -- the screen to display (see SHOW-SCREEN).
RULES -- the rules to enforce: each key in the Rules map corresponds
to a FIELD-NAME in the SCREEN array.
VALS -- field values you wish to override (see SHOW-SCREEN).
PF-KEYS, EXIT-KEYS -- the AID keys that you wish to accept (that is,
perform validation and return if successful) and treat as exit keys
(unconditionally return).
ERROR-FIELD -- the name of a field in the screen array that you wish error
messages to be written in when HANDLE-SCREEN loops waiting for a valid
user submission.
CURS-ROW, CURS-COL -- the initial cursor position.
CONN -- the network connection to the 3270 client.
"
;; Save the original field values for any named fields to support
;; the MustChange rule. Also build a map of named fields.
(let ((orig-values (make-hash-table :test #'equal))
(fields (make-hash-table :test #'equal))
(my-vals (make-hash-table :test #'equal))
)
(dbgmsg ">>> HANDLE-SCREEN: saving values and fields.~%")
(dolist (f (screen-fields screen))
(when (not (string= "" (field-name f)))
(setf (gethash (field-name f) orig-values)
(field-content f)
(gethash (field-name f) fields)
f)))
(loop for f being the hash-key of vals using (hash-value v)
do (setf (gethash f my-vals) v))
(dbgmsg ">>> HANDLE-SCREEN: saved values and fields.~%")
;; The tagbodies and the GOs are probably fixable in a better way,
;; but as such the mapping s almost 1-1 with Mtthew R. Wilson's GO
;; code.
(tagbody
:mainloop
(loop
(tagbody
:continue
;; Reset fields with FIELD-RULES-RESET set.
(when rules
(loop for field being the hash-key of rules using (hash-value rule)
when (and (field-rules-reset rule)
(nth-value 1 (gethash field fields)))
do (multiple-value-bind (v foundp)
(gethash field my-vals)
(if foundp
(setf (gethash field my-vals) v)
(remhash field my-vals)))))
(multiple-value-bind (resp err)
(show-screen screen my-vals curs-row curs-col conn)
(when err
(format *error-output* "!!! SHOW-SCREEN 1 error ~S~%" err)
(return-from handle-screen (values resp err)))
;; If we got an exit key, return without performing
;; validation.
(when (aid-in-set (response-aid resp) exit-keys)
(return-from handle-screen (values resp nil)))
;; If we got an unexpected key, set error message and restart
;; loop.
(unless (aid-in-set (response-aid resp) pf-keys)
(unless (or (is-clear-key (response-aid resp))
(is-attention-key (response-aid resp)))
(setf my-vals (merge-field-values my-vals (response-values resp))))
(setf (gethash error-field my-vals)
(format nil "~S: unknown key"
(aid-to-string (response-aid resp))))
(go :continue))
;; At this point, we have an expected key. If one of the
;; "clear" keys is expected, we can't do much, so we'll just
;; return.
(when (or (is-clear-key (response-aid resp))
(is-attention-key (response-aid resp)))
(return-from handle-screen (values resp nil)))
(setq my-vals (merge-field-values my-vals (response-values resp)))
(remhash error-field my-vals)
;; Now we can validate each field, if we must
(when rules
(loop for field being the hash-key of rules using (hash-value fr)
unless (nth-value 1 (gethash field my-vals))
do
(go :continue)
end
when (and (field-rules-must-change fr)
(string= (gethash field my-vals)
(gethash field orig-values)))
do (setf (gethash error-field my-vals)
(field-rules-error-text fr))
(go :mainloop)
end
when (and (field-rules-validator fr)
(not (funcall (field-rules-validator fr)
(gethash field my-vals))))
do (setf (gethash error-field my-vals)
(format nil "Value for ~S is not valid"
field))
(go :mainloop)
end
))
(return-from handle-screen (values resp nil))
))
))
))
(defun aid-in-set (aid aids)
(find aid aids :test #'=))
(defun merge-field-values (original current)
"Merge the ORIGINAL and CURRENT maps.
The function returns a new map, containing all keys from the current
map and keys from the original map that do not exist in the current map.
This is sometimes necessary because the caller of HANDLE-SCREEN may
provide override values for non-writable fields, and we don't get those
values back when we round-trip with the 3270 client.
"
(declare (type hash-table original current))
(let ((result (make-hash-table :test (hash-table-test original))))
(loop for key being the hash-key of current
do (setf (gethash key result) (gethash key current)))
(loop for key being the hash-key of original
unless (nth-value 1 (gethash key result))
do (setf (gethash key result)
(gethash key original)))
result
))
;;;; end of file -- looper.lisp