mobility 0.6.0 → 0.7.0

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 (66) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +3 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +39 -1
  5. data/Gemfile.lock +65 -10
  6. data/README.md +63 -27
  7. data/lib/mobility.rb +16 -31
  8. data/lib/mobility/active_record.rb +2 -12
  9. data/lib/mobility/active_record/uniqueness_validator.rb +9 -8
  10. data/lib/mobility/arel.rb +20 -0
  11. data/lib/mobility/arel/nodes.rb +16 -0
  12. data/lib/mobility/arel/nodes/pg_ops.rb +136 -0
  13. data/lib/mobility/arel/visitor.rb +61 -0
  14. data/lib/mobility/attributes.rb +82 -19
  15. data/lib/mobility/backend.rb +53 -8
  16. data/lib/mobility/backend_resetter.rb +2 -1
  17. data/lib/mobility/backends/active_record.rb +31 -11
  18. data/lib/mobility/backends/active_record/column.rb +7 -3
  19. data/lib/mobility/backends/active_record/container.rb +23 -21
  20. data/lib/mobility/backends/active_record/hstore.rb +11 -6
  21. data/lib/mobility/backends/active_record/json.rb +22 -16
  22. data/lib/mobility/backends/active_record/jsonb.rb +22 -16
  23. data/lib/mobility/backends/active_record/key_value.rb +123 -15
  24. data/lib/mobility/backends/active_record/pg_hash.rb +1 -2
  25. data/lib/mobility/backends/active_record/serialized.rb +7 -6
  26. data/lib/mobility/backends/active_record/table.rb +145 -24
  27. data/lib/mobility/backends/hash_valued.rb +15 -10
  28. data/lib/mobility/backends/key_value.rb +12 -12
  29. data/lib/mobility/backends/sequel/container.rb +3 -9
  30. data/lib/mobility/backends/sequel/hstore.rb +2 -2
  31. data/lib/mobility/backends/sequel/json.rb +15 -15
  32. data/lib/mobility/backends/sequel/jsonb.rb +14 -14
  33. data/lib/mobility/backends/sequel/key_value.rb +0 -11
  34. data/lib/mobility/backends/sequel/pg_hash.rb +2 -3
  35. data/lib/mobility/backends/sequel/pg_query_methods.rb +1 -1
  36. data/lib/mobility/backends/sequel/query_methods.rb +3 -3
  37. data/lib/mobility/backends/sequel/serialized.rb +2 -2
  38. data/lib/mobility/backends/sequel/table.rb +10 -11
  39. data/lib/mobility/backends/table.rb +17 -8
  40. data/lib/mobility/configuration.rb +4 -1
  41. data/lib/mobility/interface.rb +0 -0
  42. data/lib/mobility/plugins.rb +1 -0
  43. data/lib/mobility/plugins/active_record/query.rb +192 -0
  44. data/lib/mobility/plugins/cache.rb +1 -2
  45. data/lib/mobility/plugins/default.rb +28 -14
  46. data/lib/mobility/plugins/fallbacks.rb +1 -1
  47. data/lib/mobility/plugins/locale_accessors.rb +13 -9
  48. data/lib/mobility/plugins/presence.rb +15 -7
  49. data/lib/mobility/plugins/query.rb +28 -0
  50. data/lib/mobility/translates.rb +9 -9
  51. data/lib/mobility/version.rb +1 -1
  52. data/lib/rails/generators/mobility/templates/initializer.rb +1 -0
  53. metadata +10 -15
  54. metadata.gz.sig +0 -0
  55. data/lib/mobility/accumulator.rb +0 -33
  56. data/lib/mobility/adapter.rb +0 -20
  57. data/lib/mobility/backends/active_record/column/query_methods.rb +0 -42
  58. data/lib/mobility/backends/active_record/container/json_query_methods.rb +0 -36
  59. data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +0 -33
  60. data/lib/mobility/backends/active_record/hstore/query_methods.rb +0 -25
  61. data/lib/mobility/backends/active_record/json/query_methods.rb +0 -30
  62. data/lib/mobility/backends/active_record/jsonb/query_methods.rb +0 -26
  63. data/lib/mobility/backends/active_record/key_value/query_methods.rb +0 -76
  64. data/lib/mobility/backends/active_record/pg_query_methods.rb +0 -154
  65. data/lib/mobility/backends/active_record/serialized/query_methods.rb +0 -34
  66. data/lib/mobility/backends/active_record/table/query_methods.rb +0 -105
@@ -25,13 +25,12 @@ To use the validator, you must +extend Mobility+ before calling +validates+
25
25
  def validate_each(record, attribute, value)
26
26
  klass = record.class
27
27
 
28
- if (([*options[:scope]] + [attribute]).map(&:to_s) & klass.translated_attribute_names).present?
29
- warn %{
30
- WARNING: The Mobility uniqueness validator for translated attributes does not
31
- support case-insensitive validation. This option will be ignored for: #{attribute}
32
- } if options[:case_sensitive] == false
28
+ if (([*options[:scope]] + [attribute]).map(&:to_s) & klass.mobility_attributes).present?
33
29
  return unless value.present?
34
- relation = klass.send(Mobility.query_method).where(attribute => value)
30
+ relation = klass.__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
35
34
  relation = relation.where.not(klass.primary_key => record.id) if record.persisted?
36
35
  relation = mobility_scope_relation(record, relation)
37
36
  relation = relation.merge(options[:conditions]) if options[:conditions]
@@ -50,8 +49,10 @@ support case-insensitive validation. This option will be ignored for: #{attribut
50
49
  private
51
50
 
52
51
  def mobility_scope_relation(record, relation)
53
- [*options[:scope]].inject(relation) do |scoped_relation, scope_item|
54
- scoped_relation.where(scope_item => record.send(scope_item))
52
+ [*options[:scope]].inject(relation.unscoped) 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
55
56
  end
56
57
  end
57
58
  end
@@ -0,0 +1,20 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/arel/nodes"
3
+ require "mobility/arel/visitor"
4
+
5
+ module Mobility
6
+ module Arel
7
+ class Attribute < ::Arel::Attributes::Attribute
8
+ attr_reader :backend_class
9
+ attr_reader :locale
10
+ attr_reader :attribute_name
11
+
12
+ def initialize(relation, column_name, locale, backend_class, attribute_name: nil)
13
+ @backend_class = backend_class
14
+ @locale = locale
15
+ @attribute_name = attribute_name || column_name
16
+ super(relation, column_name)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Arel
4
+ module Nodes
5
+ class Unary < ::Arel::Nodes::Unary; end
6
+ class Binary < ::Arel::Nodes::Binary; end
7
+ class Grouping < ::Arel::Nodes::Grouping; end
8
+ class Equality < ::Arel::Nodes::Equality; end
9
+
10
+ ::Arel::Visitors::ToSql.class_eval do
11
+ alias :visit_Mobility_Arel_Nodes_Equality :visit_Arel_Nodes_Equality
12
+ alias :visit_Mobility_Arel_Nodes_Grouping :visit_Arel_Nodes_Grouping
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,136 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/arel"
3
+
4
+ module Mobility
5
+ module Arel
6
+ module Nodes
7
+ %w[
8
+ JsonDashArrow
9
+ JsonDashDoubleArrow
10
+ JsonbDashArrow
11
+ JsonbDashDoubleArrow
12
+ JsonbQuestion
13
+ HstoreDashArrow
14
+ HstoreQuestion
15
+ ].each do |name|
16
+ const_set name, (Class.new(Binary) do
17
+ include ::Arel::Expressions
18
+ include ::Arel::Predications
19
+ include ::Arel::OrderPredications
20
+ include ::Arel::AliasPredication
21
+
22
+ def eq other
23
+ Equality.new self, quoted_node(other)
24
+ end
25
+
26
+ def lower
27
+ super self
28
+ end
29
+ end)
30
+ end
31
+
32
+ # Needed for AR 4.2, can be removed when support is deprecated
33
+ if ::ActiveRecord::VERSION::STRING < '5.0'
34
+ [JsonbDashDoubleArrow, HstoreDashArrow].each do |klass|
35
+ klass.class_eval do
36
+ def quoted_node other
37
+ other && super
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class Jsonb < JsonbDashDoubleArrow
44
+ def to_dash_arrow
45
+ JsonbDashArrow.new left, right
46
+ end
47
+
48
+ def to_question
49
+ JsonbQuestion.new left, right
50
+ end
51
+
52
+ def eq other
53
+ case other
54
+ when NilClass
55
+ to_question.not
56
+ when Integer, Array, Hash
57
+ to_dash_arrow.eq other.to_json
58
+ when Jsonb
59
+ to_dash_arrow.eq other.to_dash_arrow
60
+ when JsonbDashArrow
61
+ to_dash_arrow.eq other
62
+ else
63
+ super
64
+ end
65
+ end
66
+ end
67
+
68
+ class Hstore < HstoreDashArrow
69
+ def to_question
70
+ HstoreQuestion.new left, right
71
+ end
72
+
73
+ def eq other
74
+ other.nil? ? to_question.not : super
75
+ end
76
+ end
77
+
78
+ class Json < JsonDashDoubleArrow; end
79
+
80
+ class JsonContainer < Json
81
+ def initialize column, locale, attr
82
+ super(Arel::Nodes::JsonDashArrow.new(column, locale), attr)
83
+ end
84
+ end
85
+
86
+ class JsonbContainer < Jsonb
87
+ def initialize column, locale, attr
88
+ @column, @locale = column, locale
89
+ super(JsonbDashArrow.new(column, locale), attr)
90
+ end
91
+
92
+ def eq other
93
+ other.nil? ? super.or(JsonbQuestion.new(@column, @locale).not) : super
94
+ end
95
+ end
96
+ end
97
+
98
+ module Visitors
99
+ def visit_Mobility_Arel_Nodes_JsonDashArrow o, a
100
+ json_infix o, a, '->'
101
+ end
102
+
103
+ def visit_Mobility_Arel_Nodes_JsonDashDoubleArrow o, a
104
+ json_infix o, a, '->>'
105
+ end
106
+
107
+ def visit_Mobility_Arel_Nodes_JsonbDashArrow o, a
108
+ json_infix o, a, '->'
109
+ end
110
+
111
+ def visit_Mobility_Arel_Nodes_JsonbDashDoubleArrow o, a
112
+ json_infix o, a, '->>'
113
+ end
114
+
115
+ def visit_Mobility_Arel_Nodes_JsonbQuestion o, a
116
+ json_infix o, a, '?'
117
+ end
118
+
119
+ def visit_Mobility_Arel_Nodes_HstoreDashArrow o, a
120
+ json_infix o, a, '->'
121
+ end
122
+
123
+ def visit_Mobility_Arel_Nodes_HstoreQuestion o, a
124
+ json_infix o, a, '?'
125
+ end
126
+
127
+ private
128
+
129
+ def json_infix o, a, opr
130
+ visit(Nodes::Grouping.new(::Arel::Nodes::InfixOperation.new(opr, o.left, o.right)), a)
131
+ end
132
+ end
133
+
134
+ ::Arel::Visitors::PostgreSQL.include Visitors
135
+ end
136
+ end
@@ -0,0 +1,61 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Arel
4
+ class Visitor < ::Arel::Visitors::Visitor
5
+ INNER_JOIN = ::Arel::Nodes::InnerJoin
6
+ OUTER_JOIN = ::Arel::Nodes::OuterJoin
7
+
8
+ attr_reader :backend_class, :locale
9
+
10
+ def initialize(backend_class, locale)
11
+ super()
12
+ @backend_class, @locale = backend_class, locale
13
+ end
14
+
15
+ private
16
+
17
+ def visit(object)
18
+ super
19
+ rescue TypeError
20
+ visit_default(object)
21
+ end
22
+
23
+ def visit_collection(_objects)
24
+ raise NotImplementedError
25
+ end
26
+ alias :visit_Array :visit_collection
27
+
28
+ def visit_Arel_Nodes_Unary(object)
29
+ visit(object.expr)
30
+ end
31
+
32
+ def visit_Arel_Nodes_Binary(object)
33
+ visit_collection([object.left, object.right])
34
+ end
35
+
36
+ def visit_Arel_Nodes_Function(object)
37
+ visit_collection(object.expressions)
38
+ end
39
+
40
+ def visit_Arel_Nodes_Case(object)
41
+ visit_collection([object.case, object.conditions, object.default])
42
+ end
43
+
44
+ def visit_Arel_Nodes_And(object)
45
+ visit_Array(object.children)
46
+ end
47
+
48
+ def visit_Arel_Nodes_Node(object)
49
+ visit_default(object)
50
+ end
51
+
52
+ def visit_Arel_Attributes_Attribute(object)
53
+ visit_default(object)
54
+ end
55
+
56
+ def visit_default(_object)
57
+ nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -133,20 +133,18 @@ with other backends.
133
133
  raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
134
134
  @method = method
135
135
  @options = Mobility.default_options.to_h.merge(backend_options)
136
- @names = attribute_names.map(&:to_s)
137
- raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
136
+ @names = attribute_names.map(&:to_s).freeze
137
+ raise BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
138
138
  @backend_name = backend
139
139
  end
140
140
 
141
- # Setup backend class, include modules into model class, add this
142
- # attributes module to shared {Mobility::Accumulator} and setup model with
143
- # backend setup block (see {Mobility::Backend::Setup#setup_model}).
141
+ # Setup backend class, include modules into model class, include/extend
142
+ # shared modules and setup model with backend setup block (see
143
+ # {Mobility::Backend::Setup#setup_model}).
144
144
  # @param klass [Class] Class of model
145
145
  def included(klass)
146
146
  @model_class = @options[:model_class] = klass
147
- @backend_class = Class.new(get_backend_class(backend_name).for(model_class))
148
-
149
- @backend_class.configure(options) if @backend_class.respond_to?(:configure)
147
+ @backend_class = get_backend_class(backend_name).for(model_class).with_options(options)
150
148
 
151
149
  Mobility.plugins.each do |name|
152
150
  plugin = get_plugin_class(name)
@@ -159,8 +157,10 @@ with other backends.
159
157
  define_writer(name) if %i[accessor writer].include?(method)
160
158
  end
161
159
 
162
- model_class.mobility << self
163
- backend_class.setup_model(model_class, names, options)
160
+ klass.include InstanceMethods
161
+ klass.extend ClassMethods
162
+
163
+ backend_class.setup_model(model_class, names)
164
164
  end
165
165
 
166
166
  # Yield each attribute name to block
@@ -169,6 +169,8 @@ with other backends.
169
169
  names.each(&block)
170
170
  end
171
171
 
172
+ # Show useful information about this module.
173
+ # @return [String]
172
174
  def inspect
173
175
  "#<Attributes (#{backend_name}) @names=#{names.join(", ")}>"
174
176
  end
@@ -176,37 +178,35 @@ with other backends.
176
178
  private
177
179
 
178
180
  def define_backend(attribute)
179
- backend_class_, options_ = backend_class, options
180
- define_method Backend.method_name(attribute) do
181
- @mobility_backends ||= {}
182
- @mobility_backends[attribute] ||= backend_class_.new(self, attribute, options_)
181
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
182
+ def #{Backend.method_name(attribute)}
183
+ mobility_backends[:#{attribute}]
183
184
  end
185
+ EOM
184
186
  end
185
187
 
186
188
  def define_reader(attribute)
187
- backend = Backend.method_name(attribute)
188
189
  class_eval <<-EOM, __FILE__, __LINE__ + 1
189
190
  def #{attribute}(**options)
190
191
  return super() if options.delete(:super)
191
192
  #{set_locale_from_options_inline}
192
- #{backend}.read(locale, options)
193
+ mobility_backends[:#{attribute}].read(locale, options)
193
194
  end
194
195
 
195
196
  def #{attribute}?(**options)
196
197
  return super() if options.delete(:super)
197
198
  #{set_locale_from_options_inline}
198
- #{backend}.present?(locale, options)
199
+ mobility_backends[:#{attribute}].present?(locale, options)
199
200
  end
200
201
  EOM
201
202
  end
202
203
 
203
204
  def define_writer(attribute)
204
- backend = Backend.method_name(attribute)
205
205
  class_eval <<-EOM, __FILE__, __LINE__ + 1
206
206
  def #{attribute}=(value, **options)
207
207
  return super(value) if options.delete(:super)
208
208
  #{set_locale_from_options_inline}
209
- #{backend}.write(locale, value, options)
209
+ mobility_backends[:#{attribute}].write(locale, value, options)
210
210
  end
211
211
  EOM
212
212
  end
@@ -240,5 +240,68 @@ EOL
240
240
  klass_name = key.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
241
241
  parent_class.const_get(klass_name)
242
242
  end
243
+
244
+ module InstanceMethods
245
+ # Return a new backend for an attribute name.
246
+ # @return [Hash] Hash of attribute names and backend instances
247
+ def mobility_backends
248
+ @mobility_backends ||= Hash.new do |hash, name|
249
+ next hash[name.to_sym] if String === name
250
+ hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
251
+ end
252
+ end
253
+
254
+ def initialize_dup(other)
255
+ @mobility_backends = nil
256
+ super
257
+ end
258
+ end
259
+
260
+ module ClassMethods
261
+ # Return all {Mobility::Attribute} module instances from among ancestors
262
+ # of this model.
263
+ # @return [Array<Mobility::Attributes>] Attribute modules
264
+ def mobility_modules
265
+ ancestors.select { |mod| Attributes === mod }
266
+ end
267
+
268
+ # Return translated attribute names on this model.
269
+ # @return [Array<String>] Attribute names
270
+ def mobility_attributes
271
+ mobility_modules.map(&:names).flatten
272
+ end
273
+
274
+ # @!method translated_attribute_names
275
+ # @return (see #mobility_attributes)
276
+ alias translated_attribute_names mobility_attributes
277
+
278
+ # Return backend class for a given attribute name.
279
+ # @param [Symbol,String] Name of attribute
280
+ # @return [Class] Backend class
281
+ def mobility_backend_class(name)
282
+ @backends ||= BackendsCache.new(self)
283
+ @backends[name.to_sym]
284
+ end
285
+
286
+ class BackendsCache < Hash
287
+ def initialize(klass)
288
+ # Preload backend mapping
289
+ klass.mobility_modules.each do |mod|
290
+ mod.names.each { |name| self[name.to_sym] = mod.backend_class }
291
+ end
292
+
293
+ super() do |hash, name|
294
+ if mod = klass.mobility_modules.find { |m| m.names.include? name.to_s }
295
+ hash[name] = mod.backend_class
296
+ else
297
+ raise KeyError, "No backend for: #{name}."
298
+ end
299
+ end
300
+ end
301
+ end
302
+ private_constant :BackendsCache
303
+ end
243
304
  end
305
+
306
+ class BackendRequired < ArgumentError; end
244
307
  end
@@ -66,7 +66,7 @@ On top of this, a backend will normally:
66
66
  # @!macro [new] backend_constructor
67
67
  # @param model Model on which backend is defined
68
68
  # @param [String] attribute Backend attribute
69
- def initialize(model, attribute, **_)
69
+ def initialize(model, attribute)
70
70
  @model = model
71
71
  @attribute = attribute
72
72
  end
@@ -106,10 +106,22 @@ On top of this, a backend will normally:
106
106
  Util.present?(read(locale, options))
107
107
  end
108
108
 
109
- # Extend included class with +setup+ method
109
+ # @!method model_class
110
+ # Returns name of model in which backend is used.
111
+ # @return [Class] Model class
112
+
113
+ # @return [Hash] options
114
+ def options
115
+ self.class.options
116
+ end
117
+
118
+ # Extend included class with +setup+ method and other class methods
110
119
  def self.included(base)
111
- base.extend(Setup)
112
- base.extend(ClassMethods)
120
+ base.extend ClassMethods
121
+ def base.options
122
+ @options
123
+ end
124
+ base.option_reader :model_class
113
125
  end
114
126
 
115
127
  # @param [String] attribute
@@ -120,7 +132,7 @@ On top of this, a backend will normally:
120
132
  end
121
133
 
122
134
  # Defines setup hooks for backend to customize model class.
123
- module Setup
135
+ module ClassMethods
124
136
  # Assign block to be called on model class.
125
137
  # @yield [attribute_names, options]
126
138
  # @note When called multiple times, setup blocks will be appended
@@ -139,19 +151,46 @@ On top of this, a backend will normally:
139
151
 
140
152
  def inherited(subclass)
141
153
  subclass.instance_variable_set(:@setup_block, @setup_block)
154
+ subclass.instance_variable_set(:@options, @options)
142
155
  end
143
156
 
144
157
  # Call setup block on a class with attributes and options.
145
158
  # @param model_class Class to be setup-ed
146
159
  # @param [Array<String>] attribute_names
147
160
  # @param [Hash] options
148
- def setup_model(model_class, attribute_names, **options)
161
+ def setup_model(model_class, attribute_names)
149
162
  return unless setup_block = @setup_block
150
163
  model_class.class_exec(attribute_names, options, &setup_block)
151
164
  end
152
- end
153
165
 
154
- module ClassMethods
166
+ # Build a subclass of this backend class for a given set of options
167
+ # @note This method also freezes the options hash to prevent it from
168
+ # being changed.
169
+ # @param [Hash] options
170
+ # @return [Class] backend subclass
171
+ def with_options(options = {}, &block)
172
+ configure(options) if respond_to?(:configure)
173
+ options.freeze
174
+ Class.new(self) do
175
+ @options = options
176
+ class_eval(&block) if block_given?
177
+ end
178
+ end
179
+
180
+ # Create instance and class methods to access value on options hash
181
+ # @param [Symbol] name Name of option reader
182
+ def option_reader(name)
183
+ module_eval <<-EOM, __FILE__, __LINE__ + 1
184
+ def self.#{name}
185
+ options[:#{name}]
186
+ end
187
+
188
+ def #{name}
189
+ self.class.options[:#{name}]
190
+ end
191
+ EOM
192
+ end
193
+
155
194
  # {Attributes} uses this method to get a backend class specific to the
156
195
  # model using the backend. Backend classes can override this method to
157
196
  # return a class specific to the model class using the backend (e.g.
@@ -172,6 +211,12 @@ On top of this, a backend will normally:
172
211
  def apply_plugin(_)
173
212
  false
174
213
  end
214
+
215
+ # Show useful information about this backend class, if it has no name.
216
+ # @return [String]
217
+ def inspect
218
+ name ? super : "#<#{superclass.name}>"
219
+ end
175
220
  end
176
221
 
177
222
  Translation = Struct.new(:backend, :locale) do