formulary 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|