-
Notifications
You must be signed in to change notification settings - Fork 3
/
looper.go
209 lines (185 loc) · 7.32 KB
/
looper.go
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
// This file is part of https://github.com/racingmars/go3270/
// Copyright 2020 by Matthew R. Wilson, licensed under the MIT license. See
// LICENSE in the project root for license information.
package go3270
import (
"fmt"
"net"
"regexp"
"strings"
)
// 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.
type Rules 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.
type Validator func(input string) bool
// 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.
var NonBlank Validator = func(input string) bool {
return !(strings.TrimSpace(input) == "")
}
var isIntegerRegexp = regexp.MustCompile(`^-?[0-9]+$`)
// IsInteger 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).
var IsInteger Validator = func(input string) bool {
input = strings.TrimSpace(input)
return isIntegerRegexp.MatchString(input)
}
// FieldRules provides the validation rules for a particular field.
type FieldRules struct {
// MustChange, 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.
MustChange bool
// ErrorText 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>."
ErrorText 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 Validator
// 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 bool
}
// HandleScreen is a higher-level interface to the ShowScreen() function.
// HandleScreen will loop until all validation rules are satisfied, and only
// return when an expected AID (i.e. PF) key is pressed.
//
// - screen is the Screen to display (see ShowScreen()).
// - rules are the Rules to enforce: each key in the Rules map corresponds to
// a Field.Name in the screen array.
// - values are field values you wish to override (see ShowScreen()).
// - pfkeys and exitkeys are the AID keys that you wish to accept (that is,
// perform validation and return if successful) and treat as exit keys
// (unconditionally return).
// - errorField is the name of a field in the screen array that you wish error
// messages to be written in when HandleScreen loops waiting for a valid
// user submission.
// - crow and ccol are the initial cursor position.
// - conn is the network connection to the 3270 client.
//
// HandleScreen 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, HandleScreen will re-present the screen to the user again,
// possibly with an error message set in the errorField field.
func HandleScreen(screen Screen, rules Rules, values map[string]string,
pfkeys, exitkeys []AID, errorField string, crow, ccol int,
conn net.Conn) (Response, error) {
// Save the original field values for any named fields to support
// the MustChange rule. Also build a map of named fields.
origValues := make(map[string]string)
fields := make(map[string]*Field)
for i := range screen {
if screen[i].Name != "" {
origValues[screen[i].Name] = screen[i].Content
fields[screen[i].Name] = &screen[i]
}
}
// Make our own field values map so we don't alter the caller's values
myValues := make(map[string]string)
for field := range values {
myValues[field] = values[field]
}
// Now we loop...
mainloop:
for {
// Reset fields with FieldRules.Reset set
for field := range rules {
if rules[field].Reset {
// avoid problems if there is a rule for a non-existent field
if _, ok := fields[field]; ok {
// Is the value in the origValues map?
if value, ok := origValues[field]; ok {
myValues[field] = value
} else {
// remove from the values map so we fall back to
// whatever default is set for the field
delete(myValues, field)
}
}
}
}
resp, err := ShowScreen(screen, myValues, crow, ccol, conn)
if err != nil {
return resp, err
}
// If we got an exit key, return without performing validation
if aidInArray(resp.AID, exitkeys) {
return resp, nil
}
// If we got an unexpected key, set error message and restart loop
if !aidInArray(resp.AID, pfkeys) {
if !(resp.AID == AIDClear || resp.AID == AIDPA1 || resp.AID == AIDPA2 ||
resp.AID == AIDPA3) {
myValues = mergeFieldValues(myValues, resp.Values)
}
myValues[errorField] = fmt.Sprintf("%s: unknown key",
AIDtoString(resp.AID))
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.
if resp.AID == AIDClear || resp.AID == AIDPA1 || resp.AID == AIDPA2 ||
resp.AID == AIDPA3 {
return resp, nil
}
myValues = mergeFieldValues(myValues, resp.Values)
delete(myValues, errorField) // don't persist errors across refreshes
// Now we can validate each field
for field := range rules {
// skip rules for fields that don't exist
if _, ok := myValues[field]; !ok {
continue
}
if rules[field].MustChange && myValues[field] == origValues[field] {
myValues[errorField] = rules[field].ErrorText
continue mainloop
}
if rules[field].Validator != nil && !rules[field].Validator(myValues[field]) {
myValues[errorField] = fmt.Sprintf("Value for %s is not valid", field)
continue mainloop
}
}
// Everything passed validation
return resp, nil
}
}
// aidInArray performs a linear search through the aids array and returns true
// if aid appears in the array, false otherwise.
func aidInArray(aid AID, aids []AID) bool {
for i := range aids {
if aids[i] == aid {
return true
}
}
return false
}
// mergeFieldValues will return 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 HandleScreen() may
// provide override values for non-writable fields, and we don't get those
// values back when we round-trip with the 3270 client.
func mergeFieldValues(original, current map[string]string) map[string]string {
result := make(map[string]string)
for key := range current {
result[key] = current[key]
}
for key := range original {
if _, ok := result[key]; !ok {
result[key] = original[key]
}
}
return result
}