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.
- data/README.md +50 -6
- data/formulary.gemspec +1 -0
- data/lib/formulary.rb +13 -0
- data/lib/formulary/html_form.rb +39 -58
- data/lib/formulary/html_form/fields.rb +21 -0
- data/lib/formulary/html_form/fields/checkbox_group.rb +43 -0
- data/lib/formulary/html_form/fields/email_input.rb +23 -0
- data/lib/formulary/html_form/fields/field.rb +45 -0
- data/lib/formulary/html_form/fields/field_group.rb +26 -0
- data/lib/formulary/html_form/fields/hidden_input.rb +7 -0
- data/lib/formulary/html_form/fields/input.rb +32 -0
- data/lib/formulary/html_form/fields/radio_button_group.rb +29 -0
- data/lib/formulary/html_form/fields/select.rb +24 -0
- data/lib/formulary/html_form/fields/tel_input.rb +7 -0
- data/lib/formulary/html_form/fields/text_input.rb +7 -0
- data/lib/formulary/html_form/fields/textarea.rb +12 -0
- data/lib/formulary/version.rb +1 -1
- data/spec/html_form/fields/checkbox_group_spec.rb +141 -0
- data/spec/html_form/fields/email_input_spec.rb +55 -0
- data/spec/html_form/fields/field_spec.rb +10 -0
- data/spec/html_form/fields/hidden_input_spec.rb +37 -0
- data/spec/html_form/fields/input_spec.rb +17 -0
- data/spec/html_form/fields/radio_button_group_spec.rb +94 -0
- data/spec/html_form/fields/select_spec.rb +62 -0
- data/spec/html_form/fields/tel_input_spec.rb +29 -0
- data/spec/html_form/fields/text_input_spec.rb +29 -0
- data/spec/html_form/fields/textarea_spec.rb +28 -0
- data/spec/html_form_spec.rb +53 -109
- data/spec/spec_helper.rb +5 -10
- data/spec/support/element_helper.rb +15 -0
- data/spec/support/shared_examples_for_pattern.rb +15 -0
- data/spec/support/shared_examples_for_required.rb +35 -0
- metadata +149 -94
- 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
|
-
|
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
|
-
-
|
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.
|
73
|
-
3.
|
74
|
-
4.
|
75
|
-
5.
|
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
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"
|
data/lib/formulary/html_form.rb
CHANGED
@@ -1,58 +1,19 @@
|
|
1
1
|
module Formulary
|
2
2
|
class HtmlForm
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
84
|
-
|
85
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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,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
|