formeze 1.9.1 → 2.0.0

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/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