mobility 0.8.10 → 1.0.0.beta2

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 +66 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +36 -101
  7. data/README.md +183 -91
  8. data/Rakefile +6 -4
  9. data/lib/mobility.rb +44 -166
  10. data/lib/mobility/arel.rb +1 -1
  11. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  12. data/lib/mobility/backend.rb +27 -51
  13. data/lib/mobility/backends.rb +20 -0
  14. data/lib/mobility/backends/active_record.rb +4 -0
  15. data/lib/mobility/backends/active_record/column.rb +2 -0
  16. data/lib/mobility/backends/active_record/container.rb +6 -7
  17. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  18. data/lib/mobility/backends/active_record/json.rb +2 -0
  19. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  20. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  21. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  22. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  23. data/lib/mobility/backends/active_record/table.rb +6 -4
  24. data/lib/mobility/backends/column.rb +0 -6
  25. data/lib/mobility/backends/container.rb +10 -1
  26. data/lib/mobility/backends/hash.rb +39 -0
  27. data/lib/mobility/backends/hash_valued.rb +4 -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 +1 -2
  31. data/lib/mobility/backends/key_value.rb +31 -26
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +5 -2
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +6 -6
  36. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  37. data/lib/mobility/backends/sequel/json.rb +3 -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 +6 -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 +29 -26
  44. data/lib/mobility/pluggable.rb +56 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +27 -24
  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 +119 -78
  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 +34 -17
  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 +29 -20
  57. data/lib/mobility/plugins/attributes.rb +72 -0
  58. data/lib/mobility/plugins/backend.rb +161 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +68 -26
  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 +52 -44
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +22 -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 +33 -22
  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 +96 -78
  78. metadata +28 -27
  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
@@ -2,35 +2,38 @@ module Mobility
2
2
  =begin
3
3
 
4
4
  Plugins allow modular customization of backends independent of the backend
5
- itself. They are enabled through the {Configuration.plugins} configuration
6
- setting, which takes an array of symbols corresponding to plugin names. The
7
- order of these names is important since it determines the order in which
8
- plugins will be applied.
5
+ itself. They are enabled through {Mobility::Translations.plugins} (delegated to
6
+ from {Mobility.configure}), which takes a block within which plugins can be
7
+ declared in any order (dependencies will be resolved).
9
8
 
10
- So if our {Configuration.plugins} is an array +[:foo]+, and we call
11
- `translates` on our model, +Post+, like this:
9
+ =end
10
+ module Plugins
11
+ @plugins = {}
12
+ @names = {}
12
13
 
13
- class Post
14
- translates :title, foo: true
15
- end
14
+ class << self
15
+ # @param [Symbol] name Name of plugin to load.
16
+ def load_plugin(name)
17
+ return name if Module === name || name.nil?
16
18
 
17
- Then the +Foo+ plugin will be applied with the option value +true+. Applying a
18
- module calls a class method, +apply+ (in this case +Foo.apply+), which takes
19
- two arguments:
19
+ unless (plugin = @plugins[name])
20
+ require "mobility/plugins/#{name}"
21
+ raise LoadError, "plugin #{name} did not register itself correctly in Mobility::Plugins" unless (plugin = @plugins[name])
22
+ end
23
+ plugin
24
+ end
20
25
 
21
- - an instance of the {Attributes} class, +attributes+, from which the backend
22
- can configure the backend class (+attributes.backend_class+) and the model
23
- (+attributes.model_class+), and the +attributes+ module itself (which
24
- will be included into the backend).
25
- - the value of the +option+ passed into the model with +translates+ (in this
26
- case, +true+).
26
+ # @param [Module] plugin Plugin module to lookup. Plugin must already be loaded.
27
+ def lookup_name(plugin)
28
+ @names.fetch(plugin)
29
+ end
27
30
 
28
- Typically, the plugin will include a module into either
29
- +attributes.backend_class+ or +attributes+ itself, configured according to the
30
- option value. For examples, see classes under the {Mobility::Plugins} namespace.
31
+ def register_plugin(name, plugin)
32
+ @plugins[name] = plugin
33
+ @names[plugin] = name
34
+ end
31
35
 
32
- =end
33
- module Plugins
34
- OPTION_UNSET = Object.new
36
+ class LoadError < Error; end
37
+ end
35
38
  end
36
39
  end
@@ -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
@@ -42,106 +42,138 @@ the ActiveRecord dirty plugin for more information.
42
42
 
43
43
  =end
44
44
  module Dirty
45
- # Builds module which adds suffix/prefix methods for translated
46
- # attributes so they act like normal dirty-tracked attributes.
47
- class MethodsBuilder < Module
48
- delegate :dirty_class, :handler_methods_module, :method_patterns, to: :class
45
+ extend Plugin
49
46
 
50
- def initialize(*attribute_names)
51
- define_dirty_methods(attribute_names)
47
+ requires :dirty, include: false
48
+
49
+ initialize_hook do
50
+ if options[:dirty]
51
+ define_dirty_methods(names)
52
+ include dirty_handler_methods
52
53
  end
54
+ end
53
55
 
54
- def included(model_class)
55
- model_class.include InstanceMethods
56
- model_class.include handler_methods_module
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)
57
58
 
58
- # In earlier versions of Rails, these methods are private
59
- %i[clear_attribute_changes clear_changes_information changes_applied].each do |method_name|
60
- if dirty_class.private_instance_methods.include?(method_name)
61
- model_class.class_eval { private method_name }
62
- end
63
- end
64
- end
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) }
65
63
 
66
- def append_locale(attr_name)
67
- Mobility.normalize_locale_accessor(attr_name)
64
+ backend_class.include BackendMethods
68
65
  end
66
+ end
69
67
 
70
- private
68
+ private
71
69
 
72
- def define_dirty_methods(attribute_names)
73
- m = self
70
+ # Overridden in AR::Dirty plugin to define a different HandlerMethods module
71
+ def dirty_handler_methods
72
+ HandlerMethods
73
+ end
74
74
 
75
- attribute_names.each do |name|
76
- method_patterns.each do |pattern|
77
- define_method(pattern % name) do |*args|
78
- mutations_from_mobility.send(pattern % 'attribute', m.append_locale(name), *args)
79
- end
80
- end
75
+ def active_model_dirty_class?(klass)
76
+ klass.ancestors.include?(::ActiveModel::Dirty)
77
+ end
81
78
 
82
- define_method "restore_#{name}!" do
83
- locale_accessor = m.append_locale(name)
84
- if mutations_from_mobility.attribute_changed?(locale_accessor)
85
- __send__("#{name}=", mutations_from_mobility.attribute_was(locale_accessor))
86
- mutations_from_mobility.restore_attribute!(locale_accessor)
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
+ # for %s_changed?(from:, to:) pattern
84
+ if (kwargs = args.last).is_a?(Hash)
85
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args[0,-1], **kwargs)
86
+ else
87
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args)
87
88
  end
88
89
  end
89
90
  end
90
91
 
91
- # This private method override is necessary to make
92
- # +restore_attributes+ (which is public) work with translated
93
- # attributes.
94
- define_method :restore_attribute! do |attr|
95
- attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
96
- end
97
- private :restore_attribute!
98
- end
99
-
100
- class << self
101
- def handler_methods_module
102
- @handler_methods_module ||= (AttributeHandlerMethods.new.tap do |mod|
103
- public_method_patterns.each do |pattern|
104
- method_name = pattern % 'attribute'
105
-
106
- mod.module_eval <<-EOM, __FILE__, __LINE__ + 1
107
- def #{method_name}(attr_name, *rest)
108
- if (mutations_from_mobility.attribute_changed?(attr_name) ||
109
- mutations_from_mobility.attribute_previously_changed?(attr_name))
110
- mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest)
111
- else
112
- super
113
- end
114
- end
115
- EOM
116
- end
117
- end)
92
+ define_method "restore_#{name}!" do
93
+ locale_accessor = Dirty.append_locale(name)
94
+ if mutations_from_mobility.attribute_changed?(locale_accessor)
95
+ __send__("#{name}=", mutations_from_mobility.attribute_was(locale_accessor))
96
+ mutations_from_mobility.restore_attribute!(locale_accessor)
97
+ end
118
98
  end
99
+ end
100
+
101
+ # This private method override is necessary to make
102
+ # +restore_attributes+ (which is public) work with translated
103
+ # attributes.
104
+ define_method :restore_attribute! do |attr|
105
+ attribute_names.include?(attr.to_s) ? send("restore_#{attr}!") : super(attr)
106
+ end
107
+ private :restore_attribute!
108
+ end
119
109
 
120
- # Get method suffixes. Creating an object just to get the list of
121
- # suffixes is simplest given they change from Rails version to version.
122
- def method_patterns
123
- @method_patterns ||=
124
- (dirty_class.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_method_patterns)
110
+ def self.append_locale(attr_name)
111
+ Mobility.normalize_locale_accessor(attr_name)
112
+ end
113
+
114
+ # Module builder which mimics dirty method handlers on a given dirty class.
115
+ # Used to mimic ActiveModel::Dirty and ActiveRecord::Dirty, which have
116
+ # similar but slightly different sets of handler methods. Doing it this
117
+ # way with introspection allows us to support basically all AR/AM
118
+ # versions without changes here.
119
+ class HandlerMethodsBuilder < Module
120
+ attr_reader :klass
121
+
122
+ # @param [Class] klass Dirty class to mimic
123
+ def initialize(klass)
124
+ @klass = klass
125
+ define_handler_methods
126
+ end
127
+
128
+ def each_pattern(attr_name)
129
+ patterns.each do |pattern|
130
+ yield pattern % attr_name, pattern % 'attribute'
125
131
  end
132
+ end
133
+
134
+ def define_handler_methods
135
+ public_patterns.each do |pattern|
136
+ method_name = pattern % 'attribute'
126
137
 
127
- def public_method_patterns
128
- @public_method_patterns ||= method_patterns.select do |p|
129
- !dirty_class.private_instance_methods.include?(:"#{p % 'attribute'}")
138
+ kwargs = pattern == '%s_changed?' ? ', **kwargs' : ''
139
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
140
+ def #{method_name}(attr_name, *rest#{kwargs})
141
+ if (mutations_from_mobility.attribute_changed?(attr_name) ||
142
+ mutations_from_mobility.attribute_previously_changed?(attr_name))
143
+ mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest#{kwargs})
144
+ else
145
+ super
146
+ end
130
147
  end
148
+ EOM
131
149
  end
150
+ end
132
151
 
133
- def dirty_class
134
- @dirty_class ||= Class.new { include ::ActiveModel::Dirty }
135
- end
152
+ # Get method suffixes. Creating an object just to get the list of
153
+ # suffixes is simplest given they change from Rails version to version.
154
+ def patterns
155
+ @patterns ||=
156
+ (klass.attribute_method_matchers.map { |p| "#{p.prefix}%s#{p.suffix}" } - excluded_patterns)
157
+ end
136
158
 
137
- private
159
+ private
138
160
 
139
- def excluded_method_patterns
140
- ['%s', 'restore_%s!']
161
+ def public_patterns
162
+ @public_patterns ||= patterns.select do |p|
163
+ klass.public_method_defined?(p % 'attribute')
141
164
  end
142
165
  end
166
+
167
+ def excluded_patterns
168
+ ['%s', 'restore_%s!']
169
+ end
143
170
  end
144
171
 
172
+ # Module which defines generic handler methods like
173
+ # +attribute_changed?+ that are patched to work with translated
174
+ # attributes.
175
+ HandlerMethods = HandlerMethodsBuilder.new(Class.new { include ::ActiveModel::Dirty })
176
+
145
177
  module InstanceMethods
146
178
  def changed_attributes
147
179
  super.merge(mutations_from_mobility.changed_attributes)
@@ -186,9 +218,16 @@ the ActiveRecord dirty plugin for more information.
186
218
  end
187
219
  end
188
220
 
189
- # Give the module builder a name so it's easier to see in the model's ancestors
190
- class AttributeHandlerMethods < Module; end
191
-
221
+ # @note Seriously, I really don't want to reproduce all of
222
+ # ActiveModel::Dirty here, but having fought with upstream changes
223
+ # many many times I finally decided it's more future-proof to just
224
+ # re-implement the stuff we need here, to avoid weird breakage.
225
+ #
226
+ # Although this is somewhat ugly, at least it's explicit and since
227
+ # it's self-defined (rather than hooking into fickle private methods
228
+ # in Rails), it won't break all of a sudden. We just need to ensure
229
+ # that specs are up-to-date with the latest weird dirty method
230
+ # pattern Rails has decided to support.
192
231
  class MobilityMutationTracker
193
232
  OPTION_NOT_GIVEN = Object.new
194
233
 
@@ -297,11 +336,11 @@ the ActiveRecord dirty plugin for more information.
297
336
  # @!group Backend Accessors
298
337
  # @!macro backend_writer
299
338
  # @param [Hash] options
300
- def write(locale, value, options = {})
339
+ def write(locale, value, **options)
301
340
  locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
302
341
  if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
303
342
  mutations_from_mobility.restore_attribute!(locale_accessor)
304
- elsif read(locale, options.merge(locale: true)) != value
343
+ elsif read(locale, **options.merge(locale: true)) != value
305
344
  mutations_from_mobility.attribute_will_change!(locale_accessor)
306
345
  end
307
346
  super
@@ -316,5 +355,7 @@ the ActiveRecord dirty plugin for more information.
316
355
  end
317
356
  end
318
357
  end
358
+
359
+ register_plugin(:active_model_dirty, ActiveModel::Dirty)
319
360
  end
320
361
  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
@@ -0,0 +1,25 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveRecord
4
+ module Backend
5
+ extend Plugin
6
+
7
+ requires :backend, include: :before
8
+
9
+ def load_backend(backend)
10
+ if Symbol === backend
11
+ require "mobility/backends/active_record/#{backend}"
12
+ Backends.load_backend("active_record_#{backend}".to_sym)
13
+ else
14
+ super
15
+ end
16
+ rescue LoadError => e
17
+ raise unless e.message =~ /active_record\/#{backend}/
18
+ super
19
+ end
20
+ end
21
+ end
22
+
23
+ register_plugin(:active_record_backend, ActiveRecord::Backend)
24
+ end
25
+ end
@@ -0,0 +1,28 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/plugins/active_model/cache"
3
+
4
+ module Mobility
5
+ module Plugins
6
+ module ActiveRecord
7
+ =begin
8
+
9
+ Resets cache on calls to +reload+, in addition to other AM dirty reset
10
+ methods.
11
+
12
+ =end
13
+ module Cache
14
+ extend Plugin
15
+
16
+ requires :cache, include: false
17
+
18
+ included_hook do |klass, _|
19
+ if options[:cache]
20
+ define_cache_hooks(klass, :changes_applied, :clear_changes_information, :reload)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ register_plugin(:active_record_cache, ActiveRecord::Cache)
27
+ end
28
+ end