validator.gno
6.15 Kb · 283 lines
1package validate
2
3const (
4 HardPairMax = 128
5 DefaultPairMax = 32
6)
7
8// Validator is a read-only validation policy set after construction.
9//
10// Build validators with NewValidator and replace the whole Validator when a
11// package needs a different policy. Setter methods are intentionally omitted so
12// each published Validator pointer keeps stable behavior for every caller that
13// shares it. NewValidator copies caller-owned slices and stores policies by
14// value in its own map, so later caller-side changes to the inputs cannot
15// change validation behavior.
16//
17// Validator methods use pointer receivers to avoid copying the internal
18// map/slice headers and to keep the private validity sentinel in one place.
19// The pointer receiver is not an invitation to mutate the policy. In normal
20// use the only state transition after construction is Validate repairing the
21// private isValid sentinel if package-local test code or future internal code
22// invalidates it before validation.
23//
24// Accessors expose value snapshots instead of mutable internals: Policies
25// returns a fresh slice of StringPolicy values, DenyKeys returns a copied
26// string slice, and the scalar fields are returned by value.
27type Validator struct {
28 defaultPolicy DefaultStringPolicy
29 policies map[string]StringPolicy
30 pairMax int
31 allowUnknown bool
32 denyKeys []string
33 isValid bool
34}
35
36func NewValidator(
37 defaultPolicy DefaultStringPolicy,
38 policies []StringPolicy,
39 denyKeys []string,
40 pairMax int,
41 allowUnknown bool,
42) *Validator {
43 valid, message := defaultPolicy.Validate()
44 if !valid {
45 panic(message)
46 }
47 if pairMax <= 0 {
48 panic("pair max must be positive")
49 }
50 if pairMax > HardPairMax {
51 panic("pair max exceeds hard pair max")
52 }
53 valid, message = validateDenyKeys(denyKeys)
54 if !valid {
55 panic(message)
56 }
57
58 validator := &Validator{
59 defaultPolicy: defaultPolicy,
60 policies: map[string]StringPolicy{},
61 pairMax: pairMax,
62 allowUnknown: allowUnknown,
63 denyKeys: copyStringSlice(denyKeys),
64 }
65 if len(policies) > pairMax {
66 panic("policies count exceeds pair max")
67 }
68 valid, message = validator.validatePolicies(policies)
69 if !valid {
70 panic(message)
71 }
72 for _, policy := range policies {
73 validator.policies[policy.StringKey()] = policy
74 }
75 validator.isValid = true
76 return validator
77}
78
79func (m *Validator) DefaultStringPolicy() DefaultStringPolicy {
80 return m.defaultPolicy
81}
82
83func (m *Validator) Policies() []StringPolicy {
84 policies := []StringPolicy{}
85 for _, policy := range m.policies {
86 policies = append(policies, policy)
87 }
88 return policies
89}
90
91func (m *Validator) PairMax() int {
92 return m.pairMax
93}
94
95func (m *Validator) AllowUnknown() bool {
96 return m.allowUnknown
97}
98
99func (m *Validator) DenyKeys() []string {
100 return copyStringSlice(m.denyKeys)
101}
102
103func (m *Validator) Clone() Validator {
104 return *NewValidator(m.DefaultStringPolicy(), m.Policies(), m.DenyKeys(), m.PairMax(), m.AllowUnknown())
105}
106
107func (m *Validator) Valid() (bool, string) {
108 valid, message := m.defaultPolicy.Validate()
109 if !valid {
110 return false, message
111 }
112 if m.pairMax <= 0 {
113 return false, "pair max must be positive"
114 }
115 if m.pairMax > HardPairMax {
116 return false, "pair max exceeds hard pair max"
117 }
118 valid, message = validateDenyKeys(m.denyKeys)
119 if !valid {
120 return false, message
121 }
122 if len(m.policies) > m.pairMax {
123 return false, "policies count exceeds pair max"
124 }
125
126 return m.validatePolicies(m.Policies())
127}
128
129func (m *Validator) getPolicy(key string) StringPolicy {
130 if policy, found := m.policies[key]; found {
131 return policy
132 }
133 return m.defaultPolicy.StringPolicy(key)
134}
135
136func (m *Validator) MustValid() {
137 valid, message := m.Valid()
138 if !valid {
139 panic(message)
140 }
141}
142
143func (m *Validator) validatePolicies(policies []StringPolicy) (bool, string) {
144 seen := map[string]bool{}
145 result := ""
146 for _, policy := range policies {
147 key := policy.StringKey()
148 if seen[key] {
149 if result != "" {
150 result += "\n"
151 }
152 result += "duplicated policy key: " + key
153 }
154 if m.isDenyKey(key) {
155 if result != "" {
156 result += "\n"
157 }
158 result += "policy key denied: " + key
159 }
160 valid, message := policy.Valid()
161 if !valid {
162 if result != "" {
163 result += "\n"
164 }
165 result += "policy " + key + ": " + message
166 }
167 seen[key] = true
168 }
169 if result != "" {
170 return false, result
171 }
172 return true, ""
173}
174
175func validateDenyKeys(keys []string) (bool, string) {
176 seen := map[string]bool{}
177 result := ""
178 for _, key := range keys {
179 if key == "" {
180 if result != "" {
181 result += "\n"
182 }
183 result += "denied key must be not empty"
184 continue
185 }
186 if seen[key] {
187 if result != "" {
188 result += "\n"
189 }
190 result += "duplicated denied key: " + key
191 }
192 seen[key] = true
193 }
194 if result != "" {
195 return false, result
196 }
197 return true, ""
198}
199
200func (m *Validator) Validate(input map[string]string) (bool, string) {
201 if !m.isValid {
202 m.MustValid()
203 m.isValid = true
204 }
205
206 if len(input) > m.pairMax {
207 return false, "map has too many entries"
208 }
209
210 seen := map[string]bool{}
211 result := ""
212
213 for key, policy := range m.policies {
214 val, found := input[key]
215 valid, message := policy.Validate(val, found)
216 if !valid {
217 if result != "" {
218 result += "\n"
219 }
220 result += "policy " + key + ": " + message
221 }
222 seen[key] = true
223 }
224
225 for key, val := range input {
226 if m.isDenyKey(key) {
227 if result != "" {
228 result += "\n"
229 }
230 result += "denied key: " + key
231 continue
232 }
233 if !m.allowUnknown && !seen[key] {
234 if result != "" {
235 result += "\n"
236 }
237 result += "unknown key: " + key
238 continue
239 }
240 if seen[key] {
241 continue
242 }
243
244 policy := m.getPolicy(key)
245 valid, message := policy.Validate(val, true)
246 if !valid {
247 if result != "" {
248 result += "\n"
249 }
250 result += "policy " + key + ": " + message
251 }
252 }
253
254 if result != "" {
255 return false, result
256 }
257 return true, ""
258}
259
260func (m *Validator) MustValidate(input map[string]string) {
261 valid, message := m.Validate(input)
262 if !valid {
263 panic(message)
264 }
265}
266
267func (m *Validator) isDenyKey(key string) bool {
268 for _, k := range m.denyKeys {
269 if k == key {
270 return true
271 }
272 }
273 return false
274}
275
276func copyStringSlice(source []string) []string {
277 if source == nil {
278 return nil
279 }
280 result := make([]string, len(source))
281 copy(result, source)
282 return result
283}