client_side_validations 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/LICENSE +24 -0
- data/README.rdoc +17 -0
- data/Rakefile +47 -0
- data/TODO +2 -0
- data/VERSION +1 -0
- data/client_side_validations.gemspec +79 -0
- data/javascript/jspec/commands/example_command.rb +19 -0
- data/javascript/jspec/rhino.js +10 -0
- data/javascript/jspec/unit/jquery.validate.spec.js +146 -0
- data/javascript/jspec/unit/spec.helper.js +0 -0
- data/javascript/lib/client_side_validations.js +92 -0
- data/javascript/vendor/jspec.js +1889 -0
- data/javascript/vendor/jspec.xhr.js +210 -0
- data/lib/adapters/action_view.rb +40 -0
- data/lib/adapters/active_model.rb +90 -0
- data/lib/adapters/active_record_2.rb +90 -0
- data/lib/client_side_validations.rb +47 -0
- data/spec/action_view_2_spec.rb +74 -0
- data/spec/action_view_3_spec.rb +74 -0
- data/spec/active_model_3_spec.rb +271 -0
- data/spec/active_record_2_spec.rb +286 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/support/required_gems.rb +2 -0
- data/tasks/spec.rake +31 -0
- metadata +122 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
|
2
|
+
// JSpec - XHR - Copyright TJ Holowaychuk <tj@vision-media.ca> (MIT Licensed)
|
3
|
+
|
4
|
+
(function(){
|
5
|
+
|
6
|
+
var lastRequest
|
7
|
+
|
8
|
+
// --- Original XMLHttpRequest
|
9
|
+
|
10
|
+
var OriginalXMLHttpRequest = 'XMLHttpRequest' in this ?
|
11
|
+
XMLHttpRequest :
|
12
|
+
function(){}
|
13
|
+
var OriginalActiveXObject = 'ActiveXObject' in this ?
|
14
|
+
ActiveXObject :
|
15
|
+
undefined
|
16
|
+
|
17
|
+
// --- MockXMLHttpRequest
|
18
|
+
|
19
|
+
var MockXMLHttpRequest = function() {
|
20
|
+
this.requestHeaders = {}
|
21
|
+
}
|
22
|
+
|
23
|
+
MockXMLHttpRequest.prototype = {
|
24
|
+
status: 0,
|
25
|
+
async: true,
|
26
|
+
readyState: 0,
|
27
|
+
responseXML: null,
|
28
|
+
responseText: '',
|
29
|
+
abort: function(){},
|
30
|
+
onreadystatechange: function(){},
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Return response headers hash.
|
34
|
+
*/
|
35
|
+
|
36
|
+
getAllResponseHeaders : function(){
|
37
|
+
return JSpec.inject(this.responseHeaders, '', function(buf, key, val){
|
38
|
+
return buf + key + ': ' + val + '\r\n'
|
39
|
+
})
|
40
|
+
},
|
41
|
+
|
42
|
+
/**
|
43
|
+
* Return case-insensitive value for header _name_.
|
44
|
+
*/
|
45
|
+
|
46
|
+
getResponseHeader : function(name) {
|
47
|
+
return this.responseHeaders[name.toLowerCase()]
|
48
|
+
},
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Set case-insensitive _value_ for header _name_.
|
52
|
+
*/
|
53
|
+
|
54
|
+
setRequestHeader : function(name, value) {
|
55
|
+
this.requestHeaders[name.toLowerCase()] = value
|
56
|
+
},
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Open mock request.
|
60
|
+
*/
|
61
|
+
|
62
|
+
open : function(method, url, async, user, password) {
|
63
|
+
this.user = user
|
64
|
+
this.password = password
|
65
|
+
this.url = url
|
66
|
+
this.readyState = 1
|
67
|
+
this.method = method.toUpperCase()
|
68
|
+
if (async != undefined) this.async = async
|
69
|
+
if (this.async) this.onreadystatechange()
|
70
|
+
},
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Send request _data_.
|
74
|
+
*/
|
75
|
+
|
76
|
+
send : function(data) {
|
77
|
+
var self = this
|
78
|
+
this.data = data
|
79
|
+
this.readyState = 4
|
80
|
+
if (this.method == 'HEAD') this.responseText = null
|
81
|
+
this.responseHeaders['content-length'] = (this.responseText || '').length
|
82
|
+
if(this.async) this.onreadystatechange()
|
83
|
+
this.populateResponseXML()
|
84
|
+
lastRequest = function(){
|
85
|
+
return self
|
86
|
+
}
|
87
|
+
},
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Parse request body and populate responseXML if response-type is xml
|
91
|
+
* Based on the standard specification : http://www.w3.org/TR/XMLHttpRequest/
|
92
|
+
*/
|
93
|
+
populateResponseXML: function() {
|
94
|
+
var type = this.getResponseHeader("content-type")
|
95
|
+
if (!type || !this.responseText || !type.match(/(text\/xml|application\/xml|\+xml$)/g))
|
96
|
+
return
|
97
|
+
this.responseXML = JSpec.parseXML(this.responseText)
|
98
|
+
}
|
99
|
+
}
|
100
|
+
|
101
|
+
// --- Response status codes
|
102
|
+
|
103
|
+
JSpec.statusCodes = {
|
104
|
+
100: 'Continue',
|
105
|
+
101: 'Switching Protocols',
|
106
|
+
200: 'OK',
|
107
|
+
201: 'Created',
|
108
|
+
202: 'Accepted',
|
109
|
+
203: 'Non-Authoritative Information',
|
110
|
+
204: 'No Content',
|
111
|
+
205: 'Reset Content',
|
112
|
+
206: 'Partial Content',
|
113
|
+
300: 'Multiple Choice',
|
114
|
+
301: 'Moved Permanently',
|
115
|
+
302: 'Found',
|
116
|
+
303: 'See Other',
|
117
|
+
304: 'Not Modified',
|
118
|
+
305: 'Use Proxy',
|
119
|
+
307: 'Temporary Redirect',
|
120
|
+
400: 'Bad Request',
|
121
|
+
401: 'Unauthorized',
|
122
|
+
402: 'Payment Required',
|
123
|
+
403: 'Forbidden',
|
124
|
+
404: 'Not Found',
|
125
|
+
405: 'Method Not Allowed',
|
126
|
+
406: 'Not Acceptable',
|
127
|
+
407: 'Proxy Authentication Required',
|
128
|
+
408: 'Request Timeout',
|
129
|
+
409: 'Conflict',
|
130
|
+
410: 'Gone',
|
131
|
+
411: 'Length Required',
|
132
|
+
412: 'Precondition Failed',
|
133
|
+
413: 'Request Entity Too Large',
|
134
|
+
414: 'Request-URI Too Long',
|
135
|
+
415: 'Unsupported Media Type',
|
136
|
+
416: 'Requested Range Not Satisfiable',
|
137
|
+
417: 'Expectation Failed',
|
138
|
+
422: 'Unprocessable Entity',
|
139
|
+
500: 'Internal Server Error',
|
140
|
+
501: 'Not Implemented',
|
141
|
+
502: 'Bad Gateway',
|
142
|
+
503: 'Service Unavailable',
|
143
|
+
504: 'Gateway Timeout',
|
144
|
+
505: 'HTTP Version Not Supported'
|
145
|
+
}
|
146
|
+
|
147
|
+
/**
|
148
|
+
* Mock XMLHttpRequest requests.
|
149
|
+
*
|
150
|
+
* mockRequest().and_return('some data', 'text/plain', 200, { 'X-SomeHeader' : 'somevalue' })
|
151
|
+
*
|
152
|
+
* @return {hash}
|
153
|
+
* @api public
|
154
|
+
*/
|
155
|
+
|
156
|
+
function mockRequest() {
|
157
|
+
return { and_return : function(body, type, status, headers) {
|
158
|
+
XMLHttpRequest = MockXMLHttpRequest
|
159
|
+
ActiveXObject = false
|
160
|
+
status = status || 200
|
161
|
+
headers = headers || {}
|
162
|
+
headers['content-type'] = type
|
163
|
+
JSpec.extend(XMLHttpRequest.prototype, {
|
164
|
+
responseText: body,
|
165
|
+
responseHeaders: headers,
|
166
|
+
status: status,
|
167
|
+
statusText: JSpec.statusCodes[status]
|
168
|
+
})
|
169
|
+
}}
|
170
|
+
}
|
171
|
+
|
172
|
+
/**
|
173
|
+
* Unmock XMLHttpRequest requests.
|
174
|
+
*
|
175
|
+
* @api public
|
176
|
+
*/
|
177
|
+
|
178
|
+
function unmockRequest() {
|
179
|
+
XMLHttpRequest = OriginalXMLHttpRequest
|
180
|
+
ActiveXObject = OriginalActiveXObject
|
181
|
+
}
|
182
|
+
|
183
|
+
JSpec.include({
|
184
|
+
name: 'Mock XHR',
|
185
|
+
|
186
|
+
// --- Utilities
|
187
|
+
|
188
|
+
utilities : {
|
189
|
+
mockRequest: mockRequest,
|
190
|
+
unmockRequest: unmockRequest
|
191
|
+
},
|
192
|
+
|
193
|
+
// --- Hooks
|
194
|
+
|
195
|
+
afterSpec : function() {
|
196
|
+
unmockRequest()
|
197
|
+
},
|
198
|
+
|
199
|
+
// --- DSLs
|
200
|
+
|
201
|
+
DSLs : {
|
202
|
+
snake : {
|
203
|
+
mock_request: mockRequest,
|
204
|
+
unmock_request: unmockRequest,
|
205
|
+
last_request: function(){ return lastRequest() }
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
})
|
210
|
+
})()
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module DNCLabs
|
2
|
+
module ClientSideValidations
|
3
|
+
module Adapters
|
4
|
+
module ActionView
|
5
|
+
module BaseMethods
|
6
|
+
|
7
|
+
def client_side_validations(object_name, options = {})
|
8
|
+
url = options.delete(:url)
|
9
|
+
raise "No URL Specified!" unless url
|
10
|
+
adapter = options.delete(:adapter) || 'jquery.validate'
|
11
|
+
<<-JS
|
12
|
+
<script type="text/javascript">
|
13
|
+
$(document).ready(function() {
|
14
|
+
$('##{dom_id(options[:object])}').clientSideValidations('#{url}', '#{adapter}');
|
15
|
+
})
|
16
|
+
</script>
|
17
|
+
JS
|
18
|
+
end
|
19
|
+
|
20
|
+
end # BaseMethods
|
21
|
+
|
22
|
+
module FormBuilderMethods
|
23
|
+
|
24
|
+
def client_side_validations(options = {})
|
25
|
+
@template.send(:client_side_validations, @object_name, objectify_options(options))
|
26
|
+
end
|
27
|
+
|
28
|
+
end # FormBuilderMethods
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
ActionView::Base.class_eval do
|
35
|
+
include DNCLabs::ClientSideValidations::Adapters::ActionView::BaseMethods
|
36
|
+
end
|
37
|
+
|
38
|
+
ActionView::Helpers::FormBuilder.class_eval do
|
39
|
+
include DNCLabs::ClientSideValidations::Adapters::ActionView::FormBuilderMethods
|
40
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module DNCLabs
|
2
|
+
module ClientSideValidations
|
3
|
+
module Adapters
|
4
|
+
class ActiveModel
|
5
|
+
attr_accessor :base
|
6
|
+
|
7
|
+
def initialize(base)
|
8
|
+
self.base = base
|
9
|
+
end
|
10
|
+
|
11
|
+
def validation_to_hash(_attr, _options = {})
|
12
|
+
validation_hash = {}
|
13
|
+
base._validators[_attr.to_sym].each do |validation|
|
14
|
+
message = get_validation_message(validation, _options[:locale])
|
15
|
+
validation.options.delete(:message)
|
16
|
+
options = get_validation_options(validation.options)
|
17
|
+
method = get_validation_method(validation.kind)
|
18
|
+
if conditional_method = remove_reserved_conditionals(options['if'])
|
19
|
+
if base.instance_eval(conditional_method.to_s)
|
20
|
+
options.delete('if')
|
21
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
22
|
+
end
|
23
|
+
elsif conditional_method = options['unless']
|
24
|
+
unless base.instance_eval(conditional_method.to_s)
|
25
|
+
options.delete('unless')
|
26
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
options.delete('if')
|
30
|
+
options.delete('unless')
|
31
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
validation_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def get_validation_message(validation, locale)
|
41
|
+
default = case validation.kind
|
42
|
+
when :presence
|
43
|
+
I18n.translate('errors.messages.blank', :locale => locale)
|
44
|
+
when :format
|
45
|
+
I18n.translate('errors.messages.invalid', :locale => locale)
|
46
|
+
when :length
|
47
|
+
if count = validation.options[:minimum]
|
48
|
+
I18n.translate('errors.messages.too_short', :locale => locale).sub('{{count}}', count.to_s)
|
49
|
+
elsif count = validation.options[:maximum]
|
50
|
+
I18n.translate('errors.messages.too_long', :locale => locale).sub('{{count}}', count.to_s)
|
51
|
+
end
|
52
|
+
when :numericality
|
53
|
+
I18n.translate('errors.messages.not_a_number', :locale => locale)
|
54
|
+
end
|
55
|
+
|
56
|
+
message = validation.options[:message]
|
57
|
+
if message.kind_of?(String)
|
58
|
+
message
|
59
|
+
elsif message.kind_of?(Symbol)
|
60
|
+
I18n.translate("errors.models.#{base.class.to_s.downcase}.attributes.#{validation.attributes.first}.#{message}", :locale => locale)
|
61
|
+
else
|
62
|
+
default
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_validation_options(options)
|
67
|
+
options = options.stringify_keys
|
68
|
+
options.delete('on')
|
69
|
+
options.delete('tokenizer')
|
70
|
+
options.delete('only_integer')
|
71
|
+
options.delete('allow_nil')
|
72
|
+
if options['with'].kind_of?(Regexp)
|
73
|
+
options['with'] = options['with'].inspect.to_s.sub("\\A","^").sub("\\Z","$").sub(%r{^/},"").sub(%r{/i?$}, "")
|
74
|
+
end
|
75
|
+
options
|
76
|
+
end
|
77
|
+
|
78
|
+
def get_validation_method(kind)
|
79
|
+
kind.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
def remove_reserved_conditionals(*conditionals)
|
83
|
+
conditionals.flatten!
|
84
|
+
conditionals.delete_if { |conditional| conditional =~ /@_/ }
|
85
|
+
conditionals.first
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'validation_reflection'
|
2
|
+
|
3
|
+
module DNCLabs
|
4
|
+
module ClientSideValidations
|
5
|
+
module Adapters
|
6
|
+
class ActiveRecord2
|
7
|
+
attr_accessor :base
|
8
|
+
|
9
|
+
def initialize(base)
|
10
|
+
self.base = base
|
11
|
+
end
|
12
|
+
|
13
|
+
def validation_to_hash(_attr, _options = {})
|
14
|
+
validation_hash = {}
|
15
|
+
base.class.reflect_on_validations_for(_attr).each do |validation|
|
16
|
+
message = get_validation_message(validation, _options[:locale])
|
17
|
+
validation.options.delete(:message)
|
18
|
+
options = get_validation_options(validation.options)
|
19
|
+
method = get_validation_method(validation.macro)
|
20
|
+
if conditional_method = options['if']
|
21
|
+
if base.instance_eval(conditional_method.to_s)
|
22
|
+
options.delete('if')
|
23
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
24
|
+
end
|
25
|
+
elsif conditional_method = options['unless']
|
26
|
+
unless base.instance_eval(conditional_method.to_s)
|
27
|
+
options.delete('unless')
|
28
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
29
|
+
end
|
30
|
+
else
|
31
|
+
validation_hash[method] = { 'message' => message }.merge(options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
validation_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def get_validation_message(validation, locale)
|
41
|
+
default = case validation.macro.to_s
|
42
|
+
when 'validates_presence_of'
|
43
|
+
I18n.translate('activerecord.errors.messages.blank', :locale => locale)
|
44
|
+
when 'validates_format_of'
|
45
|
+
I18n.translate('activerecord.errors.messages.invalid', :locale => locale)
|
46
|
+
when 'validates_length_of'
|
47
|
+
if count = validation.options[:minimum]
|
48
|
+
I18n.translate('activerecord.errors.messages.too_short', :locale => locale).sub('{{count}}', count.to_s)
|
49
|
+
elsif count = validation.options[:maximum]
|
50
|
+
I18n.translate('activerecord.errors.messages.too_long', :locale => locale).sub('{{count}}', count.to_s)
|
51
|
+
end
|
52
|
+
when 'validates_numericality_of'
|
53
|
+
I18n.translate('activerecord.errors.messages.not_a_number', :locale => locale)
|
54
|
+
end
|
55
|
+
|
56
|
+
message = validation.options[:message]
|
57
|
+
if message.kind_of?(String)
|
58
|
+
message
|
59
|
+
elsif message.kind_of?(Symbol)
|
60
|
+
I18n.translate("activerecord.errors.models.#{base.class.to_s.downcase}.attributes.#{validation.name}.#{message}", :locale => locale)
|
61
|
+
else
|
62
|
+
default
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_validation_options(options)
|
67
|
+
options = options.stringify_keys
|
68
|
+
options.delete('on')
|
69
|
+
if options['with'].kind_of?(Regexp)
|
70
|
+
options['with'] = options['with'].inspect.to_s.sub("\\A","^").sub("\\Z","$").sub(%r{^/},"").sub(%r{/i?$}, "")
|
71
|
+
end
|
72
|
+
options
|
73
|
+
end
|
74
|
+
|
75
|
+
def get_validation_method(method)
|
76
|
+
case method.to_s
|
77
|
+
when 'validates_presence_of'
|
78
|
+
'presence'
|
79
|
+
when 'validates_format_of'
|
80
|
+
'format'
|
81
|
+
when 'validates_numericality_of'
|
82
|
+
'numericality'
|
83
|
+
when 'validates_length_of'
|
84
|
+
'length'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems' unless defined?(Gem)
|
2
|
+
|
3
|
+
module DNCLabs
|
4
|
+
module ClientSideValidations
|
5
|
+
def validations_to_json(*attrs)
|
6
|
+
hash = Hash.new { |h, attribute| h[attribute] = {} }
|
7
|
+
attrs.each do |attr|
|
8
|
+
hash[attr].merge!(validation_to_hash(attr))
|
9
|
+
end
|
10
|
+
hash.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def validation_to_hash(_attr, _options = {})
|
14
|
+
@dnc_csv_adapter ||= Adapter.new(self)
|
15
|
+
@dnc_csv_adapter.validation_to_hash(_attr, _options)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
# ORM
|
22
|
+
|
23
|
+
if defined?(ActiveModel)
|
24
|
+
require 'adapters/active_model'
|
25
|
+
unless Object.respond_to?(:to_json)
|
26
|
+
require 'active_support/json/encoding'
|
27
|
+
end
|
28
|
+
DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveModel
|
29
|
+
klass = ActiveModel::Validations
|
30
|
+
elsif defined?(ActiveRecord)
|
31
|
+
if ActiveRecord::VERSION::MAJOR == 2
|
32
|
+
require 'adapters/active_record_2'
|
33
|
+
DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveRecord2
|
34
|
+
klass = ActiveRecord::Base
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
klass.class_eval do
|
39
|
+
include DNCLabs::ClientSideValidations
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
# Template
|
44
|
+
|
45
|
+
if defined?(ActionView)
|
46
|
+
require 'adapters/action_view'
|
47
|
+
end
|