formulary 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/README.md +50 -6
  2. data/formulary.gemspec +1 -0
  3. data/lib/formulary.rb +13 -0
  4. data/lib/formulary/html_form.rb +39 -58
  5. data/lib/formulary/html_form/fields.rb +21 -0
  6. data/lib/formulary/html_form/fields/checkbox_group.rb +43 -0
  7. data/lib/formulary/html_form/fields/email_input.rb +23 -0
  8. data/lib/formulary/html_form/fields/field.rb +45 -0
  9. data/lib/formulary/html_form/fields/field_group.rb +26 -0
  10. data/lib/formulary/html_form/fields/hidden_input.rb +7 -0
  11. data/lib/formulary/html_form/fields/input.rb +32 -0
  12. data/lib/formulary/html_form/fields/radio_button_group.rb +29 -0
  13. data/lib/formulary/html_form/fields/select.rb +24 -0
  14. data/lib/formulary/html_form/fields/tel_input.rb +7 -0
  15. data/lib/formulary/html_form/fields/text_input.rb +7 -0
  16. data/lib/formulary/html_form/fields/textarea.rb +12 -0
  17. data/lib/formulary/version.rb +1 -1
  18. data/spec/html_form/fields/checkbox_group_spec.rb +141 -0
  19. data/spec/html_form/fields/email_input_spec.rb +55 -0
  20. data/spec/html_form/fields/field_spec.rb +10 -0
  21. data/spec/html_form/fields/hidden_input_spec.rb +37 -0
  22. data/spec/html_form/fields/input_spec.rb +17 -0
  23. data/spec/html_form/fields/radio_button_group_spec.rb +94 -0
  24. data/spec/html_form/fields/select_spec.rb +62 -0
  25. data/spec/html_form/fields/tel_input_spec.rb +29 -0
  26. data/spec/html_form/fields/text_input_spec.rb +29 -0
  27. data/spec/html_form/fields/textarea_spec.rb +28 -0
  28. data/spec/html_form_spec.rb +53 -109
  29. data/spec/spec_helper.rb +5 -10
  30. data/spec/support/element_helper.rb +15 -0
  31. data/spec/support/shared_examples_for_pattern.rb +15 -0
  32. data/spec/support/shared_examples_for_required.rb +35 -0
  33. metadata +149 -94
  34. checksums.yaml +0 -7
data/README.md CHANGED
@@ -4,11 +4,16 @@
4
4
  >
5
5
  > -- <cite><a href="http://en.wikipedia.org/wiki/Formulary_%28model_documents%29">Wikipedia</a></cite>
6
6
 
7
+ A Ruby gem to parse HTML5 forms and decompose them into model validation using their field types (email, url, number, etc) and form attributes (required, pattern, etc).
8
+
9
+
7
10
  ## Installation
8
11
 
9
12
  Add this line to your application's Gemfile:
10
13
 
11
- gem 'formulary'
14
+ ```ruby
15
+ gem 'formulary'
16
+ ```
12
17
 
13
18
  And then execute:
14
19
 
@@ -18,6 +23,7 @@ Or install it yourself as:
18
23
 
19
24
  $ gem install formulary
20
25
 
26
+
21
27
  ## Usage
22
28
 
23
29
  Create a new Formulary Form
@@ -55,21 +61,59 @@ html_form.valid?({ unknown: "value" })
55
61
  # => Formulary::UnexpectedParameter: Got unexpected field 'unknown'
56
62
  ```
57
63
 
64
+
58
65
  ## Currently Supported
59
66
 
60
67
  - type="email"
61
68
  - required
62
69
  - pattern="REGEX"
70
+ - selects, selected value is one of the options
71
+
63
72
 
64
73
  ## TODO
65
74
 
66
- - select, checkbox, radio and multiselect tags have one of the valid options selected
75
+ - checkbox, radio and multiselect tags have one of the valid options selected
67
76
  - validate [other html5 field types](http://www.w3schools.com/html/html5_form_input_types.asp)
68
77
 
78
+
79
+ ## Authors
80
+
81
+ * Don Petersen / [@dpetersen](https://github.com/dpetersen)
82
+ * Matt Bohme / [@quady](https://github.com/quady)
83
+
84
+
69
85
  ## Contributing
70
86
 
71
87
  1. Fork it
72
- 2. Create your feature branch (`git checkout -b my-new-feature`)
73
- 3. Commit your changes (`git commit -am 'Add some feature'`)
74
- 4. Push to the branch (`git push origin my-new-feature`)
75
- 5. Create new Pull Request
88
+ 2. Get it running (see Installation above)
89
+ 3. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 4. Write your code and **specs**
91
+ 5. Commit your changes (`git commit -am 'Add some feature'`)
92
+ 6. Push to the branch (`git push origin my-new-feature`)
93
+ 7. Create new Pull Request
94
+
95
+
96
+ ## License
97
+
98
+ Copyright (c) 2013 G5
99
+
100
+ MIT License
101
+
102
+ Permission is hereby granted, free of charge, to any person obtaining
103
+ a copy of this software and associated documentation files (the
104
+ "Software"), to deal in the Software without restriction, including
105
+ without limitation the rights to use, copy, modify, merge, publish,
106
+ distribute, sublicense, and/or sell copies of the Software, and to
107
+ permit persons to whom the Software is furnished to do so, subject to
108
+ the following conditions:
109
+
110
+ The above copyright notice and this permission notice shall be
111
+ included in all copies or substantial portions of the Software.
112
+
113
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
114
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
115
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
116
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
117
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
118
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
119
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/formulary.gemspec CHANGED
@@ -25,4 +25,5 @@ Gem::Specification.new do |spec|
25
25
  spec.add_development_dependency "bundler", "~> 1.3"
26
26
  spec.add_development_dependency "rake"
27
27
  spec.add_development_dependency "rspec"
28
+ spec.add_development_dependency "pry"
28
29
  end
data/lib/formulary.rb CHANGED
@@ -1,6 +1,19 @@
1
1
  require 'nokogiri'
2
2
  require 'active_support/core_ext/object/blank'
3
+ require 'active_support/core_ext/object/try'
3
4
  require 'email_veracity'
4
5
 
6
+ # Don't try and determine if this email address is legitimate, just validate
7
+ # the provided address.
8
+ EmailVeracity::Config[:skip_lookup] = true
9
+
10
+ module Formulary
11
+ class HtmlForm
12
+ FIELD_TYPES = []
13
+ FIELD_GROUP_TYPES = []
14
+ end
15
+ end
16
+
5
17
  require "formulary/version"
18
+ require "formulary/html_form/fields"
6
19
  require "formulary/html_form"
@@ -1,58 +1,19 @@
1
1
  module Formulary
2
2
  class HtmlForm
3
- class Field
4
- attr_accessor :name, :type, :required, :pattern
5
-
6
- def initialize(name, type, required, pattern=nil)
7
- @name = name
8
- @type = type
9
- @required = required
10
- @pattern = pattern
11
- end
12
-
13
- def set_value(value)
14
- @value = value
15
- end
16
-
17
- def valid?
18
- presence_correct && pattern_correct && correct_for_type
19
- end
20
-
21
- def error
22
- return "required" unless presence_correct
23
- return "format" unless pattern_correct
24
- return "not a valid #{@type}" unless correct_for_type
25
- end
26
-
27
- protected
3
+ SINGULAR_FIELD_SELECTOR = <<-EOS
4
+ input[type!='submit'][type!='radio'][type!='checkbox'],
5
+ textarea,
6
+ select
7
+ EOS
28
8
 
29
- def presence_correct
30
- !required || @value.present?
31
- end
32
-
33
- def pattern_correct
34
- return true if @pattern.blank? || @value.blank?
35
- @value.match(Regexp.new(@pattern))
36
- end
37
-
38
- def correct_for_type
39
- return true if @value.blank?
40
-
41
- case @type
42
- when "email"
43
- EmailVeracity::Address.new(@value).valid?
44
- else
45
- true
46
- end
47
- end
48
- end
9
+ GROUPED_FIELD_SELECTOR = <<-EOS
10
+ input[type='radio'],
11
+ input[type='checkbox']
12
+ EOS
49
13
 
50
14
  def initialize(markup)
51
15
  @markup = markup
52
- end
53
-
54
- def fields
55
- @fields ||= build_fields
16
+ fields
56
17
  end
57
18
 
58
19
  def valid?(params)
@@ -73,29 +34,49 @@ module Formulary
73
34
 
74
35
  protected
75
36
 
37
+ def fields
38
+ @fields || build_fields
39
+ end
40
+
76
41
  def find_field(name)
77
42
  fields.detect { |field| field.name == name.to_s }
78
43
  end
79
44
 
80
45
  def build_fields
46
+ @fields = []
81
47
  doc = Nokogiri::HTML(@markup)
82
48
 
83
- doc.css("input[type!='submit'], textarea").map do |input|
84
- type = input.name == "textarea" ? "textarea" : input.attributes["type"].value
85
- pattern = input.attributes.include?("pattern") ? input.attributes["pattern"].value : nil
49
+ build_singular_fields_from(doc)
50
+ build_grouped_fields_from(doc)
51
+ end
86
52
 
53
+ def build_singular_fields_from(doc)
54
+ doc.css(SINGULAR_FIELD_SELECTOR.strip).map do |element|
55
+ field_klass = FIELD_TYPES.detect { |k| k.compatible_with?(element) }
56
+ if field_klass.nil?
57
+ raise UnsupportedFieldType.new("I can't handle this field: #{element.inspect}")
58
+ end
59
+ @fields << field_klass.new(element)
60
+ end
61
+ end
87
62
 
88
- Field.new(
89
- input.attributes["name"].value,
90
- type,
91
- input.attributes.include?("required"),
92
- pattern
93
- )
63
+ def build_grouped_fields_from(doc)
64
+ grouped_elements = doc.css(GROUPED_FIELD_SELECTOR.strip).group_by do |element|
65
+ element.attributes["name"].value
66
+ end
67
+
68
+ grouped_elements.each do |element_group|
69
+ group_name, elements = *element_group
70
+
71
+ group_klass = FIELD_GROUP_TYPES.detect { |k| k.compatible_with?(elements) }
72
+ @fields << group_klass.new(group_name, elements)
94
73
  end
95
74
  end
96
75
  end
97
76
 
98
77
  class UnexpectedParameter < StandardError
78
+ end
99
79
 
80
+ class UnsupportedFieldType < StandardError
100
81
  end
101
82
  end
@@ -0,0 +1,21 @@
1
+ require 'formulary/html_form/fields/field'
2
+ require 'formulary/html_form/fields/field_group'
3
+ require 'formulary/html_form/fields/input'
4
+
5
+ require 'formulary/html_form/fields/text_input'
6
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::TextInput
7
+ require 'formulary/html_form/fields/email_input'
8
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::EmailInput
9
+ require 'formulary/html_form/fields/tel_input'
10
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::TelInput
11
+ require 'formulary/html_form/fields/hidden_input'
12
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::HiddenInput
13
+ require 'formulary/html_form/fields/textarea'
14
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::Textarea
15
+ require 'formulary/html_form/fields/select'
16
+ Formulary::HtmlForm::FIELD_TYPES << Formulary::HtmlForm::Fields::Select
17
+
18
+ require 'formulary/html_form/fields/radio_button_group'
19
+ Formulary::HtmlForm::FIELD_GROUP_TYPES << Formulary::HtmlForm::Fields::RadioButtonGroup
20
+ require 'formulary/html_form/fields/checkbox_group'
21
+ Formulary::HtmlForm::FIELD_GROUP_TYPES << Formulary::HtmlForm::Fields::CheckboxGroup
@@ -0,0 +1,43 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class CheckboxGroup < FieldGroup
3
+ def self.compatible_type
4
+ "checkbox"
5
+ end
6
+
7
+ def self.supports_required?
8
+ true
9
+ end
10
+
11
+ def initialize(group_name, elements)
12
+ super
13
+ @values = []
14
+ end
15
+
16
+ def set_value(value)
17
+ @values = [value].flatten
18
+ end
19
+
20
+ protected
21
+
22
+ def presence_correct?
23
+ @elements.each do |element|
24
+ if element.attributes.include?("required")
25
+ return false unless @values.include?(value_from_element(element))
26
+ end
27
+ end
28
+ return true
29
+ end
30
+
31
+ def value_in_list?
32
+ return true if @values.empty?
33
+ allowed_values = @elements.map { |e| value_from_element(e) }
34
+ (allowed_values & @values) == @values
35
+ end
36
+
37
+ # Our exhaustive testing concludes that browsers submit "on" when the
38
+ # checkbox has no value.
39
+ def value_from_element(element)
40
+ element.attributes["value"].try(:value) || "on"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class EmailInput < Input
3
+ def self.compatible_type
4
+ "email"
5
+ end
6
+
7
+ def valid?
8
+ super && email_correct?
9
+ end
10
+
11
+ def error
12
+ return super if super.present?
13
+ return "email" unless email_correct?
14
+ end
15
+
16
+ protected
17
+
18
+ def email_correct?
19
+ return true if @value.blank?
20
+ EmailVeracity::Address.new(@value).valid?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class Field
3
+ def self.supports_required?
4
+ false
5
+ end
6
+
7
+ def initialize(element)
8
+ @element = element
9
+ end
10
+
11
+ def name
12
+ @element.attributes["name"].value
13
+ end
14
+
15
+ def set_value(value)
16
+ @value = value
17
+ end
18
+
19
+ def valid?
20
+ supports_required? && presence_correct?
21
+ end
22
+
23
+ def error
24
+ return "required" if supports_required? && !presence_correct?
25
+ end
26
+
27
+ protected
28
+
29
+ def supports_required?
30
+ self.class.supports_required?
31
+ end
32
+
33
+ def presence_correct?
34
+ if required? && @value.blank?
35
+ return false
36
+ else
37
+ return true
38
+ end
39
+ end
40
+
41
+ def required?
42
+ @element.attributes.include?("required")
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class FieldGroup < Field
3
+ def self.compatible_with?(elements)
4
+ elements.all? do |e|
5
+ e.name == "input" && e.attributes["type"].value == compatible_type
6
+ end
7
+ end
8
+
9
+ def initialize(group_name, elements)
10
+ @group_name, @elements = group_name, elements
11
+ end
12
+
13
+ def name
14
+ @group_name
15
+ end
16
+
17
+ def valid?
18
+ super && value_in_list?
19
+ end
20
+
21
+ def error
22
+ return super if super.present?
23
+ return "choose" if !value_in_list?
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,7 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class HiddenInput < Input
3
+ def self.compatible_type
4
+ "hidden"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ module Formulary::HtmlForm::Fields
2
+ class Input < Field
3
+ def self.compatible_with?(element)
4
+ element.name == "input" &&
5
+ element.attributes["type"].value == compatible_type
6
+ end
7
+
8
+ def self.supports_required?
9
+ true
10
+ end
11
+
12
+ def valid?
13
+ super && pattern_correct?
14
+ end
15
+
16
+ def error
17
+ return super if super.present?
18
+ return "format" unless pattern_correct?
19
+ end
20
+
21
+ protected
22
+
23
+ def pattern_correct?
24
+ return true if pattern.blank? || @value.blank?
25
+ @value.match(Regexp.new(pattern))
26
+ end
27
+
28
+ def pattern
29
+ @element.attributes["pattern"].try(:value)
30
+ end
31
+ end
32
+ end