client_side_validations 0.6.0

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