formkeeper 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +25 -0
- data/Rakefile +7 -0
- data/formkeeper.gemspec +19 -0
- data/lib/formkeeper/version.rb +3 -0
- data/lib/formkeeper.rb +849 -0
- data/spec/asset/messages.yaml +18 -0
- data/spec/messages_spec.rb +24 -0
- data/spec/record_spec.rb +28 -0
- data/spec/report_spec.rb +83 -0
- data/spec/respondent_spec.rb +174 -0
- data/spec/rule_spec.rb +138 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/validator_spec.rb +273 -0
- metadata +76 -0
data/lib/formkeeper.rb
ADDED
@@ -0,0 +1,849 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2012 Lyo Kato, <lyo.kato _at_ gmail.com>.
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
require "formkeeper/version"
|
24
|
+
require 'hpricot'
|
25
|
+
require 'yaml'
|
26
|
+
require 'rack'
|
27
|
+
require 'uri'
|
28
|
+
require 'date'
|
29
|
+
|
30
|
+
module FormKeeper
|
31
|
+
module Filter
|
32
|
+
class Base
|
33
|
+
def process(value)
|
34
|
+
return value
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class Custom < Base
|
39
|
+
def initialize(block)
|
40
|
+
@custom = block
|
41
|
+
end
|
42
|
+
def process(value)
|
43
|
+
@custom.call(value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class UpCase < Base
|
48
|
+
def process(value)
|
49
|
+
return value.upcase
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class DownCase < Base
|
54
|
+
def process(value)
|
55
|
+
return value.downcase
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Strip < Base
|
60
|
+
def process(value)
|
61
|
+
return value.strip
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module Constraint
|
67
|
+
class Base
|
68
|
+
def validate(value, arg)
|
69
|
+
return true
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Custom < Base
|
74
|
+
def initialize(block)
|
75
|
+
@custom = block
|
76
|
+
end
|
77
|
+
def validate(value, arg)
|
78
|
+
@custom.call(value, arg)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class Ascii < Base
|
83
|
+
def validate(value, arg)
|
84
|
+
result = value =~ /^[\x21-\x7e]+$/
|
85
|
+
result = !result if !arg
|
86
|
+
result
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class Regexp < Base
|
91
|
+
def validate(value, arg)
|
92
|
+
r = arg
|
93
|
+
r = Regexp.new(r) unless r.kind_of?(Regexp)
|
94
|
+
value =~ r
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Int < Base
|
99
|
+
def validate(value, arg)
|
100
|
+
result = value =~ /^\-?[[:digit:]]+$/
|
101
|
+
result = !result if !arg
|
102
|
+
result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Uint < Base
|
107
|
+
def validate(value, arg)
|
108
|
+
result = value =~ /^[[:digit:]]+$/
|
109
|
+
result = !result if !arg
|
110
|
+
result
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
class Alpha < Base
|
115
|
+
def validate(value, arg)
|
116
|
+
result = value =~ /^[[:alpha:]]+$/
|
117
|
+
result = !result if !arg
|
118
|
+
result
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class AlphaSpace < Base
|
123
|
+
def validate(value, arg)
|
124
|
+
result = value =~ /^[[:alpha:][:space:]]+$/
|
125
|
+
result = !result if !arg
|
126
|
+
result
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class Alnum < Base
|
131
|
+
def validate(value, arg)
|
132
|
+
result = value =~ /^[[:alnum:]]+$/
|
133
|
+
result = !result if !arg
|
134
|
+
result
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class AlnumSpace < Base
|
139
|
+
def validate(value, arg)
|
140
|
+
result = value =~ /^[[:alnum:][:space:]]+$/
|
141
|
+
result = !result if !arg
|
142
|
+
result
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class URI < Base
|
147
|
+
def validate(value, arg)
|
148
|
+
u = URI.parse(value)
|
149
|
+
return false if u.nil?
|
150
|
+
arg = [arg] unless arg.kind_of?(Array)
|
151
|
+
arg.collect(&:to_s).include?(u.scheme)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class Length < Base
|
156
|
+
def validate(value, arg)
|
157
|
+
l = value.length
|
158
|
+
case arg
|
159
|
+
when Fixnum
|
160
|
+
return (l == arg)
|
161
|
+
when Range
|
162
|
+
return arg.include?(l)
|
163
|
+
else
|
164
|
+
raise ArgumentError.new('Invalid number of arguments')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class Characters < Base
|
170
|
+
def validate(value, arg)
|
171
|
+
l = value.split(//u).length
|
172
|
+
case arg
|
173
|
+
when Fixnum
|
174
|
+
return (l == args)
|
175
|
+
when Range
|
176
|
+
return arg.include?(l)
|
177
|
+
else
|
178
|
+
raise ArgumentError.new('Invalid number of arguments')
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
module CombinationConstraint
|
185
|
+
class Base
|
186
|
+
def validate(values, arg)
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
class Same < Base
|
192
|
+
def validate(values, arg)
|
193
|
+
return false unless values.size == 2
|
194
|
+
values[0] == values[1]
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
class Any < Base
|
199
|
+
def validate(values, arg)
|
200
|
+
values.any? { |v| not (v.nil? or v.empty?) }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class Date < Base
|
205
|
+
def validate(values, arg)
|
206
|
+
return false unless values.size == 3
|
207
|
+
# TODO handle range by args[:from] and args[:to]
|
208
|
+
::Date.valid_date?(values[0], values[1], values[2])
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
class Time < Base
|
213
|
+
def validate(values, arg)
|
214
|
+
return false unless values.size == 3
|
215
|
+
# TODO handle range by args[:from] and args[:to]
|
216
|
+
(0..23).include?(values[0]) and (0..59).include(values[1]) and (0..59).include(values[2])
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
class DateTime < Base
|
221
|
+
def validate(values, arg)
|
222
|
+
return false unless values.size == 6
|
223
|
+
# TODO handle range by args[:from] and args[:to]
|
224
|
+
::Date.valid_date?(values[0], values[1], values[2]) and (0..23).include?(values[3]) and (0..59).include(values[5]) and (0..59).include(values[5])
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Messages
|
230
|
+
DEFAULT_ACTION_NAME = 'DEFAULT'
|
231
|
+
DEFAULT_CONSTRAINT_NAME = 'DEFAULT'
|
232
|
+
def self.from_file(path)
|
233
|
+
data = YAML.load_file(path)
|
234
|
+
self.new(data)
|
235
|
+
end
|
236
|
+
def initialize(data)
|
237
|
+
@data = data
|
238
|
+
end
|
239
|
+
def get(action_name, target_name, constraint_name)
|
240
|
+
if @data.has_key?(action_name.to_s)
|
241
|
+
action = @data[action_name.to_s]
|
242
|
+
return search_from_action_part(action, target_name, constraint_name)
|
243
|
+
else
|
244
|
+
if @data.has_key?(DEFAULT_ACTION_NAME)
|
245
|
+
action = @data[DEFAULT_ACTION_NAME]
|
246
|
+
return search_from_action_part(action, target_name, constraint_name)
|
247
|
+
else
|
248
|
+
return handle_missing_message(target_name)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
private
|
254
|
+
def search_from_action_part(action, target_name, constraint_name)
|
255
|
+
if action.has_key?(target_name.to_s)
|
256
|
+
field = action[target_name.to_s]
|
257
|
+
return search_from_field_part(target_name, field, constraint_name)
|
258
|
+
else
|
259
|
+
return handle_missing_message(target_name)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def search_from_field_part(target_name, field, constraint_name)
|
264
|
+
if field.has_key?(constraint_name.to_s)
|
265
|
+
return field[constraint_name.to_s]
|
266
|
+
else
|
267
|
+
if field.has_key?(DEFAULT_CONSTRAINT_NAME)
|
268
|
+
return field[DEFAULT_CONSTRAINT_NAME]
|
269
|
+
else
|
270
|
+
return handle_missing_message(target_name)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def handle_missing_message(target_name)
|
276
|
+
build_default_message(target_name)
|
277
|
+
end
|
278
|
+
|
279
|
+
def build_default_message(target_name)
|
280
|
+
"#{target_name.to_s} is invalid."
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
class Record
|
285
|
+
attr_reader :name, :failed_constraints
|
286
|
+
attr_accessor :value
|
287
|
+
def initialize(name)
|
288
|
+
@name = name
|
289
|
+
@value = nil
|
290
|
+
@failed_constraints = []
|
291
|
+
end
|
292
|
+
def fail(constraint)
|
293
|
+
@failed_constraints << constraint
|
294
|
+
end
|
295
|
+
def failed?
|
296
|
+
@failed_constraints.size > 0
|
297
|
+
end
|
298
|
+
def failed_by?(constraint)
|
299
|
+
@failed_constraints.include?(constraint.to_sym)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class Report
|
304
|
+
def initialize(messages=nil)
|
305
|
+
@failed_records = {}
|
306
|
+
@messages = messages || Messages.new({})
|
307
|
+
@valid_params = {}
|
308
|
+
end
|
309
|
+
def <<(record)
|
310
|
+
if record.failed?
|
311
|
+
@failed_records[record.name.to_sym] = record
|
312
|
+
else
|
313
|
+
@valid_params[record.name.to_sym] = record.value
|
314
|
+
end
|
315
|
+
end
|
316
|
+
def [](name)
|
317
|
+
@valid_params[name.to_sym]
|
318
|
+
end
|
319
|
+
def valid_fields
|
320
|
+
@valid_params.keys
|
321
|
+
end
|
322
|
+
def valid?(name)
|
323
|
+
@valid_params.has_key?(name.to_sym)
|
324
|
+
end
|
325
|
+
def failed?
|
326
|
+
!@failed_records.empty?
|
327
|
+
end
|
328
|
+
def failed_on?(name, constraint=nil)
|
329
|
+
return false unless @failed_records.has_key?(name.to_sym)
|
330
|
+
return true if constraint.nil?
|
331
|
+
record = @failed_records[name.to_sym]
|
332
|
+
record.failed_by?(constraint)
|
333
|
+
end
|
334
|
+
def failed_fields
|
335
|
+
@failed_records.keys
|
336
|
+
end
|
337
|
+
def failed_constraints(name)
|
338
|
+
return [] unless @failed_records.has_key?(name.to_sym)
|
339
|
+
record = @failed_records[name.to_sym]
|
340
|
+
record.failed_constraints
|
341
|
+
end
|
342
|
+
def messages(action, name=nil)
|
343
|
+
messages = []
|
344
|
+
if name.nil?
|
345
|
+
@failed_records.values.each do |record|
|
346
|
+
record.failed_constraints.each do |constraint|
|
347
|
+
messages << @messages.get(action, record.name, constraint)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
else
|
351
|
+
if @failed_records.has_key?(name.to_sym)
|
352
|
+
record = @failed_records[name.to_sym]
|
353
|
+
record.failed_constraints.each do |constraint|
|
354
|
+
messages << @messages.get(action, record.name, constraint)
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
messages.select{ |m| !m.nil? and !m.empty? }.uniq
|
359
|
+
end
|
360
|
+
def message(action, name, constraint)
|
361
|
+
if @failed_records.has_key?(name.to_sym) and @failed_records[name.to_sym].failed_by?(constraint)
|
362
|
+
@messages.get(action, name, constraint)
|
363
|
+
else
|
364
|
+
nil
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
class Rule
|
370
|
+
module Criteria
|
371
|
+
class Field
|
372
|
+
attr_reader :default, :filters, :constraints
|
373
|
+
def initialize(criteria)
|
374
|
+
if criteria.has_key?(:default)
|
375
|
+
default = criteria.delete :default
|
376
|
+
@default = default.empty? ? nil : default
|
377
|
+
else
|
378
|
+
@default = nil
|
379
|
+
end
|
380
|
+
if criteria.has_key?(:filters)
|
381
|
+
filters = criteria.delete :filters
|
382
|
+
case filters
|
383
|
+
when Array
|
384
|
+
@filters = filters.collect(&:to_sym)
|
385
|
+
when String
|
386
|
+
@filters = [filters.to_sym]
|
387
|
+
when Symbol
|
388
|
+
@filters = [filters]
|
389
|
+
else
|
390
|
+
raise ArgumentError.new 'invalid :filters'
|
391
|
+
end
|
392
|
+
else
|
393
|
+
@filters = []
|
394
|
+
end
|
395
|
+
|
396
|
+
if criteria.has_key?(:present)
|
397
|
+
if not @default.nil?
|
398
|
+
raise ArgumentError.new "don't set both :default and :present at once"
|
399
|
+
end
|
400
|
+
present = criteria.delete :present
|
401
|
+
@present = !!present
|
402
|
+
else
|
403
|
+
@present = false
|
404
|
+
end
|
405
|
+
@constraints = criteria
|
406
|
+
end
|
407
|
+
|
408
|
+
def require_presence?
|
409
|
+
@present
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
class Checkbox
|
414
|
+
attr_reader :default, :filters, :count, :constraints
|
415
|
+
def initialize(criteria)
|
416
|
+
if criteria.has_key?(:default)
|
417
|
+
default = criteria.delete :default
|
418
|
+
case default
|
419
|
+
when Array
|
420
|
+
@default = default.collect(&:to_s)
|
421
|
+
else
|
422
|
+
@default = [default.to_s]
|
423
|
+
end
|
424
|
+
else
|
425
|
+
@default = []
|
426
|
+
end
|
427
|
+
|
428
|
+
if criteria.has_key?(:filters)
|
429
|
+
filters = criteria.delete :filters
|
430
|
+
case filters
|
431
|
+
when Array
|
432
|
+
@filters = filters.collect(&:to_sym)
|
433
|
+
when String
|
434
|
+
@filters = [filters.to_sym]
|
435
|
+
when Symbol
|
436
|
+
@filters = [filters]
|
437
|
+
else
|
438
|
+
raise ArgumentError.new 'invalid :filters'
|
439
|
+
end
|
440
|
+
else
|
441
|
+
@filters = []
|
442
|
+
end
|
443
|
+
if criteria.has_key?(:count)
|
444
|
+
count = criteria.delete :count
|
445
|
+
case count
|
446
|
+
when Fixnum
|
447
|
+
@count = Range.new(count, count)
|
448
|
+
when Range
|
449
|
+
@count = count
|
450
|
+
else
|
451
|
+
raise ArgumentError.new 'invalid :count'
|
452
|
+
end
|
453
|
+
else
|
454
|
+
@count = nil
|
455
|
+
end
|
456
|
+
@constraints = criteria
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
class Combination
|
461
|
+
attr_reader :fields, :filters, :constraint, :arg
|
462
|
+
def initialize(criteria)
|
463
|
+
if criteria.has_key?(:fields)
|
464
|
+
fields = criteria.delete(:fields)
|
465
|
+
if fields.kind_of?(Array) && fields.size >= 2
|
466
|
+
@fields = fields.collect(&:to_sym)
|
467
|
+
else
|
468
|
+
raise ArgumentError.new("combination rule requires :fields as array which include 2 fields at lease")
|
469
|
+
end
|
470
|
+
else
|
471
|
+
raise ArgumentError.new("combination rule requires :fields")
|
472
|
+
end
|
473
|
+
if criteria.has_key?(:filters)
|
474
|
+
filters = criteria.delete :filters
|
475
|
+
case filters
|
476
|
+
when Array
|
477
|
+
@filters = filters.collect(&:to_sym)
|
478
|
+
when String
|
479
|
+
@filters = [filters.to_sym]
|
480
|
+
when Symbol
|
481
|
+
@filters = [filters]
|
482
|
+
else
|
483
|
+
raise ArgumentError.new 'invalid :filters'
|
484
|
+
end
|
485
|
+
else
|
486
|
+
@filters = []
|
487
|
+
end
|
488
|
+
constraint = criteria.shift
|
489
|
+
if constraint.nil?
|
490
|
+
raise ArgumentError.new 'constraint not found'
|
491
|
+
else
|
492
|
+
@constraint = constraint[0]
|
493
|
+
@arg = constraint[1]
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
attr_reader :default_filters, :fields, :checkboxes, :combinations
|
500
|
+
|
501
|
+
def initialize
|
502
|
+
@default_filters = []
|
503
|
+
@fields = {}
|
504
|
+
@checkboxes = {}
|
505
|
+
@combinations = {}
|
506
|
+
end
|
507
|
+
|
508
|
+
def filters(*args)
|
509
|
+
@default_filters = args
|
510
|
+
end
|
511
|
+
|
512
|
+
def field(name, criteria)
|
513
|
+
raise ArgumentError.new unless criteria.kind_of?(Hash)
|
514
|
+
@fields[name.to_sym] = Criteria::Field.new(criteria)
|
515
|
+
end
|
516
|
+
|
517
|
+
def checkbox(name, criteria)
|
518
|
+
raise ArgumentError.new unless criteria.kind_of?(Hash)
|
519
|
+
@checkboxes[name.to_sym] = Criteria::Checkbox.new(criteria)
|
520
|
+
end
|
521
|
+
|
522
|
+
def combination(name, criteria)
|
523
|
+
raise ArgumentError.new unless criteria.kind_of?(Hash)
|
524
|
+
@combinations[name.to_sym] = Criteria::Combination.new(criteria)
|
525
|
+
end
|
526
|
+
|
527
|
+
def method_missing(name, *args)
|
528
|
+
rule = args[0]
|
529
|
+
criteria = {}
|
530
|
+
criteria[:fields] = args[1]
|
531
|
+
opts = args[2] || {}
|
532
|
+
if opts.has_key?(:filters)
|
533
|
+
criteria[:filters] = opts.delete(:filters)
|
534
|
+
end
|
535
|
+
if opts.empty?
|
536
|
+
criteria[name.to_sym] = true
|
537
|
+
else
|
538
|
+
criteria[name.to_sym] = opts
|
539
|
+
end
|
540
|
+
combination(rule, criteria)
|
541
|
+
end
|
542
|
+
end
|
543
|
+
|
544
|
+
class Validator
|
545
|
+
|
546
|
+
@@filter_store = {}
|
547
|
+
@@constraint_store = {}
|
548
|
+
@@combination_constraint_store = {}
|
549
|
+
|
550
|
+
def self.register_filter(name, filter)
|
551
|
+
@@filter_store[name] = filter
|
552
|
+
end
|
553
|
+
|
554
|
+
def self.register_constraint(name, constraint)
|
555
|
+
@@constraint_store[name] = constraint
|
556
|
+
end
|
557
|
+
|
558
|
+
def self.register_combination_constraint(name, constraint)
|
559
|
+
@@combination_constraint_store[name] = constraint
|
560
|
+
end
|
561
|
+
|
562
|
+
register_filter :strip, Filter::Strip.new
|
563
|
+
register_filter :downcase, Filter::DownCase.new
|
564
|
+
register_filter :upcase, Filter::UpCase.new
|
565
|
+
|
566
|
+
register_constraint :ascii, Constraint::Ascii.new
|
567
|
+
register_constraint :regexp, Constraint::Regexp.new
|
568
|
+
register_constraint :int, Constraint::Int.new
|
569
|
+
register_constraint :uint, Constraint::Uint.new
|
570
|
+
register_constraint :alpha, Constraint::Alpha.new
|
571
|
+
register_constraint :alpha_space, Constraint::AlphaSpace.new
|
572
|
+
register_constraint :alnum, Constraint::Alnum.new
|
573
|
+
register_constraint :alnum_space, Constraint::AlnumSpace.new
|
574
|
+
register_constraint :uri, Constraint::URI.new
|
575
|
+
register_constraint :length, Constraint::Length.new
|
576
|
+
register_constraint :characters, Constraint::Characters.new
|
577
|
+
|
578
|
+
register_combination_constraint :datetime, CombinationConstraint::DateTime.new
|
579
|
+
register_combination_constraint :date, CombinationConstraint::Date.new
|
580
|
+
register_combination_constraint :time, CombinationConstraint::Time.new
|
581
|
+
register_combination_constraint :same, CombinationConstraint::Same.new
|
582
|
+
register_combination_constraint :any, CombinationConstraint::Any.new
|
583
|
+
|
584
|
+
def initialize
|
585
|
+
end
|
586
|
+
|
587
|
+
def validate(params, rule, messages=nil)
|
588
|
+
report = Report.new(messages)
|
589
|
+
rule.fields.each do |name, criteria|
|
590
|
+
criteria.filters.concat(rule.default_filters)
|
591
|
+
report << validate_field(name, criteria, params)
|
592
|
+
end
|
593
|
+
rule.checkboxes.each do |name, criteria|
|
594
|
+
criteria.filters.concat(rule.default_filters)
|
595
|
+
report << validate_checkbox(name, criteria, params)
|
596
|
+
end
|
597
|
+
rule.combinations.each do |name, criteria|
|
598
|
+
criteria.filters.concat(rule.default_filters)
|
599
|
+
report << validate_combination(name, criteria, params)
|
600
|
+
end
|
601
|
+
return report
|
602
|
+
end
|
603
|
+
|
604
|
+
private
|
605
|
+
def validate_combination(name, criteria, params)
|
606
|
+
record = Record.new(name)
|
607
|
+
values = criteria.fields.collect { |name| params[name.to_s] }
|
608
|
+
values = filter_combination_values(values, criteria.filters)
|
609
|
+
constraint = find_combination_constraint(criteria.constraint)
|
610
|
+
result = constraint.validate(values, criteria.arg)
|
611
|
+
record.fail(name) unless result
|
612
|
+
record
|
613
|
+
end
|
614
|
+
|
615
|
+
def validate_checkbox(name, criteria, params)
|
616
|
+
record = Record.new(name)
|
617
|
+
if params.has_key?(name.to_s)
|
618
|
+
values = params[name.to_s]
|
619
|
+
if values.kind_of?(Array)
|
620
|
+
values = filter_checkbox_values(values, criteria.filters)
|
621
|
+
record.value = values
|
622
|
+
if criteria.count.nil?
|
623
|
+
if values.size == 0
|
624
|
+
handle_missing_checkbox(criteria, record)
|
625
|
+
else
|
626
|
+
values.each do |value|
|
627
|
+
validate_value(value, criteria, record)
|
628
|
+
end
|
629
|
+
end
|
630
|
+
else
|
631
|
+
if criteria.count.include?(values.size)
|
632
|
+
values.each do |value|
|
633
|
+
validate_value(value, criteria, record)
|
634
|
+
end
|
635
|
+
else
|
636
|
+
if values.size == 0
|
637
|
+
handle_missing_checkbox(criteria, record)
|
638
|
+
else
|
639
|
+
record.fail(:count)
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|
643
|
+
else
|
644
|
+
handle_missing_checkbox(criteria, record)
|
645
|
+
end
|
646
|
+
else
|
647
|
+
handle_missing_checkbox(criteria, record)
|
648
|
+
end
|
649
|
+
record
|
650
|
+
end
|
651
|
+
|
652
|
+
def filter_combination_values(values, filters)
|
653
|
+
values = values.collect{ |v| filter_value(v, filters) }
|
654
|
+
values
|
655
|
+
end
|
656
|
+
|
657
|
+
def filter_checkbox_values(values, filters)
|
658
|
+
values = filter_combination_values(values, filters)
|
659
|
+
values.delete_if { |v| v.nil? or v.empty? }
|
660
|
+
values
|
661
|
+
end
|
662
|
+
|
663
|
+
def handle_missing_checkbox(criteria, record)
|
664
|
+
if criteria.default.empty?
|
665
|
+
record.fail(:count) unless criteria.count.nil?
|
666
|
+
else
|
667
|
+
record.value = criteria.default
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def validate_field(name, criteria, params)
|
672
|
+
record = Record.new name
|
673
|
+
if params.has_key?(name.to_s)
|
674
|
+
value = params[name.to_s]
|
675
|
+
unless value.kind_of?(Array)
|
676
|
+
value = filter_field_value(value, criteria.filters)
|
677
|
+
record.value = value
|
678
|
+
if value.empty?
|
679
|
+
handle_missing_field(criteria, record)
|
680
|
+
else
|
681
|
+
validate_value(value, criteria, record)
|
682
|
+
end
|
683
|
+
else
|
684
|
+
handle_missing_field(criteria, record)
|
685
|
+
end
|
686
|
+
else
|
687
|
+
handle_missing_field(criteria, record)
|
688
|
+
end
|
689
|
+
record
|
690
|
+
end
|
691
|
+
|
692
|
+
def filter_field_value(value, filters)
|
693
|
+
filter_value(value, filters)
|
694
|
+
end
|
695
|
+
|
696
|
+
def handle_missing_field(criteria, record)
|
697
|
+
if criteria.default.nil?
|
698
|
+
record.fail(:present) if criteria.require_presence?
|
699
|
+
else
|
700
|
+
record.value = criteria.default
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
def find_filter(type)
|
705
|
+
raise ArgumentError.new("unknown filter type: %s" % type) unless @@filter_store.has_key?(type)
|
706
|
+
@@filter_store[type]
|
707
|
+
end
|
708
|
+
|
709
|
+
def find_constraint(type)
|
710
|
+
raise ArgumentError.new("unknown constraint type: %s" % type) unless @@constraint_store.has_key?(type)
|
711
|
+
@@constraint_store[type]
|
712
|
+
end
|
713
|
+
|
714
|
+
def find_combination_constraint(type)
|
715
|
+
raise ArgumentError.new("unknown combination constraint type: %s" % type) unless @@combination_constraint_store.has_key?(type)
|
716
|
+
@@combination_constraint_store[type]
|
717
|
+
end
|
718
|
+
|
719
|
+
def filter_value(value, filters)
|
720
|
+
filters.each { |f| value = find_filter(f).process(value) }
|
721
|
+
value
|
722
|
+
end
|
723
|
+
|
724
|
+
def validate_value(value, criteria, record)
|
725
|
+
criteria.constraints.each do |constraint, arg|
|
726
|
+
result = find_constraint(constraint).validate(value, arg)
|
727
|
+
record.fail(constraint) unless result
|
728
|
+
end
|
729
|
+
end
|
730
|
+
end
|
731
|
+
|
732
|
+
class Respondent
|
733
|
+
|
734
|
+
def initialize
|
735
|
+
replace_method = Proc.new { |elem, param| replase_value(elem, param) }
|
736
|
+
check_method = Proc.new { |elem, param| check_if_selected(elem, param) }
|
737
|
+
@input_elem_fill_methods = {}
|
738
|
+
@input_elem_fill_methods[:text] = replace_method
|
739
|
+
@input_elem_fill_methods[:password] = replace_method
|
740
|
+
@input_elem_fill_methods[:hidden] = replace_method
|
741
|
+
@input_elem_fill_methods[:search] = replace_method
|
742
|
+
@input_elem_fill_methods[:number] = replace_method
|
743
|
+
@input_elem_fill_methods[:range] = replace_method
|
744
|
+
@input_elem_fill_methods[:tel] = replace_method
|
745
|
+
@input_elem_fill_methods[:url] = replace_method
|
746
|
+
@input_elem_fill_methods[:email] = replace_method
|
747
|
+
@input_elem_fill_methods[:time] = replace_method
|
748
|
+
@input_elem_fill_methods[:date] = replace_method
|
749
|
+
@input_elem_fill_methods[:week] = replace_method
|
750
|
+
@input_elem_fill_methods[:color] = replace_method
|
751
|
+
@input_elem_fill_methods[:datetime] = replace_method
|
752
|
+
@input_elem_fill_methods[:"datetime-local"] = replace_method
|
753
|
+
@input_elem_fill_methods[:radio] = check_method
|
754
|
+
@input_elem_fill_methods[:checkbox] = check_method
|
755
|
+
end
|
756
|
+
|
757
|
+
def fill_up(str, params)
|
758
|
+
doc = Hpricot(str)
|
759
|
+
(doc/"input").each do |elem|
|
760
|
+
name = elem[:name]
|
761
|
+
next if name.nil?
|
762
|
+
name = name.sub(/\[\]$/, '');
|
763
|
+
next unless params.has_key?(name.to_s)
|
764
|
+
type = elem[:type]
|
765
|
+
next if type.nil?
|
766
|
+
next unless @input_elem_fill_methods.has_key?(type.to_sym)
|
767
|
+
@input_elem_fill_methods[type.to_sym].call(elem, params[name.to_s])
|
768
|
+
end
|
769
|
+
(doc/"select").each do |select_elem|
|
770
|
+
name = select_elem[:name]
|
771
|
+
next if name.nil?
|
772
|
+
name = name.sub(/\[\]$/, '');
|
773
|
+
next unless params.has_key?(name.to_s)
|
774
|
+
param = params[name.to_s]
|
775
|
+
multiple = select_elem.has_attribute?(:multiple)
|
776
|
+
can_select_more = true
|
777
|
+
(select_elem/"option").each do |option_elem|
|
778
|
+
value = option_elem[:value]
|
779
|
+
next if value.nil? or value.empty?
|
780
|
+
case param
|
781
|
+
when Array
|
782
|
+
if can_select_more and param.include?(value)
|
783
|
+
option_elem[:selected] = "selected"
|
784
|
+
can_select_more = false unless multiple
|
785
|
+
else
|
786
|
+
option_elem.remove_attribute(:selected)
|
787
|
+
end
|
788
|
+
when String
|
789
|
+
if can_select_more and param == value
|
790
|
+
option_elem[:selected] = "selected"
|
791
|
+
can_select_more = false unless multiple
|
792
|
+
else
|
793
|
+
option_elem.remove_attribute(:selected)
|
794
|
+
end
|
795
|
+
else
|
796
|
+
if can_select_more and param.to_s == value
|
797
|
+
option_elem[:selected] = "selected"
|
798
|
+
can_select_more = false unless multiple
|
799
|
+
else
|
800
|
+
option_elem.remove_attribute(:selected)
|
801
|
+
end
|
802
|
+
end
|
803
|
+
end
|
804
|
+
end
|
805
|
+
(doc/"textarea").each do |elem|
|
806
|
+
name = elem[:name]
|
807
|
+
next if name.nil?
|
808
|
+
next unless params.has_key?(name.to_s)
|
809
|
+
param = params[name.to_s]
|
810
|
+
if param.kind_of?(Array)
|
811
|
+
elem.innerHTML = Rack::Utils::escape_html(param[0])
|
812
|
+
else
|
813
|
+
elem.innerHTML = Rack::Utils::escape_html(param)
|
814
|
+
end
|
815
|
+
end
|
816
|
+
doc.to_html
|
817
|
+
end
|
818
|
+
|
819
|
+
private
|
820
|
+
def check_if_selected(elem, param)
|
821
|
+
value = elem[:value]
|
822
|
+
if value.nil?
|
823
|
+
value = ''
|
824
|
+
end
|
825
|
+
if param.kind_of?(Array)
|
826
|
+
if param.collect(&:to_s).include?(value)
|
827
|
+
elem[:checked] = 'checked'
|
828
|
+
else
|
829
|
+
elem.remove_attribute('checked')
|
830
|
+
end
|
831
|
+
else
|
832
|
+
if value == param.to_s
|
833
|
+
elem[:checked] = 'checked'
|
834
|
+
else
|
835
|
+
elem.remove_attribute('checked')
|
836
|
+
end
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
def replase_value(elem, param)
|
841
|
+
if param.kind_of?(Array)
|
842
|
+
elem[:value] = param[0]
|
843
|
+
else
|
844
|
+
elem[:value] = param
|
845
|
+
end
|
846
|
+
end
|
847
|
+
end
|
848
|
+
|
849
|
+
end
|