mobility 0.8.8 → 1.0.0.alpha

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 (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