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,60 +0,0 @@
1
- module Mobility
2
- module ActiveRecord
3
- =begin
4
-
5
- A backend-agnostic uniqueness validator for ActiveRecord translated attributes.
6
- To use the validator, you must +extend Mobility+ before calling +validates+
7
- (see example below).
8
-
9
- @note This validator does not support case sensitivity, since doing so would
10
- significantly complicate implementation.
11
-
12
- @example Validating uniqueness on translated model
13
- class Post < ActiveRecord::Base
14
- extend Mobility
15
- translates :title
16
-
17
- # This must come *after* extending Mobility.
18
- validates :title, uniqueness: true
19
- end
20
- =end
21
- class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
22
- # @param [ActiveRecord::Base] record Translated model
23
- # @param [String] attribute Name of attribute
24
- # @param [Object] value Attribute value
25
- def validate_each(record, attribute, value)
26
- klass = record.class
27
-
28
- if (([*options[:scope]] + [attribute]).map(&:to_s) & klass.mobility_attributes).present?
29
- return unless value.present?
30
- relation = klass.unscoped.__mobility_query_scope__ do |m|
31
- node = m.__send__(attribute)
32
- options[:case_sensitive] == false ? node.lower.eq(value.downcase) : node.eq(value)
33
- end
34
- relation = relation.where.not(klass.primary_key => record.id) if record.persisted?
35
- relation = mobility_scope_relation(record, relation)
36
- relation = relation.merge(options[:conditions]) if options[:conditions]
37
-
38
- if relation.exists?
39
- error_options = options.except(:case_sensitive, :scope, :conditions)
40
- error_options[:value] = value
41
-
42
- record.errors.add(attribute, :taken, error_options)
43
- end
44
- else
45
- super
46
- end
47
- end
48
-
49
- private
50
-
51
- def mobility_scope_relation(record, relation)
52
- [*options[:scope]].inject(relation) do |scoped_relation, scope_item|
53
- scoped_relation.__mobility_query_scope__ do |m|
54
- m.__send__(scope_item).eq(record.send(scope_item))
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,324 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/util"
3
-
4
- module Mobility
5
- =begin
6
-
7
- Defines accessor methods to include on model class. Inspired by Traco's
8
- +Traco::Attributes+ class.
9
-
10
- Normally this class will be created through class methods defined using
11
- {Mobility::Translates} accessor methods, and need not be created directly.
12
- However, the class is central to how Mobility hooks into models to add
13
- accessors and other methods, and should be useful as a reference when
14
- understanding and designing backends.
15
-
16
- ==Including Attributes in a Class
17
-
18
- Since {Attributes} is a subclass of +Module+, including an instance of it is
19
- like including a module. Creating an instance like this:
20
-
21
- Attributes.new("title", backend: :my_backend, locale_accessors: [:en, :ja], cache: true, fallbacks: true)
22
-
23
- will generate an anonymous module that behaves approximately like this:
24
-
25
- Module.new do
26
- def mobility_backends
27
- # Returns a memoized hash with attribute name keys and backend instance
28
- # values. When a key is fetched from the hash, the hash calls
29
- # +self.class.mobility_backend_class(name)+ (where +name+ is the
30
- # attribute name) to get the backend class, then instantiate it (passing
31
- # the model instance and attribute name to its initializer) and return it.
32
- #
33
- # The backend class returned from the class method
34
- # +mobility_backend_class+ returns a subclass of
35
- # +Mobility::Backends::MyBackend+ and includes into it:
36
- #
37
- # - Mobility::Plugins::Cache (from the +cache: true+ option)
38
- # - instance of Mobility::Plugins::Fallbacks (from the +fallbacks: true+ option)
39
- # - Mobility::Plugins::Presence (by default, disabled by +presence: false+)
40
- end
41
-
42
- def title(locale: Mobility.locale)
43
- mobility_backends[:title].read(locale)
44
- end
45
-
46
- def title?(locale: Mobility.locale)
47
- mobility_backends[:title].read(locale).present?
48
- end
49
-
50
- def title=(value, locale: Mobility.locale)
51
- mobility_backends[:title].write(locale, value)
52
- end
53
-
54
- # Start Locale Accessors
55
- #
56
- def title_en
57
- title(locale: :en)
58
- end
59
-
60
- def title_en?
61
- title?(locale: :en)
62
- end
63
-
64
- def title_en=(value)
65
- public_send(:title=, value, locale: :en)
66
- end
67
-
68
- def title_ja
69
- title(locale: :ja)
70
- end
71
-
72
- def title_ja?
73
- title?(locale: :ja)
74
- end
75
-
76
- def title_ja=(value)
77
- public_send(:title=, value, locale: :ja)
78
- end
79
- # End Locale Accessors
80
- end
81
-
82
- Including this module into a model class will thus add the backend method, the
83
- reader, writer and presence methods, and the locale accessor so the model
84
- class. (These methods are in fact added to the model in an +included+ hook.)
85
-
86
- Note that some simplifications have been made above for readability. (In
87
- reality, all getters and setters accept an options hash which is passed along
88
- to the backend instance.)
89
-
90
- ==Setting up the Model Class
91
-
92
- Accessor methods alone are of limited use without a hook to actually modify the
93
- model class. This hook is provided by the {Backend::Setup#setup_model} method,
94
- which is added to every backend class when it includes the {Backend} module.
95
-
96
- Assuming the backend has defined a setup block by calling +setup+, this block
97
- will be called when {Attributes} is {#included} in the model class, passed
98
- attributes and options defined when the backend was defined on the model class.
99
- This allows a backend to do things like (for example) define associations on a
100
- model class required by the backend, as happens in the {Backends::KeyValue} and
101
- {Backends::Table} backends.
102
-
103
- Since setup blocks are evaluated on the model class, it is possible that
104
- backends can conflict (for example, overwriting previously defined methods).
105
- Care should be taken to avoid defining methods on the model class, or where
106
- necessary, ensure that names are defined in such a way as to avoid conflicts
107
- with other backends.
108
-
109
- =end
110
- class Attributes < Module
111
-
112
- # Method (accessor, reader or writer)
113
- # @return [Symbol] method
114
- attr_reader :method
115
-
116
- # Attribute names for which accessors will be defined
117
- # @return [Array<String>] Array of names
118
- attr_reader :names
119
-
120
- # Backend options
121
- # @return [Hash] Backend options
122
- attr_reader :options
123
-
124
- # Backend class
125
- # @return [Class] Backend class
126
- attr_reader :backend_class
127
-
128
- # Name of backend
129
- # @return [Symbol,Class] Name of backend, or backend class
130
- attr_reader :backend_name
131
-
132
- # Model class
133
- # @return [Class] Class of model
134
- attr_reader :model_class
135
-
136
- # @param [Symbol] method One of: [reader, writer, accessor]
137
- # @param [Array<String>] attribute_names Names of attributes to define backend for
138
- # @param [Hash] backend_options Backend options hash
139
- # @option backend_options [Class] model_class Class of model
140
- # @raise [ArgumentError] if method is not reader, writer or accessor
141
- def initialize(*attribute_names, method: :accessor, backend: Mobility.default_backend, **backend_options)
142
- raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
143
- @method = method
144
- @options = Mobility.default_options.to_h.merge(backend_options)
145
- @names = attribute_names.map(&:to_s).freeze
146
- raise BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
147
- @backend_name = backend
148
- end
149
-
150
- # Setup backend class, include modules into model class, include/extend
151
- # shared modules and setup model with backend setup block (see
152
- # {Mobility::Backend::Setup#setup_model}).
153
- # @param klass [Class] Class of model
154
- def included(klass)
155
- @model_class = @options[:model_class] = klass
156
- @backend_class = get_backend_class(backend_name).for(model_class).with_options(options)
157
-
158
- Mobility.plugins.each do |name|
159
- plugin = get_plugin_class(name)
160
- plugin.apply(self, options[name])
161
- end
162
-
163
- each do |name|
164
- define_backend(name)
165
- define_reader(name) if %i[accessor reader].include?(method)
166
- define_writer(name) if %i[accessor writer].include?(method)
167
- end
168
-
169
- klass.include InstanceMethods
170
- klass.extend ClassMethods
171
-
172
- backend_class.setup_model(model_class, names)
173
- end
174
-
175
- # Yield each attribute name to block
176
- # @yieldparam [String] Attribute
177
- def each &block
178
- names.each(&block)
179
- end
180
-
181
- # Show useful information about this module.
182
- # @return [String]
183
- def inspect
184
- "#<Attributes (#{backend_name}) @names=#{names.join(", ")}>"
185
- end
186
-
187
- private
188
-
189
- def define_backend(attribute)
190
- module_eval <<-EOM, __FILE__, __LINE__ + 1
191
- def #{Backend.method_name(attribute)}
192
- mobility_backends[:#{attribute}]
193
- end
194
- EOM
195
- end
196
-
197
- def define_reader(attribute)
198
- class_eval <<-EOM, __FILE__, __LINE__ + 1
199
- def #{attribute}(**options)
200
- return super() if options.delete(:super)
201
- #{set_locale_from_options_inline}
202
- mobility_backends[:#{attribute}].read(locale, options)
203
- end
204
-
205
- def #{attribute}?(**options)
206
- return super() if options.delete(:super)
207
- #{set_locale_from_options_inline}
208
- mobility_backends[:#{attribute}].present?(locale, options)
209
- end
210
- EOM
211
- end
212
-
213
- def define_writer(attribute)
214
- class_eval <<-EOM, __FILE__, __LINE__ + 1
215
- def #{attribute}=(value, **options)
216
- return super(value) if options.delete(:super)
217
- #{set_locale_from_options_inline}
218
- mobility_backends[:#{attribute}].write(locale, value, options)
219
- end
220
- EOM
221
- end
222
-
223
- # This string is evaluated inline in order to optimize performance of
224
- # getters and setters, avoiding extra steps where they are unneeded.
225
- def set_locale_from_options_inline
226
- <<-EOL
227
- if options[:locale]
228
- #{"Mobility.enforce_available_locales!(options[:locale])" if I18n.enforce_available_locales}
229
- locale = options[:locale].to_sym
230
- options[:locale] &&= !!locale
231
- else
232
- locale = Mobility.locale
233
- end
234
- EOL
235
- end
236
-
237
- def get_backend_class(backend)
238
- return backend if Module === backend
239
- require "mobility/backends/#{backend}"
240
- get_class_from_key(Mobility::Backends, backend)
241
- end
242
-
243
- def get_plugin_class(plugin)
244
- require "mobility/plugins/#{plugin}"
245
- get_class_from_key(Mobility::Plugins, plugin)
246
- end
247
-
248
- def get_class_from_key(parent_class, key)
249
- klass_name = key.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
250
- parent_class.const_get(klass_name)
251
- end
252
-
253
- module InstanceMethods
254
- # Return a new backend for an attribute name.
255
- # @return [Hash] Hash of attribute names and backend instances
256
- # @api private
257
- def mobility_backends
258
- @mobility_backends ||= Hash.new do |hash, name|
259
- next hash[name.to_sym] if String === name
260
- hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
261
- end
262
- end
263
-
264
- def initialize_dup(other)
265
- @mobility_backends = nil
266
- super
267
- end
268
- end
269
-
270
- module ClassMethods
271
- # Return all {Mobility::Attribute} module instances from among ancestors
272
- # of this model.
273
- # @return [Array<Mobility::Attributes>] Attribute modules
274
- def mobility_modules
275
- ancestors.grep(Attributes)
276
- end
277
-
278
- # Return translated attribute names on this model.
279
- # @return [Array<String>] Attribute names
280
- def mobility_attributes
281
- mobility_modules.map(&:names).flatten.uniq
282
- end
283
-
284
- # Return true if attribute name is translated on this model.
285
- # @param [String, Symbol] Attribute name
286
- # @return [Boolean]
287
- def mobility_attribute?(name)
288
- mobility_attributes.include?(name.to_s)
289
- end
290
-
291
- # @!method translated_attribute_names
292
- # @return (see #mobility_attributes)
293
- alias translated_attribute_names mobility_attributes
294
-
295
- # Return backend class for a given attribute name.
296
- # @param [Symbol,String] Name of attribute
297
- # @return [Class] Backend class
298
- def mobility_backend_class(name)
299
- @backends ||= BackendsCache.new(self)
300
- @backends[name.to_sym]
301
- end
302
-
303
- class BackendsCache < Hash
304
- def initialize(klass)
305
- # Preload backend mapping
306
- klass.mobility_modules.each do |mod|
307
- mod.names.each { |name| self[name.to_sym] = mod.backend_class }
308
- end
309
-
310
- super() do |hash, name|
311
- if mod = klass.mobility_modules.find { |m| m.names.include? name.to_s }
312
- hash[name] = mod.backend_class
313
- else
314
- raise KeyError, "No backend for: #{name}."
315
- end
316
- end
317
- end
318
- end
319
- private_constant :BackendsCache
320
- end
321
- end
322
-
323
- class BackendRequired < ArgumentError; end
324
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
- module Mobility
3
- module Backend
4
- =begin
5
-
6
- Adds {#for} method to backend to return ORM-specific backend.
7
-
8
- @example KeyValue backend for AR model
9
- class Post < ActiveRecord::Base
10
- # ...
11
- end
12
- Mobility::Backends::KeyValue.for(Post)
13
- #=> Mobility::Backends::ActiveRecord::KeyValue
14
-
15
- =end
16
- module OrmDelegator
17
- # @param [Class] model_class Class of model
18
- # @return [Class] Class of backend to use for model
19
- def for(model_class)
20
- namespace = name.split('::')
21
- if Loaded::ActiveRecord && model_class < ::ActiveRecord::Base
22
- require_backend("active_record", namespace.last.underscore)
23
- const_get(namespace.insert(-2, "ActiveRecord").join("::"))
24
- elsif Loaded::Sequel && model_class < ::Sequel::Model
25
- require_backend("sequel", namespace.last.underscore)
26
- const_get(namespace.insert(-2, "Sequel").join("::"))
27
- else
28
- raise ArgumentError, "#{namespace.last} backend can only be used by ActiveRecord or Sequel models"
29
- end
30
- end
31
-
32
- private
33
-
34
- def require_backend(orm, backend)
35
- begin
36
- orm_backend = "mobility/backends/#{orm}/#{backend}"
37
- require orm_backend
38
- rescue LoadError => e
39
- raise unless e.message =~ /#{orm_backend}/
40
- end
41
- end
42
- end
43
- end
44
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mobility
4
- =begin
5
-
6
- Resets backend cache when reset events occur.
7
-
8
- @example Add trigger to call a method +my_backend_reset_method+ on backend instance when reset event(s) occurs on model
9
- resetter = Mobility::BackendResetter.for(MyModel).new(attributes) { my_backend_reset_method }
10
- MyModel.include(resetter)
11
-
12
- @see Mobility::ActiveRecord::BackendResetter
13
- @see Mobility::ActiveModel::BackendResetter
14
- @see Mobility::Sequel::BackendResetter
15
-
16
- =end
17
- class BackendResetter < Module
18
- # @param [Array<String>] attribute_names Names of attributes whose backends should be reset
19
- # @yield Backend to reset as context for block
20
- # @raise [ArgumentError] if no block is provided.
21
- def initialize(attribute_names, &block)
22
- raise ArgumentError, "block required" unless block_given?
23
- names = attribute_names.map(&:to_sym)
24
- @model_reset_method = Proc.new do
25
- names.each do |name|
26
- if @mobility_backends && @mobility_backends[name]
27
- @mobility_backends[name].instance_eval(&block)
28
- end
29
- end
30
- end
31
- end
32
-
33
- # Returns backend resetter class for model class
34
- # @param [Class] model_class Class of model to which backend resetter will be applied
35
- def self.for(model_class)
36
- if Loaded::ActiveRecord && model_class < ::ActiveRecord::Base
37
- require "mobility/active_record/backend_resetter"
38
- ActiveRecord::BackendResetter
39
- elsif Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveModel::Dirty)
40
- require "mobility/active_model/backend_resetter"
41
- ActiveModel::BackendResetter
42
- elsif Loaded::Sequel && model_class < ::Sequel::Model
43
- require "mobility/sequel/backend_resetter"
44
- Sequel::BackendResetter
45
- else
46
- self
47
- end
48
- end
49
- end
50
- end