formeze 1.9.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
- Formeze: A little library for handling form data/input
2
- ======================================================
1
+ formeze
2
+ =======
3
+
4
+
5
+ A little library for handling form data/input.
3
6
 
4
7
 
5
8
  Motivation
@@ -207,14 +210,93 @@ Custom scrub methods can be defined by adding a symbol/proc entry to the
207
210
  `Formeze.scrub_methods` hash.
208
211
 
209
212
 
213
+ Custom validation
214
+ -----------------
215
+
216
+ You may need additional validation logic beyond what the field options
217
+ described above provide, such as validating the format of a field without
218
+ using a regular expression, validating that two fields are equal etc.
219
+ This can be accomplished using the `validates` class method. Pass the
220
+ name of the field to be validated, and a block/proc that encapsulates
221
+ the validation logic. For example:
222
+
223
+ ```ruby
224
+ class ExampleForm < Formeze::Form
225
+ field :email
226
+
227
+ validates :email, &EmailAddress.method(:valid?)
228
+ end
229
+ ```
230
+
231
+ If the block/proc takes no arguments then it will be evaluated in the
232
+ scope of the form instance, which gives you access to the values of other
233
+ fields (and methods defined on the form). For example:
234
+
235
+ ```ruby
236
+ class ExampleForm < Formeze::Form
237
+ field :password
238
+ field :password_confirmation
239
+
240
+ validates :password_confirmation do
241
+ password_confirmation == password
242
+ end
243
+ end
244
+ ```
245
+
246
+ Specify the `when` option with a proc to peform the validation conditionally.
247
+ Similar to the `defined_if` and `defined_unless` field options, the proc is
248
+ evaluated in the scope of the form instance. For example:
249
+
250
+ ```ruby
251
+ class ExampleForm < Formeze::Form
252
+ field :business_name, :defined_if => :business_account?
253
+ field :vat_number, :defined_if => :business_account?
254
+
255
+ validates :vat_number, :when => :business_account? do
256
+ # ...
257
+ end
258
+
259
+ def initialize(account)
260
+ @account = account
261
+ end
262
+
263
+ def business_account?
264
+ @account.business?
265
+ end
266
+ end
267
+ ```
268
+
269
+ Specify the `error` option with a symbol to control which error the validation
270
+ generates. The I18n integration described below can be used to specify the
271
+ error message used, both for errors that are explicitly specified using this
272
+ option, and the default "invalid" error. For example:
273
+
274
+ ```ruby
275
+ class ExampleForm < Formeze::Form
276
+ field :email
277
+ field :password
278
+ field :password_confirmation
279
+
280
+ validates :email, &EmailAddress.method(:valid?)
281
+
282
+ validates :password_confirmation, :error => :does_not_match do
283
+ password_confirmation == password
284
+ end
285
+ end
286
+ ```
287
+
288
+ The error for the email validation would use the `formeze.errors.invalid`
289
+ I18n key, defaulting to "is invalid". The error message for the password
290
+ confirmation validation would use the `formeze.errors.does_not_match` key.
291
+
292
+
210
293
  Rails usage
211
294
  -----------
212
295
 
213
296
  This is the basic pattern for using a formeze form in a Rails controller:
214
297
 
215
298
  ```ruby
216
- form = SomeForm.new
217
- form.parse(request.raw_post)
299
+ form = SomeForm.parse(request.raw_post)
218
300
 
219
301
  if form.valid?
220
302
  # do something with form data
@@ -234,8 +316,7 @@ Using formeze with sinatra is similar, the only difference is that there is
234
316
  no raw_post method on the request object so the body has to be read directly:
235
317
 
236
318
  ```ruby
237
- form = SomeForm.new
238
- form.parse(request.body.read)
319
+ form = SomeForm.parse(request.body.read)
239
320
 
240
321
  if form.valid?
241
322
  # do something with form data
@@ -4,4 +4,5 @@ task :default => :spec
4
4
 
5
5
  Rake::TestTask.new(:spec) do |t|
6
6
  t.test_files = FileList['spec/*_spec.rb']
7
+ t.warning = true
7
8
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'formeze'
3
- s.version = '1.9.1'
3
+ s.version = '2.0.0'
4
4
  s.platform = Gem::Platform::RUBY
5
5
  s.authors = ['Tim Craft']
6
6
  s.email = ['mail@timcraft.com']
@@ -16,32 +16,36 @@ module Formeze
16
16
 
17
17
  def initialize(name, options = {})
18
18
  @name, @options = name, options
19
+
20
+ if options.has_key?(:word_limit)
21
+ Kernel.warn '[formeze] :word_limit option is deprecated, please use custom validation instead'
22
+ end
19
23
  end
20
24
 
21
25
  def validate(value, form)
22
26
  value = Formeze.scrub(value, @options[:scrub])
23
27
 
24
28
  if value !~ /\S/
25
- yield error(:required, 'is required') if required?
29
+ form.add_error(self, error(:required, 'is required')) if required?
26
30
 
27
31
  form.send(:"#{name}=", blank_value? ? blank_value : value)
28
32
  else
29
- yield error(:not_multiline, 'cannot contain newlines') if !multiline? && value.lines.count > 1
33
+ form.add_error(self, error(:not_multiline, 'cannot contain newlines')) if !multiline? && value.lines.count > 1
30
34
 
31
- yield error(:too_long, 'is too long') if too_long?(value)
35
+ form.add_error(self, error(:too_long, 'is too long')) if too_long?(value)
32
36
 
33
- yield error(:too_short, 'is too short') if too_short?(value)
37
+ form.add_error(self, error(:too_short, 'is too short')) if too_short?(value)
34
38
 
35
- yield error(:no_match, 'is invalid') if no_match?(value)
39
+ form.add_error(self, error(:no_match, 'is invalid')) if no_match?(value)
36
40
 
37
- yield error(:bad_value, 'is invalid') if values? && !values.include?(value)
41
+ form.add_error(self, error(:bad_value, 'is invalid')) if values? && !values.include?(value)
38
42
 
39
43
  form.send(:"#{name}=", value)
40
44
  end
41
45
  end
42
46
 
43
- def error(i18n_key, default)
44
- translate(i18n_key, :scope => [:formeze, :errors], :default => default)
47
+ def error(key, default)
48
+ Formeze.translate(key, :scope => [:formeze, :errors], :default => default)
45
49
  end
46
50
 
47
51
  def key
@@ -53,7 +57,7 @@ module Formeze
53
57
  end
54
58
 
55
59
  def label
56
- @options.fetch(:label) { translate(name, :scope => [:formeze, :labels], :default => Label.new(name)) }
60
+ @options.fetch(:label) { Formeze.translate(name, :scope => [:formeze, :labels], :default => Label.new(name)) }
57
61
  end
58
62
 
59
63
  def required?
@@ -77,15 +81,7 @@ module Formeze
77
81
  end
78
82
 
79
83
  def too_many_characters?(value)
80
- if @options.has_key?(:maxlength)
81
- value.chars.count > @options.fetch(:maxlength)
82
- elsif @options.has_key?(:char_limit)
83
- Kernel.warn '[formeze] :char_limit option is deprecated, please use :maxlength instead'
84
-
85
- value.chars.count > @options.fetch(:char_limit)
86
- else
87
- value.chars.count > 64
88
- end
84
+ value.chars.count > @options.fetch(:maxlength) { 64 }
89
85
  end
90
86
 
91
87
  def too_many_words?(value)
@@ -127,12 +123,60 @@ module Formeze
127
123
  def defined_unless
128
124
  @options.fetch(:defined_unless)
129
125
  end
126
+ end
130
127
 
131
- def translate(key, options)
132
- if defined?(I18n)
133
- I18n.translate(key, options)
134
- else
135
- options.fetch(:default)
128
+ class FieldSet
129
+ include Enumerable
130
+
131
+ def initialize
132
+ @fields, @index = [], {}
133
+ end
134
+
135
+ def each(&block)
136
+ @fields.each(&block)
137
+ end
138
+
139
+ def <<(field)
140
+ @fields << field
141
+
142
+ @index[field.name] = field
143
+ end
144
+
145
+ def [](field_name)
146
+ @index.fetch(field_name)
147
+ end
148
+ end
149
+
150
+ class Validation
151
+ def initialize(field, options, &block)
152
+ @field, @options, @block = field, options, block
153
+ end
154
+
155
+ def error_key
156
+ @options.fetch(:error) { :invalid }
157
+ end
158
+
159
+ def error_message
160
+ Formeze.translate(error_key, :scope => [:formeze, :errors], :default => 'is invalid')
161
+ end
162
+
163
+ def validates?(form)
164
+ @options.has_key?(:when) ? form.instance_eval(&@options[:when]) : true
165
+ end
166
+
167
+ def value?(form)
168
+ form.send(@field.name) =~ /\S/
169
+ end
170
+
171
+ def validate(form)
172
+ if validates?(form) && value?(form)
173
+ return_value = if @block.arity == 1
174
+ @block.call(form.send(@field.name))
175
+ else
176
+ form.instance_eval(&@block)
177
+ end
178
+
179
+ form.add_error(@field, error_message) unless return_value
136
180
  end
137
181
  end
138
182
  end
@@ -164,7 +208,7 @@ module Formeze
164
208
  include ArrayAttrAccessor
165
209
 
166
210
  def fields
167
- @fields ||= []
211
+ @fields ||= FieldSet.new
168
212
  end
169
213
 
170
214
  def field(*args)
@@ -179,20 +223,12 @@ module Formeze
179
223
  end
180
224
  end
181
225
 
182
- def checks
183
- @checks ||= []
226
+ def validations
227
+ @validations ||= []
184
228
  end
185
229
 
186
- def check(&block)
187
- checks << block
188
- end
189
-
190
- def errors
191
- @errors ||= []
192
- end
193
-
194
- def error(message)
195
- errors << message
230
+ def validates(field_name, options = {}, &block)
231
+ validations << Validation.new(fields[field_name], options, &block)
196
232
  end
197
233
 
198
234
  def parse(encoded_form_data)
@@ -236,23 +272,33 @@ module Formeze
236
272
  end
237
273
 
238
274
  values.each do |value|
239
- field.validate(value, self) do |error|
240
- error!("#{field.label} #{error}", field.name)
241
- end
275
+ field.validate(value, self)
242
276
  end
243
277
  end
244
278
 
245
279
  if defined?(Rails)
246
- %w(utf8 authenticity_token).each { |field_key| form_data.delete(field_key) }
280
+ %w(utf8 authenticity_token).each do |key|
281
+ form_data.delete(key)
282
+ end
247
283
  end
248
284
 
249
- raise KeyError unless form_data.empty?
285
+ unless form_data.empty?
286
+ raise KeyError, "unexpected form keys: #{form_data.keys.sort.join(', ')}"
287
+ end
250
288
 
251
- self.class.checks.zip(self.class.errors) do |check, message|
252
- instance_eval(&check) ? next : error!(message)
289
+ self.class.validations.each do |validation|
290
+ validation.validate(self)
253
291
  end
254
292
  end
255
293
 
294
+ def add_error(field, message)
295
+ error = ValidationError.new("#{field.label} #{message}")
296
+
297
+ errors << error
298
+
299
+ field_errors[field.name] << error
300
+ end
301
+
256
302
  def valid?
257
303
  errors.empty?
258
304
  end
@@ -273,13 +319,15 @@ module Formeze
273
319
  field_errors[field_name]
274
320
  end
275
321
 
276
- def to_hash
322
+ def to_h
277
323
  self.class.fields.inject({}) do |hash, field|
278
324
  hash[field.name] = send(field.name)
279
325
  hash
280
326
  end
281
327
  end
282
328
 
329
+ alias_method :to_hash, :to_h
330
+
283
331
  private
284
332
 
285
333
  def field_defined?(field)
@@ -295,14 +343,6 @@ module Formeze
295
343
  def field_errors
296
344
  @field_errors ||= Hash.new { |h, k| h[k] = [] }
297
345
  end
298
-
299
- def error!(message, field_name = nil)
300
- error = ValidationError.new(message)
301
-
302
- errors << error
303
-
304
- field_errors[field_name] << error unless field_name.nil?
305
- end
306
346
  end
307
347
 
308
348
  def self.scrub_methods
@@ -321,6 +361,10 @@ module Formeze
321
361
  end
322
362
  end
323
363
 
364
+ def self.translate(key, options)
365
+ defined?(I18n) ? I18n.translate(key, options) : options.fetch(:default)
366
+ end
367
+
324
368
  def self.setup(form)
325
369
  form.send :include, InstanceMethods
326
370
 
@@ -38,8 +38,10 @@ describe 'FormWithField' do
38
38
  proc { @form.parse('title=foo&title=bar') }.must_raise(Formeze::ValueError)
39
39
  end
40
40
 
41
- it 'raises an exception when there is an unexpected key' do
42
- proc { @form.parse('title=Untitled&foo=bar') }.must_raise(Formeze::KeyError)
41
+ it 'raises an exception when the data contains unexpected keys' do
42
+ exception = proc { @form.parse('title=Untitled&foo=bar&baz=') }.must_raise(Formeze::KeyError)
43
+
44
+ exception.message.must_equal('unexpected form keys: baz, foo')
43
45
  end
44
46
  end
45
47
 
@@ -105,6 +107,12 @@ describe 'FormWithField after parsing valid input' do
105
107
  end
106
108
  end
107
109
 
110
+ describe 'to_h method' do
111
+ it 'returns a hash containing the field name and its value' do
112
+ @form.to_h.must_equal({:title => 'Untitled'})
113
+ end
114
+ end
115
+
108
116
  describe 'to_hash method' do
109
117
  it 'returns a hash containing the field name and its value' do
110
118
  @form.to_hash.must_equal({:title => 'Untitled'})
@@ -227,23 +235,6 @@ describe 'FormWithFieldThatCanHaveMultipleLines after parsing input containing n
227
235
  end
228
236
  end
229
237
 
230
- class FormWithCharacterLimitedField < Formeze::Form
231
- field :title, :char_limit => 16
232
- end
233
-
234
- describe 'FormWithCharacterLimitedField after parsing input with too many characters' do
235
- before do
236
- @form = FormWithCharacterLimitedField.new
237
- @form.parse('title=This+Title+Will+Be+Too+Long')
238
- end
239
-
240
- describe 'valid query method' do
241
- it 'returns false' do
242
- @form.valid?.must_equal(false)
243
- end
244
- end
245
- end
246
-
247
238
  class FormWithMaxLengthField < Formeze::Form
248
239
  field :title, :maxlength => 16
249
240
  end
@@ -509,16 +500,55 @@ describe 'FormWithHaltingCondition after parsing input with same_address set and
509
500
  end
510
501
  end
511
502
 
512
- class FormWithCustomValidation < Formeze::Form
503
+ class FormWithOptionalKey < Formeze::Form
504
+ field :accept_terms, :values => %w(true), :key_required => false
505
+ end
506
+
507
+ describe 'FormWithOptionalKey after parsing input without the key' do
508
+ before do
509
+ @form = FormWithOptionalKey.new
510
+ @form.parse('')
511
+ end
512
+
513
+ describe 'valid query method' do
514
+ it 'returns true' do
515
+ @form.valid?.must_equal(true)
516
+ end
517
+ end
518
+ end
519
+
520
+ class FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues < Formeze::Form
521
+ field :size, :required => false, :values => %w(S M L XL)
522
+ end
523
+
524
+ describe 'FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues after parsing blank input' do
525
+ before do
526
+ @form = FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues.new
527
+ @form.parse('size=')
528
+ end
529
+
530
+ describe 'valid query method' do
531
+ it 'returns true' do
532
+ @form.valid?.must_equal(true)
533
+ end
534
+ end
535
+ end
536
+
537
+ module EmailAddress
538
+ def self.valid?(address)
539
+ address.include?('@')
540
+ end
541
+ end
542
+
543
+ class FormWithCustomEmailValidation < Formeze::Form
513
544
  field :email
514
545
 
515
- check { email.include?(?@) }
516
- error 'Email is invalid'
546
+ validates :email, &EmailAddress.method(:valid?)
517
547
  end
518
548
 
519
- describe 'FormWithCustomValidation after parsing invalid input' do
549
+ describe 'FormWithCustomEmailValidation after parsing invalid input' do
520
550
  before do
521
- @form = FormWithCustomValidation.new
551
+ @form = FormWithCustomEmailValidation.new
522
552
  @form.parse('email=alice')
523
553
  end
524
554
 
@@ -527,33 +557,85 @@ describe 'FormWithCustomValidation after parsing invalid input' do
527
557
  @form.valid?.must_equal(false)
528
558
  end
529
559
  end
560
+
561
+ describe 'errors method' do
562
+ it 'includes a generic error message for the named field' do
563
+ @form.errors.map(&:to_s).must_include('Email is invalid')
564
+ end
565
+ end
566
+
567
+ describe 'errors_on query method' do
568
+ it 'returns true when given the field name' do
569
+ @form.errors_on?(:email).must_equal(true)
570
+ end
571
+ end
530
572
  end
531
573
 
532
- class FormWithOptionalKey < Formeze::Form
533
- field :accept_terms, :values => %w(true), :key_required => false
574
+ describe 'FormWithCustomEmailValidation after parsing blank input' do
575
+ before do
576
+ @form = FormWithCustomEmailValidation.new
577
+ @form.parse('email=')
578
+ end
579
+
580
+ describe 'errors method' do
581
+ it 'will not include the custom validation error message' do
582
+ @form.errors.map(&:to_s).wont_include('Email is invalid')
583
+ end
584
+ end
534
585
  end
535
586
 
536
- describe 'FormWithOptionalKey after parsing input without the key' do
587
+ class FormWithCustomPasswordConfirmationCheck < Formeze::Form
588
+ field :password
589
+ field :password_confirmation
590
+
591
+ validates :password_confirmation, :error => :does_not_match do
592
+ password_confirmation == password
593
+ end
594
+ end
595
+
596
+ describe 'FormWithCustomPasswordConfirmationCheck after parsing invalid input' do
537
597
  before do
538
- @form = FormWithOptionalKey.new
539
- @form.parse('')
598
+ @form = FormWithCustomPasswordConfirmationCheck.new
599
+ @form.parse('password=foo&password_confirmation=bar')
540
600
  end
541
601
 
542
602
  describe 'valid query method' do
543
- it 'returns true' do
544
- @form.valid?.must_equal(true)
603
+ it 'returns false' do
604
+ @form.valid?.must_equal(false)
605
+ end
606
+ end
607
+
608
+ describe 'errors method' do
609
+ it 'includes a generic error message for the named field' do
610
+ @form.errors.map(&:to_s).must_include('Password confirmation is invalid')
611
+ end
612
+ end
613
+
614
+ describe 'errors_on query method' do
615
+ it 'returns true when given the field name' do
616
+ @form.errors_on?(:password_confirmation).must_equal(true)
545
617
  end
546
618
  end
547
619
  end
548
620
 
549
- class FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues < Formeze::Form
550
- field :size, :required => false, :values => %w(S M L XL)
621
+ class FormWithCustomMinimumSpendValidation < Formeze::Form
622
+ field :minimum_spend
623
+
624
+ field :fixed_discount, :required => false, :blank => nil
625
+
626
+ validates :minimum_spend, :when => :fixed_discount? do
627
+ minimum_spend.to_f > 0
628
+ end
629
+
630
+ def fixed_discount?
631
+ !fixed_discount.nil?
632
+ end
551
633
  end
552
634
 
553
- describe 'FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues after parsing blank input' do
635
+ describe 'FormWithCustomMinimumSpendValidation after parsing valid input' do
554
636
  before do
555
- @form = FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues.new
556
- @form.parse('size=')
637
+ @form = FormWithCustomMinimumSpendValidation.new
638
+ @form.parse('minimum_spend=0.00&fixed_discount=')
557
639
  end
558
640
 
559
641
  describe 'valid query method' do
@@ -561,6 +643,43 @@ describe 'FormWithOptionalFieldThatCanOnlyHaveSpecifiedValues after parsing blan
561
643
  @form.valid?.must_equal(true)
562
644
  end
563
645
  end
646
+
647
+ describe 'errors method' do
648
+ it 'returns an empty array' do
649
+ @form.errors.must_be_empty
650
+ end
651
+ end
652
+
653
+ describe 'errors_on query method' do
654
+ it 'returns false when given the field name' do
655
+ @form.errors_on?(:minimum_spend).must_equal(false)
656
+ end
657
+ end
658
+ end
659
+
660
+ describe 'FormWithCustomMinimumSpendValidation after parsing invalid input' do
661
+ before do
662
+ @form = FormWithCustomMinimumSpendValidation.new
663
+ @form.parse('minimum_spend=0.00&fixed_discount=10%')
664
+ end
665
+
666
+ describe 'valid query method' do
667
+ it 'returns false' do
668
+ @form.valid?.must_equal(false)
669
+ end
670
+ end
671
+
672
+ describe 'errors method' do
673
+ it 'includes a generic error message for the named field' do
674
+ @form.errors.map(&:to_s).must_include('Minimum spend is invalid')
675
+ end
676
+ end
677
+
678
+ describe 'errors_on query method' do
679
+ it 'returns true when given the field name' do
680
+ @form.errors_on?(:minimum_spend).must_equal(true)
681
+ end
682
+ end
564
683
  end
565
684
 
566
685
  describe 'FormWithField on Rails' do
@@ -601,6 +720,22 @@ describe 'I18n integration' do
601
720
  form.errors.first.to_s.must_equal('Title cannot be blank')
602
721
  end
603
722
 
723
+ it 'provides i18n support for overriding the default custom validation error message' do
724
+ I18n.backend.store_translations :en, {:formeze => {:errors => {:invalid => 'is not valid'}}}
725
+
726
+ form = FormWithCustomEmailValidation.new
727
+ form.parse('email=alice')
728
+ form.errors.first.to_s.must_equal('Email is not valid')
729
+ end
730
+
731
+ it 'provides i18n support for specifying custom validation error messages' do
732
+ I18n.backend.store_translations :en, {:formeze => {:errors => {:does_not_match => 'does not match'}}}
733
+
734
+ form = FormWithCustomPasswordConfirmationCheck.new
735
+ form.parse('password=foo&password_confirmation=bar')
736
+ form.errors.first.to_s.must_equal('Password confirmation does not match')
737
+ end
738
+
604
739
  it 'provides i18n support for specifying field labels globally' do
605
740
  I18n.backend.store_translations :en, {:formeze => {:labels => {:title => 'TITLE'}}}
606
741
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: formeze
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.1
4
+ version: 2.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-06 00:00:00.000000000 Z
12
+ date: 2013-06-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rake
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  version: '0'
76
76
  requirements: []
77
77
  rubyforge_project:
78
- rubygems_version: 1.8.24
78
+ rubygems_version: 1.8.25
79
79
  signing_key:
80
80
  specification_version: 3
81
81
  summary: See description