formulary 0.0.1 → 0.0.2

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