hash_attributes 1.0.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.
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