hash_attributes 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +26 -0
  4. data/Rakefile +20 -0
  5. data/lib/core_ext/active_record_relation_extend.rb +23 -0
  6. data/lib/hash_attributes.rb +4 -0
  7. data/lib/hash_attributes/date_time_serializer.rb +30 -0
  8. data/lib/hash_attributes/hash_attributes.rb +357 -0
  9. data/lib/hash_attributes/version.rb +3 -0
  10. data/spec/concerns/hash_attributes_spec.rb +645 -0
  11. data/spec/dummy/Rakefile +6 -0
  12. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  13. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  14. data/spec/dummy/app/assets/stylesheets/scaffold.css +56 -0
  15. data/spec/dummy/app/controllers/application_controller.rb +5 -0
  16. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  17. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  18. data/spec/dummy/bin/bundle +3 -0
  19. data/spec/dummy/bin/rails +4 -0
  20. data/spec/dummy/bin/rake +4 -0
  21. data/spec/dummy/config.ru +4 -0
  22. data/spec/dummy/config/application.rb +30 -0
  23. data/spec/dummy/config/boot.rb +5 -0
  24. data/spec/dummy/config/database.yml +25 -0
  25. data/spec/dummy/config/environment.rb +5 -0
  26. data/spec/dummy/config/environments/development.rb +29 -0
  27. data/spec/dummy/config/environments/production.rb +80 -0
  28. data/spec/dummy/config/environments/test.rb +36 -0
  29. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  30. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  31. data/spec/dummy/config/initializers/inflections.rb +16 -0
  32. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  33. data/spec/dummy/config/initializers/secret_token.rb +12 -0
  34. data/spec/dummy/config/initializers/session_store.rb +3 -0
  35. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  36. data/spec/dummy/config/locales/en.yml +23 -0
  37. data/spec/dummy/config/routes.rb +56 -0
  38. data/spec/dummy/db/test.sqlite3 +0 -0
  39. data/spec/dummy/public/404.html +58 -0
  40. data/spec/dummy/public/422.html +58 -0
  41. data/spec/dummy/public/500.html +57 -0
  42. data/spec/dummy/public/favicon.ico +0 -0
  43. data/spec/spec_helper.rb +52 -0
  44. metadata +145 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 55c46aa2869d991f5505c977e94fc3337ff7f517
4
+ data.tar.gz: 7514a99ac4c19ee09cfdaadcb979c9409b5f52cc
5
+ SHA512:
6
+ metadata.gz: 4a96d03a494104cb0908b57105d0f2d98f74d4c8f2851ad140055bf6de648165a860e77eaf391b5963df03939e22ddaf47f8f936919e6a5e3f82ca83f2fa18b3
7
+ data.tar.gz: d62ee925d689c46acf34fc8b13593a319cb40cce4f9fb2effc3011098552e705dc017ffa3c3b4be285dff66f8ab3af0acd2e01c6a5c0fbbf7a01f49b137ff5f7
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Webnuts (www.webnuts.com / hello@webnuts.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # Welcome to HashAttributes
2
+
3
+ ## The future
4
+
5
+ We would love to hear new ideas or random thoughts about HashAttributes.
6
+
7
+ ## Requirements
8
+
9
+ - ActiveRecord
10
+
11
+ ## License
12
+
13
+ HashAttributes is released under the MIT License. See the MIT-LICENSE file.
14
+
15
+ ## Want to contribute?
16
+
17
+ That's awesome, thank you!
18
+
19
+ Do you have an idea or suggestion? Please create an issue or send us an e-mail (hello@webnuts.com). We would be happy to implement right away.
20
+
21
+ You can also send us a pull request with your contribution.
22
+
23
+ <a href="http://www.webnuts.com" target="_blank">
24
+ <img src="http://www.webnuts.com/logo/hash_attributes/logo.png" alt="Webnuts.com">
25
+ </a>
26
+ ##### Sponsored by Webnuts.com
@@ -0,0 +1,20 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'HashAttributes'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,23 @@
1
+ ActiveRecord::Relation.class_eval do
2
+ def update_all_with_hash_column(attributes)
3
+ update_all_without_hash_column(attributes) unless attributes.is_a?(Hash) && attributes.present?
4
+
5
+ attributes = attributes.with_indifferent_access
6
+ hash_column_attributes = attributes.except(*model.column_names)
7
+ if hash_column_attributes.present?
8
+ hash_column_attributes = model.serialize_hash_column_attribute(model.hash_column, hash_column_attributes)
9
+ attributes = attributes.slice(*model.column_names).merge({model.hash_column => hash_column_attributes})
10
+ end
11
+
12
+ if attributes.has_key?(model.hash_column)
13
+ attribute_serializer = model.serialized_attributes[model.hash_column]
14
+ if attribute_serializer
15
+ attributes[model.hash_column] = attribute_serializer.dump(attributes[model.hash_column])
16
+ end
17
+ end
18
+
19
+ update_all_without_hash_column(attributes)
20
+ end
21
+
22
+ alias_method_chain :update_all, :hash_column
23
+ end
@@ -0,0 +1,4 @@
1
+ require 'core_ext/active_record_relation_extend'
2
+ require 'hash_attributes/hash_attributes'
3
+ require 'hash_attributes/date_time_serializer'
4
+ require 'hash_attributes/version'
@@ -0,0 +1,30 @@
1
+ module HashAttributes
2
+ class DateTimeSerializer
3
+ def self.supported_classes
4
+ [Time, DateTime, Date]
5
+ end
6
+
7
+ def self.is_serializable?(object)
8
+ supported_classes.include?(object.class)
9
+ end
10
+
11
+ def self.is_deserializable?(value)
12
+ if value.is_a?(String)
13
+ /^[0-9]{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\.[0-9]{3}Z$/.match(value) != nil
14
+ else
15
+ false
16
+ end
17
+ end
18
+
19
+ def initialize(options)
20
+ end
21
+
22
+ def load(value_str)
23
+ DateTime.parse(value_str)
24
+ end
25
+
26
+ def dump(value)
27
+ value.in_time_zone.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,357 @@
1
+ module HashAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ self.register_value_serializer("HashAttributes::DateTimeSerializer")
6
+ end
7
+
8
+ def initialize(*args, &block)
9
+ if args[0].is_a?(Hash)
10
+ args[0] = args[0].with_indifferent_access
11
+ args[0] = args[0].inject(HashWithIndifferentAccess.new(self.class.hash_column => {})) do |result, (attribute_name, value)|
12
+ if self.class.hash_column == attribute_name
13
+ result[self.class.hash_column].merge!(value)
14
+ elsif self.class.column_names.include?(attribute_name)
15
+ result[attribute_name] = value
16
+ else
17
+ result[self.class.hash_column][attribute_name] = value
18
+ end
19
+ result
20
+ end
21
+ end
22
+
23
+ if block_given?
24
+ super(*args) do |new_record|
25
+ yield new_record if block_given?
26
+ end
27
+ else
28
+ super(*args)
29
+ end
30
+ end
31
+
32
+ def is_valid_hash_column_attribute_name?(attribute_name)
33
+ attribute_name = attribute_name.to_s
34
+ self.class.column_names.include?(attribute_name) == false && self.class.is_valid_attribute_name?(attribute_name)
35
+ end
36
+
37
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_methods/read.rb
38
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Read.html#method-i-read_attribute
39
+ # prefix: "", suffix: ""
40
+ def read_attribute(attribute_name)
41
+ attribute_name = attribute_name.to_s
42
+ if attribute_name == self.class.hash_column
43
+ (super(attribute_name) || {}).with_indifferent_access
44
+ elsif is_valid_hash_column_attribute_name?(attribute_name)
45
+ read_hash_column_attribute(attribute_name)
46
+ else
47
+ super(attribute_name)
48
+ end
49
+ end
50
+
51
+ def read_hash_column_attribute(attribute_name)
52
+ self.class.deserialize_hash_column_attribute(attribute_name, read_attribute(self.class.hash_column).try(:[], attribute_name))
53
+ end
54
+
55
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_methods/write.rb
56
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Write.html
57
+ # prefix: "", suffix: "="
58
+ def write_attribute(attribute_name, value)
59
+ attribute_name = attribute_name.to_s
60
+ if attribute_name == self.class.hash_column
61
+ value = value.try(:with_indifferent_access)
62
+ unless read_attribute(self.class.hash_column).try(:with_indifferent_access) == value
63
+ attribute_will_change!(attribute_name)
64
+ end
65
+ super(self.class.hash_column, self.class.serialize_hash_column_attribute(self.class.hash_column, value))
66
+ value
67
+ elsif is_valid_hash_column_attribute_name?(attribute_name)
68
+ write_hash_column_attribute(attribute_name, value)
69
+ else
70
+ super(attribute_name, value)
71
+ end
72
+ end
73
+
74
+ def write_hash_column_attribute(attribute_name, value)
75
+ if value != read_attribute(attribute_name)
76
+ verify_readonly_attribute(attribute_name)
77
+ attribute_will_change!(attribute_name)
78
+ write_attribute(self.class.hash_column, read_attribute(self.class.hash_column).merge(attribute_name => value))
79
+ end
80
+ value
81
+ end
82
+
83
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
84
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
85
+ # prefix: "", suffix: "_before_type_cast"
86
+ def read_attribute_before_type_cast(attribute_name)
87
+ if is_valid_hash_column_attribute_name?(attribute_name)
88
+ read_hash_column_attribute(attribute_name)
89
+ else
90
+ super(attribute_name)
91
+ end
92
+ end
93
+
94
+ def attributes_before_type_cast
95
+ self.class.column_names.inject(HashWithIndifferentAccess.new) do |result, attribute_name|
96
+ if attribute_name == self.class.hash_column
97
+ result.merge(read_attribute(attribute_name) || {})
98
+ else
99
+ result.merge(attribute_name => read_attribute(attribute_name))
100
+ end
101
+ end
102
+ end
103
+
104
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_methods/query.rb
105
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Query.html
106
+ # prefix: "", suffix: "?"
107
+ def query_attribute(attribute_name)
108
+ is_valid_hash_column_attribute_name?(attribute_name) || super(attribute_name)
109
+ end
110
+
111
+ # # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods.html
112
+ def [](attribute_name)
113
+ read_attribute(attribute_name)
114
+ end
115
+
116
+ def []=(attribute_name, value)
117
+ write_attribute(attribute_name, value)
118
+ end
119
+
120
+ def hash_column_attributes
121
+ self.class.deserialize_hash_column_attribute(self.class.hash_column, read_attribute(self.class.hash_column))
122
+ end
123
+
124
+ def hash_column_attribute_names
125
+ read_attribute(self.class.hash_column).keys.sort
126
+ end
127
+
128
+ def attribute_names
129
+ ((self.class.column_names - [self.class.hash_column]) + hash_column_attribute_names).sort
130
+ end
131
+
132
+ def attributes
133
+ (super || {}).with_indifferent_access.except(self.class.hash_column).merge(hash_column_attributes)
134
+ end
135
+
136
+ def column_for_attribute(attribute_name)
137
+ attribute_name = attribute_name.to_s
138
+ if is_valid_hash_column_attribute_name?(attribute_name)
139
+ nil
140
+ else
141
+ super(attribute_name)
142
+ end
143
+ end
144
+
145
+ def has_attribute?(attribute_name)
146
+ attribute_name = attribute_name.to_s
147
+ hash_column_attribute_names.include?(attribute_name) || super(attribute_name)
148
+ end
149
+
150
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeAssignment.html
151
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_assignment.rb
152
+ def assign_attributes(new_attributes)
153
+ return if new_attributes.blank?
154
+ new_attributes = new_attributes.with_indifferent_access
155
+ __hash_column_attributes__ = new_attributes.except(*self.class.column_names)
156
+ if __hash_column_attributes__.present?
157
+ __hash_column_attributes__ = {self.class.hash_column => read_attribute(self.class.hash_column).merge(__hash_column_attributes__)}
158
+ new_attributes = new_attributes.slice(*self.class.column_names).merge(__hash_column_attributes__)
159
+ end
160
+ super(new_attributes)
161
+ end
162
+
163
+ def to_h
164
+ serializable_hash.with_indifferent_access
165
+ end
166
+
167
+ # http://api.rubyonrails.org/classes/ActiveRecord/Integration.html
168
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/integration.rb
169
+ def cache_key
170
+ "#{self.class.name.underscore.dasherize}-#{read_attribute(self.class.primary_key)}-version-#{Digest::MD5.hexdigest(attributes.inspect)}"
171
+ end
172
+
173
+ def inspect
174
+ "#<#{self.class.name} #{attributes.map{ |k, v| "#{k}: #{v.inspect}" }.join(", ")}>"
175
+ end
176
+
177
+ def delete_hash_column_attribute(attribute_name)
178
+ if is_valid_hash_column_attribute_name?(attribute_name)
179
+ __hash_column_value__ = read_attribute(self.class.hash_column)
180
+ __result__ = __hash_column_value__.delete(attribute_name)
181
+ write_attribute(self.class.hash_column, __hash_column_value__)
182
+ self.class.undefine_attribute_method(attribute_name)
183
+ __result__
184
+ else
185
+ nil
186
+ end
187
+ end
188
+
189
+ def update_columns(attributes)
190
+ __hash_column_attributes__ = attributes.except(*self.class.column_names)
191
+ if __hash_column_attributes__.present?
192
+ __hash_column_attributes__ = self.class.serialize_hash_column_attribute(self.class.hash_column, __hash_column_attributes__)
193
+ attributes = attributes.slice(*self.class.column_names).merge({self.class.hash_column => __hash_column_attributes__})
194
+ end
195
+ super(attributes)
196
+ end
197
+
198
+ def respond_to?(method_symbol, include_private = false)
199
+ if super
200
+ true
201
+ else
202
+ attribute_name = self.class.extract_attribute_name(method_symbol)
203
+ attribute_name.present? && hash_column_attribute_names.include?(attribute_name)
204
+ end
205
+ end
206
+
207
+ def method_missing(method_symbol, *args, &block)
208
+ attribute_name = self.class.extract_attribute_name(method_symbol)
209
+ if attribute_name.present? &&
210
+ self.class.column_names.include?(attribute_name) == false &&
211
+ self.class.method_defined?(attribute_name) == false
212
+
213
+ self.class.define_hash_column_attribute(attribute_name)
214
+ __send__(method_symbol, *args)
215
+ else
216
+ super
217
+ end
218
+ end
219
+
220
+ # def attribute_was(attribute_name)
221
+ # attribute_name = attribute_name.to_s
222
+ # if is_valid_hash_column_attribute_name?(attribute_name)
223
+ # __hash_column__ = changed_attributes[self.class.hash_column]
224
+ # if __hash_column__.present?
225
+ # __hash_column__[attribute_name]
226
+ # else
227
+ # __send__(attribute_name)
228
+ # end
229
+ # else
230
+ # super(attribute_name)
231
+ # end
232
+ # end
233
+
234
+
235
+
236
+ module ClassMethods
237
+ def hash_column
238
+ @hash_column ||= "__hash_column"
239
+ end
240
+
241
+ def hash_column=(column_name)
242
+ raise ArgumentError, "column_name must be present" unless column_name.present?
243
+ @hash_column = column_name.to_s
244
+ end
245
+
246
+ def extract_attribute_name(candidate_name)
247
+ candidate_name = candidate_name.to_s
248
+ return unless candidate_name.present?
249
+ found_attribute_method_matchers = attribute_method_matchers.select{|m| m.plain? == false}.map{|m| m.match(candidate_name)}.compact
250
+ attribute_method_match = found_attribute_method_matchers.sort_by{|m| m.target.length * -1}.first # make sure 'attribute_changed?' is selected before 'attribute?'
251
+ attribute_name = attribute_method_match.try(:attr_name) || candidate_name
252
+ return if attribute_name.to_sym.inspect.start_with?(':"')
253
+ attribute_name
254
+ end
255
+
256
+ def is_valid_attribute_name?(attribute_name)
257
+ attribute_name.to_s == extract_attribute_name(attribute_name)
258
+ end
259
+
260
+ def define_hash_column_attribute(name)
261
+ attribute_name = extract_attribute_name(name)
262
+ raise ArgumentError, "'#{name}'' is not a valid attribute name" unless attribute_name.present?
263
+ unless method_defined?(attribute_name) || method_defined?("#{attribute_name}=")
264
+ class_eval <<-RUBY
265
+ def #{attribute_name}
266
+ read_attribute('#{attribute_name}')
267
+ end
268
+
269
+ def #{attribute_name}=(value)
270
+ # if value != read_attribute('#{attribute_name}')
271
+ # reset_attribute!('#{attribute_name}')
272
+ # attribute_will_change!('#{attribute_name}')
273
+ # end
274
+ write_attribute('#{attribute_name}', value)
275
+ end
276
+ RUBY
277
+ end
278
+
279
+ define_attribute_method attribute_name
280
+ end
281
+
282
+ def undefine_attribute_method(attribute_name)
283
+ attribute_name = attribute_name.to_s
284
+ generated_attribute_methods.synchronize do
285
+ attribute_method_matchers.each do |matcher|
286
+ method_name = matcher.method_name(attribute_name)
287
+ undef_method(method_name) if method_defined?(method_name)
288
+ end
289
+ attribute_method_matchers_cache.clear
290
+ end
291
+ end
292
+
293
+ # http://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/TimeZoneConversion/ClassMethods.html
294
+ # https://github.com/rails/rails/blob/4-0-stable/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
295
+ # Prevent error, since hash column attribute have no corresponding column object
296
+ def create_time_zone_conversion_attribute?(name, column)
297
+ if column # make sure column is not nil, since 'super' will check if column type is :datetime or :timestamp
298
+ super(name, column)
299
+ else
300
+ false
301
+ end
302
+ end
303
+
304
+ def value_serializers
305
+ @value_serializers ||= []
306
+ end
307
+
308
+ def serialize_hash_column_attribute(attribute_name, value)
309
+ serializer = value_serializers.select{|s| s.is_serializable?(value)}.first
310
+
311
+ if serializer.present?
312
+ serializer.new({}).dump(value)
313
+ elsif value.is_a?(Hash)
314
+ value.inject(HashWithIndifferentAccess.new) do |result, (key, value)|
315
+ result[key] = serialize_hash_column_attribute("#{attribute_name}.#{key}", value)
316
+ result
317
+ end
318
+ elsif value.is_a?(Array)
319
+ value.map.with_index do |array_value, index|
320
+ serialize_hash_column_attribute("#{attribute_name}[#{index}]", array_value)
321
+ end
322
+ else
323
+ value
324
+ end
325
+ end
326
+
327
+ def deserialize_hash_column_attribute(attribute_name, value)
328
+ deserializer = value_serializers.select{|s| s.is_deserializable?(value)}.first
329
+
330
+ if deserializer.present?
331
+ deserializer.new({}).load(value)
332
+ elsif value.is_a?(Hash)
333
+ value.inject(HashWithIndifferentAccess.new) do |result, (key, value)|
334
+ result[key] = deserialize_hash_column_attribute("#{attribute_name}.#{key}", value)
335
+ result
336
+ end
337
+ elsif value.is_a?(Array)
338
+ value.map.with_index do |array_value, index|
339
+ deserialize_hash_column_attribute("#{attribute_name}[#{index}]", array_value)
340
+ end
341
+ else
342
+ value
343
+ end
344
+ end
345
+
346
+ def register_value_serializer(serializer_class)
347
+ serializer_class = serializer_class.constantize if serializer_class.is_a?(String)
348
+ deregister_value_serializer(serializer_class)
349
+ value_serializers.insert(0, serializer_class)
350
+ end
351
+
352
+ def deregister_value_serializer(serializer_class)
353
+ serializer_class = serializer_class.constantize if serializer_class.is_a?(String)
354
+ value_serializers.delete(serializer_class)
355
+ end
356
+ end
357
+ end