mobility 0.8.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) 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 +63 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +44 -52
  7. data/Guardfile +23 -1
  8. data/README.md +183 -91
  9. data/Rakefile +6 -4
  10. data/lib/mobility.rb +44 -166
  11. data/lib/mobility/active_record/translation.rb +1 -1
  12. data/lib/mobility/arel.rb +1 -1
  13. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  14. data/lib/mobility/backend.rb +27 -51
  15. data/lib/mobility/backends.rb +20 -0
  16. data/lib/mobility/backends/active_record.rb +4 -0
  17. data/lib/mobility/backends/active_record/column.rb +2 -0
  18. data/lib/mobility/backends/active_record/container.rb +6 -7
  19. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  20. data/lib/mobility/backends/active_record/json.rb +2 -0
  21. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  22. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  23. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  24. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  25. data/lib/mobility/backends/active_record/table.rb +6 -4
  26. data/lib/mobility/backends/column.rb +0 -6
  27. data/lib/mobility/backends/container.rb +10 -1
  28. data/lib/mobility/backends/hash.rb +39 -0
  29. data/lib/mobility/backends/hash_valued.rb +4 -0
  30. data/lib/mobility/backends/hstore.rb +0 -1
  31. data/lib/mobility/backends/json.rb +0 -1
  32. data/lib/mobility/backends/jsonb.rb +1 -2
  33. data/lib/mobility/backends/key_value.rb +31 -26
  34. data/lib/mobility/backends/null.rb +2 -0
  35. data/lib/mobility/backends/sequel.rb +5 -2
  36. data/lib/mobility/backends/sequel/column.rb +2 -0
  37. data/lib/mobility/backends/sequel/container.rb +6 -6
  38. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  39. data/lib/mobility/backends/sequel/json.rb +3 -0
  40. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  41. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  42. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  43. data/lib/mobility/backends/sequel/table.rb +5 -2
  44. data/lib/mobility/backends/serialized.rb +1 -3
  45. data/lib/mobility/backends/table.rb +29 -26
  46. data/lib/mobility/pluggable.rb +56 -0
  47. data/lib/mobility/plugin.rb +260 -0
  48. data/lib/mobility/plugins.rb +27 -24
  49. data/lib/mobility/plugins/active_model.rb +17 -0
  50. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  51. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  52. data/lib/mobility/plugins/active_record.rb +34 -0
  53. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  54. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  55. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  56. data/lib/mobility/plugins/active_record/query.rb +48 -34
  57. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  58. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  59. data/lib/mobility/plugins/attributes.rb +70 -0
  60. data/lib/mobility/plugins/backend.rb +161 -0
  61. data/lib/mobility/plugins/backend_reader.rb +34 -0
  62. data/lib/mobility/plugins/cache.rb +68 -26
  63. data/lib/mobility/plugins/default.rb +22 -17
  64. data/lib/mobility/plugins/dirty.rb +12 -33
  65. data/lib/mobility/plugins/fallbacks.rb +52 -44
  66. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  67. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  68. data/lib/mobility/plugins/presence.rb +28 -21
  69. data/lib/mobility/plugins/query.rb +8 -17
  70. data/lib/mobility/plugins/reader.rb +50 -0
  71. data/lib/mobility/plugins/sequel.rb +34 -0
  72. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  73. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  74. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  75. data/lib/mobility/plugins/sequel/query.rb +21 -6
  76. data/lib/mobility/plugins/writer.rb +44 -0
  77. data/lib/mobility/translations.rb +95 -0
  78. data/lib/mobility/version.rb +12 -1
  79. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  80. metadata +51 -51
  81. metadata.gz.sig +0 -0
  82. data/lib/mobility/active_model.rb +0 -4
  83. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record.rb +0 -23
  85. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  86. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  87. data/lib/mobility/attributes.rb +0 -324
  88. data/lib/mobility/backend/orm_delegator.rb +0 -44
  89. data/lib/mobility/backend_resetter.rb +0 -50
  90. data/lib/mobility/configuration.rb +0 -138
  91. data/lib/mobility/fallbacks.rb +0 -28
  92. data/lib/mobility/interface.rb +0 -0
  93. data/lib/mobility/loaded.rb +0 -4
  94. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  95. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  96. data/lib/mobility/sequel.rb +0 -9
  97. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  98. data/lib/mobility/translates.rb +0 -73
metadata.gz.sig CHANGED
Binary file
@@ -1,4 +0,0 @@
1
- module Mobility
2
- module ActiveModel
3
- end
4
- end
@@ -1,26 +0,0 @@
1
- module Mobility
2
- module ActiveModel
3
- =begin
4
-
5
- Backend resetter for ActiveModel models. Adds hook to reset backend when
6
- +changes_applied+ or +clear_changes_information+ methods are called on model.
7
-
8
- =end
9
- class BackendResetter < Mobility::BackendResetter
10
-
11
- # (see Mobility::BackendResetter#initialize)
12
- def initialize(attribute_names, &block)
13
- super
14
-
15
- model_reset_method = @model_reset_method
16
-
17
- %i[changes_applied clear_changes_information].each do |method|
18
- define_method method do
19
- super()
20
- instance_eval(&model_reset_method)
21
- end
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/arel"
3
-
4
- module Mobility
5
- =begin
6
-
7
- Module loading ActiveRecord-specific classes for Mobility models.
8
-
9
- =end
10
- module ActiveRecord
11
- require "mobility/active_record/uniqueness_validator"
12
-
13
- def self.included(model_class)
14
- model_class.class_eval do
15
- unless const_defined?(:UniquenessValidator)
16
- const_set(:UniquenessValidator,
17
- Class.new(::Mobility::ActiveRecord::UniquenessValidator))
18
- end
19
- delegate :translated_attribute_names, to: :class
20
- end
21
- end
22
- end
23
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/active_model/backend_resetter"
3
-
4
- module Mobility
5
- module ActiveRecord
6
- =begin
7
-
8
- Backend resetter for ActiveRecord models. Adds hook on +reload+ event to
9
- {Mobility::ActiveModel::BackendResetter}.
10
-
11
- =end
12
- class BackendResetter < Mobility::ActiveModel::BackendResetter
13
-
14
- # (see Mobility::BackendResetter#initialize)
15
- def initialize(attribute_names, &block)
16
- super
17
-
18
- model_reset_method = @model_reset_method
19
-
20
- define_method :reload do |*args|
21
- super(*args).tap { instance_eval(&model_reset_method) }
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -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