mobility 0.8.13 → 1.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) 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 +26 -0
  5. data/Gemfile +5 -2
  6. data/Gemfile.lock +79 -8
  7. data/README.md +183 -91
  8. data/lib/mobility.rb +40 -166
  9. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  10. data/lib/mobility/backend.rb +19 -41
  11. data/lib/mobility/backends.rb +20 -0
  12. data/lib/mobility/backends/active_record.rb +4 -0
  13. data/lib/mobility/backends/active_record/column.rb +2 -0
  14. data/lib/mobility/backends/active_record/container.rb +4 -2
  15. data/lib/mobility/backends/active_record/hstore.rb +2 -0
  16. data/lib/mobility/backends/active_record/json.rb +2 -0
  17. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  18. data/lib/mobility/backends/active_record/key_value.rb +5 -3
  19. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  20. data/lib/mobility/backends/active_record/serialized.rb +2 -0
  21. data/lib/mobility/backends/active_record/table.rb +5 -3
  22. data/lib/mobility/backends/column.rb +0 -6
  23. data/lib/mobility/backends/container.rb +2 -1
  24. data/lib/mobility/backends/hash.rb +39 -0
  25. data/lib/mobility/backends/hstore.rb +0 -1
  26. data/lib/mobility/backends/json.rb +0 -1
  27. data/lib/mobility/backends/jsonb.rb +0 -1
  28. data/lib/mobility/backends/key_value.rb +22 -14
  29. data/lib/mobility/backends/null.rb +2 -0
  30. data/lib/mobility/backends/sequel.rb +3 -0
  31. data/lib/mobility/backends/sequel/column.rb +2 -0
  32. data/lib/mobility/backends/sequel/container.rb +3 -1
  33. data/lib/mobility/backends/sequel/hstore.rb +2 -0
  34. data/lib/mobility/backends/sequel/json.rb +2 -0
  35. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  36. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  37. data/lib/mobility/backends/sequel/serialized.rb +2 -0
  38. data/lib/mobility/backends/sequel/table.rb +5 -2
  39. data/lib/mobility/backends/serialized.rb +1 -3
  40. data/lib/mobility/backends/table.rb +14 -6
  41. data/lib/mobility/pluggable.rb +36 -0
  42. data/lib/mobility/plugin.rb +260 -0
  43. data/lib/mobility/plugins.rb +26 -25
  44. data/lib/mobility/plugins/active_model.rb +17 -0
  45. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  46. data/lib/mobility/plugins/active_model/dirty.rb +112 -77
  47. data/lib/mobility/plugins/active_record.rb +34 -0
  48. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  49. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  50. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  51. data/lib/mobility/plugins/active_record/query.rb +43 -31
  52. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  53. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  54. data/lib/mobility/plugins/attributes.rb +70 -0
  55. data/lib/mobility/plugins/backend.rb +138 -0
  56. data/lib/mobility/plugins/backend_reader.rb +34 -0
  57. data/lib/mobility/plugins/cache.rb +59 -24
  58. data/lib/mobility/plugins/default.rb +22 -17
  59. data/lib/mobility/plugins/dirty.rb +12 -33
  60. data/lib/mobility/plugins/fallbacks.rb +51 -43
  61. data/lib/mobility/plugins/fallthrough_accessors.rb +20 -23
  62. data/lib/mobility/plugins/locale_accessors.rb +25 -35
  63. data/lib/mobility/plugins/presence.rb +28 -21
  64. data/lib/mobility/plugins/query.rb +8 -17
  65. data/lib/mobility/plugins/reader.rb +50 -0
  66. data/lib/mobility/plugins/sequel.rb +34 -0
  67. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  68. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  69. data/lib/mobility/plugins/sequel/dirty.rb +32 -21
  70. data/lib/mobility/plugins/sequel/query.rb +21 -6
  71. data/lib/mobility/plugins/writer.rb +44 -0
  72. data/lib/mobility/translations.rb +95 -0
  73. data/lib/mobility/version.rb +12 -1
  74. data/lib/rails/generators/mobility/templates/initializer.rb +95 -77
  75. metadata +28 -27
  76. metadata.gz.sig +0 -0
  77. data/lib/mobility/active_model.rb +0 -4
  78. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  79. data/lib/mobility/active_record.rb +0 -23
  80. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  81. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  82. data/lib/mobility/attributes.rb +0 -324
  83. data/lib/mobility/backend/orm_delegator.rb +0 -44
  84. data/lib/mobility/backend_resetter.rb +0 -50
  85. data/lib/mobility/configuration.rb +0 -138
  86. data/lib/mobility/fallbacks.rb +0 -28
  87. data/lib/mobility/interface.rb +0 -0
  88. data/lib/mobility/loaded.rb +0 -4
  89. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  90. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  91. data/lib/mobility/sequel.rb +0 -9
  92. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  93. data/lib/mobility/translates.rb +0 -73
@@ -65,7 +65,6 @@ set.
65
65
  @see Mobility::Backends::Sequel::Table
66
66
  =end
67
67
  module Table
68
- extend Backend::OrmDelegator
69
68
  # @!method association_name
70
69
  # Returns the name of the translations association.
71
70
  # @return [Symbol] Name of the association
@@ -134,7 +133,18 @@ set.
134
133
  # Simple hash cache to memoize translations as a hash so they can be
135
134
  # fetched quickly.
136
135
  module Cache
137
- include Plugins::Cache::TranslationCacher.new(:translation_for)
136
+ def translation_for(locale, **options)
137
+ return super(locale, options) if options.delete(:cache) == false
138
+ if cache.has_key?(locale)
139
+ cache[locale]
140
+ else
141
+ cache[locale] = super(locale, options)
142
+ end
143
+ end
144
+
145
+ def clear_cache
146
+ model_cache && model_cache.clear
147
+ end
138
148
 
139
149
  private
140
150
 
@@ -145,11 +155,9 @@ set.
145
155
  def model_cache
146
156
  model.instance_variable_get(:"@__mobility_#{association_name}_cache")
147
157
  end
148
-
149
- def clear_cache
150
- model_cache && model_cache.clear
151
- end
152
158
  end
153
159
  end
160
+
161
+ register_backend(:table, Table)
154
162
  end
155
163
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mobility
4
+ =begin
5
+
6
+ Abstract Module subclass with methods to define plugins and defaults.
7
+ Works with {Mobility::Plugin}. (Subclassed by {Mobility::Translations}.)
8
+
9
+ =end
10
+ class Pluggable < Module
11
+ class << self
12
+ def plugin(name, *args)
13
+ Plugin.configure(self, defaults) { __send__ name, *args }
14
+ end
15
+
16
+ def plugins(&block)
17
+ Plugin.configure(self, defaults, &block)
18
+ end
19
+
20
+ def defaults
21
+ @defaults ||= {}
22
+ end
23
+
24
+ def inherited(klass)
25
+ super
26
+ klass.defaults.merge!(defaults)
27
+ end
28
+ end
29
+
30
+ def initialize(*, **options)
31
+ @options = self.class.defaults.merge(options)
32
+ end
33
+
34
+ attr_reader :options
35
+ end
36
+ end
@@ -0,0 +1,260 @@
1
+ # frozen-string-literal: true
2
+ require "tsort"
3
+ require "mobility/util"
4
+
5
+ module Mobility
6
+ =begin
7
+
8
+ Defines convenience methods on plugin module to hook into initialize/included
9
+ method calls on +Mobility::Pluggable+ instance.
10
+
11
+ - #initialize_hook: called after {Mobility::Pluggable#initialize}, with
12
+ attribute names.
13
+ - #included_hook: called after {Mobility::Pluggable#included}. (This can be
14
+ used to include any module(s) into the backend class, see
15
+ {Mobility::Plugins::Backend}.)
16
+
17
+ Also includes a +configure+ class method to apply plugins to a pluggable
18
+ ({Mobility::Pluggable} instance), with a block.
19
+
20
+ @example Defining a plugin
21
+ module MyPlugin
22
+ extend Mobility::Plugin
23
+
24
+ initialize_hook do |*names|
25
+ names.each do |name|
26
+ define_method "#{name}_foo" do
27
+ # method body
28
+ end
29
+ end
30
+ end
31
+
32
+ included_hook do |klass, backend_class|
33
+ backend_class.include MyBackendMethods
34
+ klass.include MyModelMethods
35
+ end
36
+ end
37
+
38
+ @example Configure an attributes class with plugins
39
+ class Translations < Mobility::Translations
40
+ end
41
+
42
+ Mobility::Plugin.configure(Translations) do
43
+ cache
44
+ fallbacks
45
+ end
46
+
47
+ Translations.included_modules
48
+ #=> [Mobility::Plugins::Fallbacks, Mobility::Plugins::Cache, ...]
49
+ =end
50
+ module Plugin
51
+ class << self
52
+ # Configure a pluggable {Mobility::Pluggable} with a block. Yields to a
53
+ # clean room where plugin names define plugins on the module. Plugin
54
+ # dependencies are resolved before applying them.
55
+ #
56
+ # @param [Class, Module] pluggable
57
+ # @param [Hash] defaults Plugin defaults hash to update
58
+ # @yield Block to define plugins
59
+ # @return [Hash] Updated plugin defaults
60
+ # @raise [Mobility::Plugin::CyclicDependency] if dependencies cannot be met
61
+ # @example
62
+ # Mobility::Plugin.configure(Translations) do
63
+ # cache
64
+ # fallbacks [:en, :de]
65
+ # end
66
+ def configure(pluggable, defaults = pluggable.defaults, &block)
67
+ DependencyResolver.new(pluggable, defaults).call(&block)
68
+ end
69
+ end
70
+
71
+ def initialize_hook(&block)
72
+ plugin = self
73
+
74
+ define_method :initialize do |*args, **options|
75
+ super(*args, **options)
76
+
77
+ class_exec(*args, &block) if plugin.dependencies_satisfied?(self.class)
78
+ end
79
+ end
80
+
81
+ def included_hook(&block)
82
+ plugin = self
83
+
84
+ define_method :included do |klass|
85
+ super(klass).tap do |backend_class|
86
+ if plugin.dependencies_satisfied?(self.class)
87
+ class_exec(klass, backend_class, &block)
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def included(pluggable)
94
+ if defined?(@default) && !pluggable.defaults.has_key?(name = Plugins.lookup_name(self))
95
+ pluggable.defaults[name] = @default
96
+ end
97
+ super
98
+ end
99
+
100
+ def dependencies
101
+ @dependencies ||= {}
102
+ end
103
+
104
+ def default(value)
105
+ @default = value
106
+ end
107
+
108
+ # Method called when defining plugins to assign a default based on
109
+ # arguments and keyword arguments to the plugin method. By default, we
110
+ # simply assign the first argument, but plugins can opt to customize this
111
+ # if additional arguments or keyword arguments are required.
112
+ # (The backend plugin uses keyword arguments to set backend options.)
113
+ #
114
+ # @param [Hash] defaults
115
+ # @param [Symbol] key Plugin key on hash
116
+ # @param [Array] args Method arguments
117
+ def configure_default(defaults, key, *args)
118
+ defaults[key] = args[0] unless args.empty?
119
+ end
120
+
121
+ # Does this class include all plugins this plugin depends (directly) on?
122
+ # @param [Class] klass Pluggable class
123
+ def dependencies_satisfied?(klass)
124
+ required_plugins = dependencies.keys.map { |name| Plugins.load_plugin(name) }
125
+ (required_plugins - klass.included_modules).none?
126
+ end
127
+
128
+ # Specifies a dependency of this plugin.
129
+ #
130
+ # By default, the dependency is included (include: true). Passing +:before+
131
+ # or +:after+ will ensure the dependency is included before or after this
132
+ # plugin.
133
+ #
134
+ # Passing +false+ does not include the dependency, but checks that it has
135
+ # been included when running include and initialize hooks (so hooks will
136
+ # not run for this plugin if it has not been included). In other words:
137
+ # disable this plugin unless this dependency has been included elsewhere.
138
+ # (Note that this check is not applied recursively.)
139
+ #
140
+ # @param [Symbol] plugin Name of plugin dependency
141
+ # @option [TrueClass, FalseClass, Symbol] include
142
+ def requires(plugin, include: true)
143
+ unless [true, false, :before, :after].include?(include)
144
+ raise ArgumentError, "requires 'include' keyword argument must be one of: true, false, :before or :after"
145
+ end
146
+ dependencies[plugin] = include
147
+ end
148
+
149
+ DependencyResolver = Struct.new(:pluggable, :defaults) do
150
+ def call(&block)
151
+ plugins = DSL.call(defaults, &block)
152
+ tree = create_tree(plugins)
153
+
154
+ pluggable.include(*tree.tsort.reverse) unless tree.empty?
155
+ rescue TSort::Cyclic => e
156
+ raise_cyclic_dependency!(e.message)
157
+ end
158
+
159
+ private
160
+
161
+ def create_tree(plugins)
162
+ DependencyTree.new.tap do |tree|
163
+ visited = included_plugins
164
+ plugins.each { |plugin| traverse(tree, plugin, visited) }
165
+ end
166
+ end
167
+
168
+ def included_plugins
169
+ pluggable.included_modules.grep(Plugin)
170
+ end
171
+
172
+ # Recursively traverse dependencies and add their dependencies to tree
173
+ def traverse(tree, plugin, visited)
174
+ return if visited.include?(plugin)
175
+
176
+ tree.add(plugin)
177
+
178
+ plugin.dependencies.each do |dep_name, include_order|
179
+ next unless include_order
180
+ dep = Plugins.load_plugin(dep_name)
181
+ add_dependency(plugin, dep, tree, include_order)
182
+
183
+ traverse(tree, dep, visited << plugin)
184
+ end
185
+ end
186
+
187
+ def add_dependency(plugin, dep, tree, include_order)
188
+ case include_order
189
+ when :before
190
+ tree[plugin] += [dep]
191
+ when :after
192
+ check_after_dependency!(plugin, dep)
193
+ tree.add(dep)
194
+ tree[dep] += [plugin]
195
+ end
196
+ end
197
+
198
+ def check_after_dependency!(plugin, dep)
199
+ if included_plugins.include?(dep)
200
+ message = "'#{name(dep)}' plugin must come after '#{name(plugin)}' plugin"
201
+ raise DependencyConflict, append_pluggable_name(message)
202
+ end
203
+ end
204
+
205
+ def raise_cyclic_dependency!(error_message)
206
+ components = error_message.scan(/(?<=\[).*(?=\])/).first
207
+ names = components.split(', ').map! do |plugin|
208
+ name(Object.const_get(plugin)).to_s
209
+ end
210
+ message = "Dependencies cannot be resolved between: #{names.sort.join(', ')}"
211
+ raise CyclicDependency, append_pluggable_name(message)
212
+ end
213
+
214
+ def append_pluggable_name(message)
215
+ pluggable.name ? "#{message} in #{pluggable}" : message
216
+ end
217
+
218
+ def name(plugin)
219
+ Plugins.lookup_name(plugin)
220
+ end
221
+
222
+ class DependencyTree < Hash
223
+ include ::TSort
224
+ NO_DEPENDENCIES = Set.new.freeze
225
+
226
+ def add(key)
227
+ self[key] ||= NO_DEPENDENCIES
228
+ end
229
+
230
+ alias tsort_each_node each_key
231
+
232
+ def tsort_each_child(dep, &block)
233
+ self.fetch(dep, []).each(&block)
234
+ end
235
+ end
236
+
237
+ class DSL < BasicObject
238
+ def self.call(defaults, &block)
239
+ new(plugins = ::Set.new, defaults).instance_eval(&block)
240
+ plugins
241
+ end
242
+
243
+ def initialize(plugins, defaults)
244
+ @plugins = plugins
245
+ @defaults = defaults
246
+ end
247
+
248
+ def method_missing(m, *args)
249
+ plugin = Plugins.load_plugin(m)
250
+ @plugins << plugin
251
+ plugin.configure_default(@defaults, m, *args)
252
+ end
253
+ end
254
+ end
255
+ private_constant :DependencyResolver
256
+
257
+ class DependencyConflict < Mobility::Error; end
258
+ class CyclicDependency < DependencyConflict; end
259
+ end
260
+ end
@@ -2,35 +2,36 @@ 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:
12
-
13
- class Post
14
- translates :title, foo: true
15
- end
9
+ =end
10
+ module Plugins
11
+ @plugins = {}
12
+ @names = {}
16
13
 
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:
14
+ class << self
15
+ # @param [Symbol] name Name of plugin to load.
16
+ def load_plugin(name)
17
+ unless (plugin = @plugins[name])
18
+ require "mobility/plugins/#{name}"
19
+ raise LoadError, "plugin #{name} did not register itself correctly in Mobility::Plugins" unless (plugin = @plugins[name])
20
+ end
21
+ plugin
22
+ end
20
23
 
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+).
24
+ # @param [Module] plugin Plugin module to lookup. Plugin must already be loaded.
25
+ def lookup_name(plugin)
26
+ @names.fetch(plugin)
27
+ end
27
28
 
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.
29
+ def register_plugin(name, plugin)
30
+ @plugins[name] = plugin
31
+ @names[plugin] = name
32
+ end
31
33
 
32
- =end
33
- module Plugins
34
- OPTION_UNSET = Object.new
34
+ class LoadError < Error; end
35
+ end
35
36
  end
36
37
  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