mobility 0.8.8 → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +56 -0
  5. data/Gemfile +52 -16
  6. data/Gemfile.lock +113 -52
  7. data/Guardfile +23 -1
  8. data/README.md +184 -92
  9. data/Rakefile +6 -4
  10. data/lib/mobility.rb +40 -166
  11. data/lib/mobility/active_record/translation.rb +1 -1
  12. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  13. data/lib/mobility/backend.rb +19 -41
  14. data/lib/mobility/backends.rb +20 -0
  15. data/lib/mobility/backends/active_record.rb +4 -0
  16. data/lib/mobility/backends/active_record/column.rb +2 -0
  17. data/lib/mobility/backends/active_record/container.rb +4 -2
  18. data/lib/mobility/backends/active_record/hstore.rb +2 -0
  19. data/lib/mobility/backends/active_record/json.rb +2 -0
  20. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  21. data/lib/mobility/backends/active_record/key_value.rb +5 -3
  22. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  23. data/lib/mobility/backends/active_record/serialized.rb +2 -0
  24. data/lib/mobility/backends/active_record/table.rb +5 -3
  25. data/lib/mobility/backends/column.rb +0 -6
  26. data/lib/mobility/backends/container.rb +2 -1
  27. data/lib/mobility/backends/hash.rb +39 -0
  28. data/lib/mobility/backends/hstore.rb +0 -1
  29. data/lib/mobility/backends/json.rb +0 -1
  30. data/lib/mobility/backends/jsonb.rb +0 -1
  31. data/lib/mobility/backends/key_value.rb +22 -14
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +3 -0
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +3 -1
  36. data/lib/mobility/backends/sequel/hstore.rb +2 -0
  37. data/lib/mobility/backends/sequel/json.rb +2 -0
  38. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  39. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  40. data/lib/mobility/backends/sequel/serialized.rb +2 -0
  41. data/lib/mobility/backends/sequel/table.rb +5 -2
  42. data/lib/mobility/backends/serialized.rb +1 -3
  43. data/lib/mobility/backends/table.rb +14 -6
  44. data/lib/mobility/pluggable.rb +36 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +26 -25
  47. data/lib/mobility/plugins/active_model.rb +17 -0
  48. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  49. data/lib/mobility/plugins/active_model/dirty.rb +310 -54
  50. data/lib/mobility/plugins/active_record.rb +34 -0
  51. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  52. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  53. data/lib/mobility/plugins/active_record/dirty.rb +72 -101
  54. data/lib/mobility/plugins/active_record/query.rb +48 -34
  55. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  56. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  57. data/lib/mobility/plugins/attributes.rb +70 -0
  58. data/lib/mobility/plugins/backend.rb +138 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +59 -24
  61. data/lib/mobility/plugins/default.rb +22 -17
  62. data/lib/mobility/plugins/dirty.rb +12 -33
  63. data/lib/mobility/plugins/fallbacks.rb +51 -43
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +26 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +25 -35
  66. data/lib/mobility/plugins/presence.rb +28 -21
  67. data/lib/mobility/plugins/query.rb +8 -17
  68. data/lib/mobility/plugins/reader.rb +50 -0
  69. data/lib/mobility/plugins/sequel.rb +34 -0
  70. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  71. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  72. data/lib/mobility/plugins/sequel/dirty.rb +45 -32
  73. data/lib/mobility/plugins/sequel/query.rb +21 -6
  74. data/lib/mobility/plugins/writer.rb +44 -0
  75. data/lib/mobility/translations.rb +95 -0
  76. data/lib/mobility/version.rb +12 -1
  77. data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
  78. metadata +51 -51
  79. metadata.gz.sig +0 -0
  80. data/lib/mobility/active_model.rb +0 -4
  81. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  82. data/lib/mobility/active_record.rb +0 -23
  83. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  85. data/lib/mobility/attributes.rb +0 -324
  86. data/lib/mobility/backend/orm_delegator.rb +0 -44
  87. data/lib/mobility/backend_resetter.rb +0 -50
  88. data/lib/mobility/configuration.rb +0 -138
  89. data/lib/mobility/fallbacks.rb +0 -28
  90. data/lib/mobility/interface.rb +0 -0
  91. data/lib/mobility/loaded.rb +0 -4
  92. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  93. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  94. data/lib/mobility/sequel.rb +0 -9
  95. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  96. data/lib/mobility/translates.rb +0 -73
@@ -1,6 +1,23 @@
1
+ require_relative "./active_model/dirty"
2
+ require_relative "./active_model/cache"
3
+
1
4
  module Mobility
2
5
  module Plugins
6
+ =begin
7
+
8
+ Plugin for ActiveModel models. In practice, this is simply a wrapper to include
9
+ a few plugins which apply to models which include ActiveModel::Dirty but are
10
+ not ActiveRecord models.
11
+
12
+ =end
3
13
  module ActiveModel
14
+ extend Plugin
15
+
16
+ requires :active_model_dirty
17
+ requires :active_model_cache
18
+ requires :backend, include: :before
4
19
  end
20
+
21
+ register_plugin(:active_model, ActiveModel)
5
22
  end
6
23
  end
@@ -0,0 +1,26 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ module ActiveModel
6
+ =begin
7
+
8
+ Adds hooks to clear Mobility cache when AM dirty reset methods are called.
9
+
10
+ =end
11
+ module Cache
12
+ extend Plugin
13
+
14
+ requires :cache, include: false
15
+
16
+ included_hook do |klass, _|
17
+ if options[:cache]
18
+ define_cache_hooks(klass, :changes_applied, :clear_changes_information)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ register_plugin(:active_model_cache, ActiveModel::Cache)
25
+ end
26
+ end
@@ -17,83 +17,339 @@ following methods:
17
17
  - +title_previous_change+
18
18
  - +restore_title!+
19
19
 
20
- In addition, the private method +restore_attribute!+ will also restore the
21
- value of the translated attribute if passed to it.
20
+ The following methods are also patched to work with translated attributes:
21
+ - +changed_attributes+
22
+ - +changes+
23
+ - +changed+
24
+ - +changed?+
25
+ - +previous_changes+
26
+ - +clear_attribute_changes+
27
+ - +restore_attributes+
28
+
29
+ In addition, the following ActiveModel attribute handler methods are also
30
+ patched to work with translated attributes:
31
+ - +attribute_changed?+
32
+ - +attribute_previously_changed?+
33
+ - +attribute_was+
34
+
35
+ (When using these methods, you must pass the attribute name along with its
36
+ locale suffix, so +title_en+, +title_pt_br+, etc.)
37
+
38
+ Other methods are also included for ActiveRecord models, see documentation on
39
+ the ActiveRecord dirty plugin for more information.
22
40
 
23
41
  @see http://api.rubyonrails.org/classes/ActiveModel/Dirty.html Rails documentation for Active Model Dirty module
24
42
 
25
43
  =end
26
44
  module Dirty
27
- # @!group Backend Accessors
28
- # @!macro backend_writer
29
- # @param [Hash] options
30
- def write(locale, value, options = {})
31
- locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
32
- if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
33
- model.send(:attributes_changed_by_setter).except!(locale_accessor)
34
- elsif read(locale, options.merge(locale: true)) != value
35
- model.send(:mobility_changed_attributes) << locale_accessor
36
- model.send(:attribute_will_change!, locale_accessor)
37
- end
38
- super
45
+ extend Plugin
46
+
47
+ requires :dirty, include: false
48
+
49
+ initialize_hook do
50
+ if options[:dirty]
51
+ define_dirty_methods(names)
52
+ include dirty_handler_methods
53
+ end
39
54
  end
40
- # @!endgroup
41
-
42
- # Builds module which adds suffix/prefix methods for translated
43
- # attributes so they act like normal dirty-tracked attributes.
44
- class MethodsBuilder < Module
45
- def initialize(*attribute_names)
46
- attribute_names.each do |name|
47
- method_suffixes.each do |suffix|
48
- define_method "#{name}#{suffix}" do
49
- __send__("attribute#{suffix}", Mobility.normalize_locale_accessor(name))
50
- end
55
+
56
+ included_hook do |klass, backend_class|
57
+ raise TypeError, "#{name} should include ActiveModel::Dirty to use the active_model plugin" unless active_model_dirty_class?(klass)
58
+
59
+ if options[:dirty]
60
+ private_methods = InstanceMethods.instance_methods & klass.private_instance_methods
61
+ klass.include InstanceMethods
62
+ klass.class_eval { private(*private_methods) }
63
+
64
+ backend_class.include BackendMethods
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ # Overridden in AR::Dirty plugin to define a different HandlerMethods module
71
+ def dirty_handler_methods
72
+ HandlerMethods
73
+ end
74
+
75
+ def active_model_dirty_class?(klass)
76
+ klass.ancestors.include?(::ActiveModel::Dirty)
77
+ end
78
+
79
+ def define_dirty_methods(attribute_names)
80
+ attribute_names.each do |name|
81
+ dirty_handler_methods.each_pattern(name) do |method_name, attribute_method|
82
+ define_method(method_name) do |*args|
83
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args)
51
84
  end
85
+ end
86
+
87
+ define_method "restore_#{name}!" do
88
+ locale_accessor = Dirty.append_locale(name)
89
+ if mutations_from_mobility.attribute_changed?(locale_accessor)
90
+ __send__("#{name}=", mutations_from_mobility.attribute_was(locale_accessor))
91
+ mutations_from_mobility.restore_attribute!(locale_accessor)
92
+ end
93
+ end
94
+ end
95
+
96
+ # This private method override is necessary to make
97
+ # +restore_attributes+ (which is public) work with translated
98
+ # attributes.
99
+ define_method :restore_attribute! do |attr|
100
+ attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
101
+ end
102
+ private :restore_attribute!
103
+ end
104
+
105
+ def self.append_locale(attr_name)
106
+ Mobility.normalize_locale_accessor(attr_name)
107
+ end
108
+
109
+ # Module builder which mimics dirty method handlers on a given dirty class.
110
+ # Used to mimic ActiveModel::Dirty and ActiveRecord::Dirty, which have
111
+ # similar but slightly different sets of handler methods. Doing it this
112
+ # way with introspection allows us to support basically all AR/AM
113
+ # versions without changes here.
114
+ class HandlerMethodsBuilder < Module
115
+ attr_reader :klass
52
116
 
53
- define_method "restore_#{name}!" do
54
- locale_accessor = Mobility.normalize_locale_accessor(name)
55
- if attribute_changed?(locale_accessor)
56
- __send__("#{name}=", changed_attributes[locale_accessor])
117
+ # @param [Class] klass Dirty class to mimic
118
+ def initialize(klass)
119
+ @klass = klass
120
+ define_handler_methods
121
+ end
122
+
123
+ def each_pattern(attr_name)
124
+ patterns.each do |pattern|
125
+ yield pattern % attr_name, pattern % 'attribute'
126
+ end
127
+ end
128
+
129
+ def define_handler_methods
130
+ public_patterns.each do |pattern|
131
+ method_name = pattern % 'attribute'
132
+
133
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
134
+ def #{method_name}(attr_name, *rest)
135
+ if (mutations_from_mobility.attribute_changed?(attr_name) ||
136
+ mutations_from_mobility.attribute_previously_changed?(attr_name))
137
+ mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest)
138
+ else
139
+ super
57
140
  end
58
141
  end
142
+ EOM
59
143
  end
144
+ end
60
145
 
61
- define_method :restore_attribute! do |attr|
62
- attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
146
+ # Get method suffixes. Creating an object just to get the list of
147
+ # suffixes is simplest given they change from Rails version to version.
148
+ def patterns
149
+ @patterns ||=
150
+ (klass.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_patterns)
151
+ end
152
+
153
+ private
154
+
155
+ def public_patterns
156
+ @public_patterns ||= patterns.select do |p|
157
+ klass.public_method_defined?(p % 'attribute')
63
158
  end
64
- private :restore_attribute!
65
159
  end
66
160
 
67
- def included(model_class)
68
- model_class.include ChangedAttributes
161
+ def excluded_patterns
162
+ ['%s', 'restore_%s!']
163
+ end
164
+ end
165
+
166
+ # Module which defines generic handler methods like
167
+ # +attribute_changed?+ that are patched to work with translated
168
+ # attributes.
169
+ HandlerMethods = HandlerMethodsBuilder.new(Class.new { include ::ActiveModel::Dirty })
170
+
171
+ module InstanceMethods
172
+ def changed_attributes
173
+ super.merge(mutations_from_mobility.changed_attributes)
174
+ end
175
+
176
+ def changes_applied
177
+ super
178
+ mutations_from_mobility.finalize_changes
179
+ end
180
+
181
+ def changes
182
+ super.merge(mutations_from_mobility.changes)
183
+ end
184
+
185
+ def changed
186
+ # uniq is required for Rails < 6.0
187
+ (super + mutations_from_mobility.changed).uniq
188
+ end
189
+
190
+ def changed?
191
+ super || mutations_from_mobility.changed?
192
+ end
193
+
194
+ def previous_changes
195
+ super.merge(mutations_from_mobility.previous_changes)
196
+ end
197
+
198
+ def clear_changes_information
199
+ @mutations_from_mobility = nil
200
+ super
201
+ end
202
+
203
+ def clear_attribute_changes(attr_names)
204
+ attr_names.each { |attr_name| mutations_from_mobility.restore_attribute!(attr_name) }
205
+ super
69
206
  end
70
207
 
71
208
  private
72
209
 
73
- # Get method suffixes. Creating an object just to get the list of
74
- # suffixes is not very efficient, but the most reliable way given that
75
- # they change from Rails version to version.
76
- def method_suffixes
77
- @method_suffixes ||=
78
- Class.new do
79
- include ::ActiveModel::Dirty
80
- end.attribute_method_matchers.map(&:suffix).select { |m| m =~ /\A_/ }
81
- end
82
-
83
- # Tracks which translated attributes have been changed, separate from
84
- # the default tracking of changes in ActiveModel/ActiveRecord Dirty.
85
- # This is required in order for the Mobility ActiveRecord Dirty
86
- # plugin to correctly read the value of locale accessors like
87
- # +title_en+ in dirty tracking.
88
- module ChangedAttributes
89
- private
90
-
91
- def mobility_changed_attributes
92
- @mobility_changed_attributes ||= Set.new
210
+ def mutations_from_mobility
211
+ @mutations_from_mobility ||= MobilityMutationTracker.new(self)
212
+ end
213
+ end
214
+
215
+ # @note Seriously, I really don't want to reproduce all of
216
+ # ActiveModel::Dirty here, but having fought with upstream changes
217
+ # many many times I finally decided it's more future-proof to just
218
+ # re-implement the stuff we need here, to avoid weird breakage.
219
+ #
220
+ # Although this is somewhat ugly, at least it's explicit and since
221
+ # it's self-defined (rather than hooking into fickle private methods
222
+ # in Rails), it won't break all of a sudden. We just need to ensure
223
+ # that specs are up-to-date with the latest weird dirty method
224
+ # pattern Rails has decided to support.
225
+ class MobilityMutationTracker
226
+ OPTION_NOT_GIVEN = Object.new
227
+
228
+ attr_reader :previous_changes
229
+
230
+ def initialize(model)
231
+ @model = model
232
+ @current_changes = {}.with_indifferent_access
233
+ @previous_changes = {}.with_indifferent_access
234
+ end
235
+
236
+ def finalize_changes
237
+ @previous_changes = changes
238
+ @current_changes = {}.with_indifferent_access
239
+ end
240
+
241
+ def changed
242
+ attr_names.select { |attr_name| attribute_changed?(attr_name) }
243
+ end
244
+
245
+ def changed_attributes
246
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
247
+ if attribute_changed?(attr_name)
248
+ result[attr_name] = attribute_was(attr_name)
249
+ end
250
+ end
251
+ end
252
+
253
+ def changes
254
+ attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
255
+ if change = attribute_change(attr_name)
256
+ result.merge!(attr_name => change)
257
+ end
258
+ end
259
+ end
260
+
261
+ def changed?
262
+ attr_names.any? { |attr| attribute_changed?(attr) }
263
+ end
264
+
265
+ def attribute_change(attr_name)
266
+ if attribute_changed?(attr_name)
267
+ [attribute_was(attr_name), fetch_value(attr_name)]
268
+ end
269
+ end
270
+
271
+ def attribute_previous_change(attr_name)
272
+ previous_changes[attr_name]
273
+ end
274
+
275
+ def attribute_previously_was(attr_name)
276
+ if attribute_previously_changed?(attr_name)
277
+ # Calling +first+ here fetches the value before change from the
278
+ # hash.
279
+ previous_changes[attr_name].first
280
+ end
281
+ end
282
+
283
+ def attribute_changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
284
+ current_changes.include?(attr_name) &&
285
+ (OPTION_NOT_GIVEN == from || attribute_was(attr_name) == from) &&
286
+ (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
287
+ end
288
+
289
+ def attribute_previously_changed?(attr_name)
290
+ previous_changes.include?(attr_name)
291
+ end
292
+
293
+ def attribute_was(attr_name)
294
+ if attribute_changed?(attr_name)
295
+ current_changes[attr_name]
296
+ else
297
+ fetch_value(attr_name)
298
+ end
299
+ end
300
+
301
+ def attribute_will_change!(attr_name)
302
+ current_changes[attr_name] = fetch_value(attr_name) unless current_changes.include?(attr_name)
303
+ end
304
+
305
+ def restore_attribute!(attr_name)
306
+ current_changes.delete(attr_name)
307
+ end
308
+
309
+ # These are for ActiveRecord, but we'll define them here.
310
+ alias_method :saved_change_to_attribute?, :attribute_previously_changed?
311
+ alias_method :saved_change_to_attribute, :attribute_previous_change
312
+ alias_method :attribute_before_last_save, :attribute_previously_was
313
+ alias_method :will_save_change_to_attribute?, :attribute_changed?
314
+ alias_method :attribute_change_to_be_saved, :attribute_change
315
+ alias_method :attribute_in_database, :attribute_was
316
+
317
+ private
318
+ attr_reader :model, :current_changes
319
+
320
+ def attr_names
321
+ current_changes.keys
322
+ end
323
+
324
+ def fetch_value(attr_name)
325
+ model.__send__(attr_name)
326
+ end
327
+ end
328
+
329
+ module BackendMethods
330
+ # @!group Backend Accessors
331
+ # @!macro backend_writer
332
+ # @param [Hash] options
333
+ def write(locale, value, options = {})
334
+ locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
335
+ if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
336
+ mutations_from_mobility.restore_attribute!(locale_accessor)
337
+ elsif read(locale, options.merge(locale: true)) != value
338
+ mutations_from_mobility.attribute_will_change!(locale_accessor)
93
339
  end
340
+ super
341
+ end
342
+ # @!endgroup
343
+
344
+ private
345
+
346
+ def mutations_from_mobility
347
+ model.send(:mutations_from_mobility)
94
348
  end
95
349
  end
96
350
  end
97
351
  end
352
+
353
+ register_plugin(:active_model_dirty, ActiveModel::Dirty)
98
354
  end
99
355
  end
@@ -1,6 +1,40 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./active_record/backend"
3
+ require_relative "./active_record/dirty"
4
+ require_relative "./active_record/cache"
5
+ require_relative "./active_record/query"
6
+ require_relative "./active_record/uniqueness_validation"
7
+
1
8
  module Mobility
9
+ =begin
10
+
11
+ Plugin for ActiveRecord models.
12
+
13
+ =end
2
14
  module Plugins
3
15
  module ActiveRecord
16
+ extend Plugin
17
+
18
+ requires :active_record_backend, include: :after
19
+ requires :active_record_dirty
20
+ requires :active_record_cache
21
+ requires :active_record_query
22
+ requires :active_record_uniqueness_validation
23
+
24
+ included_hook do |klass|
25
+ unless active_record_class?(klass)
26
+ name = klass.name || klass.to_s
27
+ raise TypeError, "#{name} should be a subclass of ActiveRecord::Base to use the active_record plugin"
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def active_record_class?(klass)
34
+ klass < ::ActiveRecord::Base
35
+ end
4
36
  end
37
+
38
+ register_plugin(:active_record, ActiveRecord)
5
39
  end
6
40
  end