mobility 1.0.0.beta2 → 1.0.0.rc1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +3 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +5 -0
  5. data/Gemfile.lock +19 -0
  6. data/README.md +4 -4
  7. data/lib/mobility.rb +60 -6
  8. data/lib/mobility/backends/active_record.rb +1 -1
  9. data/lib/mobility/backends/active_record/column.rb +1 -1
  10. data/lib/mobility/backends/active_record/container.rb +4 -4
  11. data/lib/mobility/backends/active_record/hstore.rb +3 -3
  12. data/lib/mobility/backends/active_record/json.rb +3 -3
  13. data/lib/mobility/backends/active_record/jsonb.rb +3 -3
  14. data/lib/mobility/backends/active_record/key_value.rb +27 -11
  15. data/lib/mobility/backends/active_record/table.rb +11 -6
  16. data/lib/mobility/backends/sequel.rb +32 -0
  17. data/lib/mobility/backends/sequel/container.rb +5 -3
  18. data/lib/mobility/backends/sequel/key_value.rb +79 -12
  19. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  20. data/lib/mobility/backends/sequel/table.rb +18 -8
  21. data/lib/mobility/backends/table.rb +11 -6
  22. data/lib/mobility/plugins/active_record.rb +3 -0
  23. data/lib/mobility/plugins/active_record/backend.rb +2 -0
  24. data/lib/mobility/plugins/active_record/query.rb +2 -2
  25. data/lib/mobility/plugins/arel.rb +125 -0
  26. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  27. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  28. data/lib/mobility/plugins/sequel/dirty.rb +1 -1
  29. data/lib/mobility/version.rb +1 -1
  30. metadata +5 -17
  31. metadata.gz.sig +0 -0
  32. data/lib/mobility/active_record/model_translation.rb +0 -14
  33. data/lib/mobility/active_record/string_translation.rb +0 -10
  34. data/lib/mobility/active_record/text_translation.rb +0 -10
  35. data/lib/mobility/active_record/translation.rb +0 -14
  36. data/lib/mobility/arel.rb +0 -49
  37. data/lib/mobility/arel/nodes.rb +0 -13
  38. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  39. data/lib/mobility/arel/visitor.rb +0 -61
  40. data/lib/mobility/sequel/column_changes.rb +0 -28
  41. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  42. data/lib/mobility/sequel/model_translation.rb +0 -20
  43. data/lib/mobility/sequel/sql.rb +0 -16
  44. data/lib/mobility/sequel/string_translation.rb +0 -10
  45. data/lib/mobility/sequel/text_translation.rb +0 -10
  46. data/lib/mobility/sequel/translation.rb +0 -53
@@ -30,6 +30,38 @@ module Mobility
30
30
  def prepare_dataset(dataset, _predicate, _locale)
31
31
  dataset
32
32
  end
33
+
34
+ # Forces Sequel to notice changes when Mobility setter method is
35
+ # called.
36
+ # TODO: Find a better way to do this.
37
+ def define_column_changes(mod, attributes, column_affix: "%s")
38
+ mod.class_eval do
39
+ attributes.each do |attribute|
40
+ define_method "#{attribute}=" do |value, **options|
41
+ if !options[:super] && send(attribute) != value
42
+ locale = options[:locale] || Mobility.locale
43
+ column = (column_affix % attribute).to_sym
44
+ attribute_with_locale = :"#{attribute}_#{Mobility.normalize_locale(locale)}"
45
+ @changed_columns = changed_columns | [column, attribute.to_sym, attribute_with_locale]
46
+ end
47
+ super(value, **options)
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # Initialize column value(s) by default to a hash.
54
+ # TODO: Find a better way to do this.
55
+ def define_hash_initializer(mod, columns)
56
+ mod.class_eval do
57
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
58
+ def initialize_set(values)
59
+ #{columns.map { |c| "self[:#{c}] = {}" }.join(';')}
60
+ super
61
+ end
62
+ EOM
63
+ end
64
+ end
33
65
  end
34
66
  end
35
67
  end
@@ -57,9 +57,11 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
57
57
  end
58
58
  end
59
59
 
60
+ backend = self
61
+
60
62
  setup do |attributes, options|
61
63
  column_name = options[:column_name]
62
- before_validation = Module.new do
64
+ mod = Module.new do
63
65
  define_method :before_validation do
64
66
  self[column_name].each do |k, v|
65
67
  v.delete_if { |_locale, translation| Util.blank?(translation) }
@@ -68,8 +70,8 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
68
70
  super()
69
71
  end
70
72
  end
71
- include before_validation
72
- include Mobility::Sequel::HashInitializer.new(column_name)
73
+ include mod
74
+ backend.define_hash_initializer(mod, [column_name])
73
75
 
74
76
  plugin :defaults_setter
75
77
  attributes.each { |attribute| default_values[attribute.to_sym] = {} }
@@ -2,11 +2,6 @@
2
2
  require "mobility/util"
3
3
  require "mobility/backends/sequel"
4
4
  require "mobility/backends/key_value"
5
- require "mobility/sequel/column_changes"
6
- require "mobility/sequel/hash_initializer"
7
- require "mobility/sequel/string_translation"
8
- require "mobility/sequel/text_translation"
9
- require "mobility/sequel/sql"
10
5
 
11
6
  module Mobility
12
7
  module Backends
@@ -34,7 +29,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
34
29
  super
35
30
  if type = options[:type]
36
31
  options[:association_name] ||= :"#{options[:type]}_translations"
37
- options[:class_name] ||= Mobility::Sequel.const_get("#{type.capitalize}Translation")
32
+ options[:class_name] ||= const_get("#{type.capitalize}Translation")
38
33
  end
39
34
  options[:table_alias_affix] = "#{model_class}_%s_#{options[:association_name]}"
40
35
  rescue NameError
@@ -43,7 +38,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
43
38
  # @!endgroup
44
39
 
45
40
  def build_op(attr, locale)
46
- ::Mobility::Sequel::SQL::QualifiedIdentifier.new(table_alias(attr, locale), :value, locale, self, attribute_name: attr)
41
+ QualifiedIdentifier.new(table_alias(attr, locale), :value, locale, self, attr)
47
42
  end
48
43
 
49
44
  # @param [Sequel::Dataset] dataset Dataset to prepare
@@ -75,7 +70,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
75
70
  case predicate
76
71
  when Array
77
72
  visit_collection(predicate, locale)
78
- when ::Mobility::Sequel::SQL::QualifiedIdentifier
73
+ when QualifiedIdentifier
79
74
  visit_sql_identifier(predicate, locale)
80
75
  when ::Sequel::SQL::BooleanExpression
81
76
  visit_boolean(predicate, locale)
@@ -128,6 +123,8 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
128
123
  end
129
124
  end
130
125
 
126
+ backend = self
127
+
131
128
  setup do |attributes, options|
132
129
  association_name = options[:association_name]
133
130
  translations_class = options[:class_name]
@@ -159,13 +156,14 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
159
156
  include callback_methods
160
157
 
161
158
  include DestroyKeyValueTranslations
162
- include Mobility::Sequel::ColumnChanges.new(attributes)
159
+ include(mod = Module.new)
160
+ backend.define_column_changes(mod, attributes)
163
161
  end
164
162
 
165
163
  # Returns translation for a given locale, or initializes one if none is present.
166
164
  # @param [Symbol] locale
167
- # @return [Mobility::Sequel::TextTranslation,Mobility::Sequel::StringTranslation]
168
- def translation_for(locale, _)
165
+ # @return [Mobility::Backends::Sequel::KeyValue::TextTranslation,Mobility::Backends::Sequel::KeyValue::StringTranslation]
166
+ def translation_for(locale, **)
169
167
  translation = model.send(association_name).find { |t| t.key == attribute && t.locale == locale.to_s }
170
168
  translation ||= class_name.new(locale: locale, key: attribute)
171
169
  translation
@@ -184,7 +182,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
184
182
  def after_destroy
185
183
  super
186
184
  [:string, :text].freeze.each do |type|
187
- Mobility::Sequel.const_get("#{type.capitalize}Translation").
185
+ Mobility::Backends::Sequel::KeyValue.const_get("#{type.capitalize}Translation").
188
186
  where(translatable_id: id, translatable_type: self.class.name).destroy
189
187
  end
190
188
  end
@@ -201,6 +199,75 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
201
199
  (model.send(association_name) + cache.values).uniq
202
200
  end
203
201
  end
202
+
203
+ class QualifiedIdentifier < ::Sequel::SQL::QualifiedIdentifier
204
+ attr_reader :backend_class, :locale, :attribute_name
205
+
206
+ def initialize(table, column, locale, backend_class, attribute_name)
207
+ @backend_class = backend_class
208
+ @locale = locale
209
+ @attribute_name = attribute_name || column
210
+ super(table, column)
211
+ end
212
+ end
213
+
214
+ module Translation
215
+ def self.included(base)
216
+ base.class_eval do
217
+ plugin :validation_helpers
218
+
219
+ # Paraphased from sequel_polymorphic gem
220
+ #
221
+ model = underscore(self.to_s)
222
+ plural_model = pluralize(model)
223
+ many_to_one :translatable,
224
+ reciprocal: plural_model.to_sym,
225
+ reciprocal_type: :many_to_one,
226
+ setter: (proc do |able_instance|
227
+ self[:translatable_id] = (able_instance.pk if able_instance)
228
+ self[:translatable_type] = (able_instance.class.name if able_instance)
229
+ end),
230
+ dataset: (proc do
231
+ translatable_type = send :translatable_type
232
+ translatable_id = send :translatable_id
233
+ return if translatable_type.nil? || translatable_id.nil?
234
+ klass = self.class.send(:constantize, translatable_type)
235
+ klass.where(klass.primary_key => translatable_id)
236
+ end),
237
+ eager_loader: (proc do |eo|
238
+ id_map = {}
239
+ eo[:rows].each do |model|
240
+ model_able_type = model.send :translatable_type
241
+ model_able_id = model.send :translatable_id
242
+ model.associations[:translatable] = nil
243
+ ((id_map[model_able_type] ||= {})[model_able_id] ||= []) << model if !model_able_type.nil? && !model_able_id.nil?
244
+ end
245
+ id_map.each do |klass_name, id_map|
246
+ klass = constantize(camelize(klass_name))
247
+ klass.where(klass.primary_key=>id_map.keys).all do |related_obj|
248
+ id_map[related_obj.pk].each do |model|
249
+ model.associations[:translatable] = related_obj
250
+ end
251
+ end
252
+ end
253
+ end)
254
+
255
+ def validate
256
+ super
257
+ validates_presence [:locale, :key, :translatable_id, :translatable_type]
258
+ validates_unique [:locale, :key, :translatable_id, :translatable_type]
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ class TextTranslation < ::Sequel::Model(:mobility_text_translations)
265
+ include Translation
266
+ end
267
+
268
+ class StringTranslation < ::Sequel::Model(:mobility_string_translations)
269
+ include Translation
270
+ end
204
271
  end
205
272
 
206
273
  register_backend(:sequel_key_value, Sequel::KeyValue)
@@ -2,8 +2,6 @@
2
2
  require "mobility/util"
3
3
  require "mobility/backends/sequel"
4
4
  require "mobility/backends/hash_valued"
5
- require "mobility/sequel/column_changes"
6
- require "mobility/sequel/hash_initializer"
7
5
 
8
6
  module Mobility
9
7
  module Backends
@@ -35,10 +33,12 @@ jsonb).
35
33
  model[column_name.to_sym]
36
34
  end
37
35
 
36
+ backend = self
37
+
38
38
  setup do |attributes, options|
39
39
  columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym }
40
40
 
41
- before_validation = Module.new do
41
+ mod = Module.new do
42
42
  define_method :before_validation do
43
43
  columns.each do |column|
44
44
  self[column].delete_if { |_, v| Util.blank?(v) }
@@ -46,9 +46,9 @@ jsonb).
46
46
  super()
47
47
  end
48
48
  end
49
- include before_validation
50
- include Mobility::Sequel::HashInitializer.new(*columns)
51
- include Mobility::Sequel::ColumnChanges.new(attributes, column_affix: options[:column_affix])
49
+ include mod
50
+ backend.define_hash_initializer(mod, columns)
51
+ backend.define_column_changes(mod, attributes, column_affix: options[:column_affix])
52
52
 
53
53
  plugin :defaults_setter
54
54
  columns.each { |column| default_values[column] = {} }
@@ -2,9 +2,6 @@
2
2
  require "mobility/util"
3
3
  require "mobility/backends/sequel"
4
4
  require "mobility/backends/table"
5
- require "mobility/sequel/column_changes"
6
- require "mobility/sequel/model_translation"
7
- require "mobility/sequel/sql"
8
5
 
9
6
  module Mobility
10
7
  module Backends
@@ -52,7 +49,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
52
49
  # @param [Symbol] locale Locale
53
50
  # @return [Sequel::SQL::QualifiedIdentifier]
54
51
  def build_op(attr, locale)
55
- ::Mobility::Sequel::SQL::QualifiedIdentifier.new(table_alias(locale), attr, locale, self, attribute_name: attr)
52
+ ::Sequel::SQL::QualifiedIdentifier.new(table_alias(locale), attr || :value)
56
53
  end
57
54
 
58
55
  # @param [Sequel::Dataset] dataset Dataset to prepare
@@ -83,7 +80,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
83
80
  case predicate
84
81
  when Array
85
82
  visit_collection(predicate, locale)
86
- when ::Mobility::Sequel::SQL::QualifiedIdentifier
83
+ when ::Sequel::SQL::QualifiedIdentifier
87
84
  visit_sql_identifier(predicate, locale)
88
85
  when ::Sequel::SQL::BooleanExpression
89
86
  visit_boolean(predicate, locale)
@@ -119,6 +116,8 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
119
116
  end
120
117
  end
121
118
 
119
+ backend = self
120
+
122
121
  setup do |attributes, options|
123
122
  association_name = options[:association_name]
124
123
  subclass_name = options[:subclass_name]
@@ -128,7 +127,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
128
127
  const_get(subclass_name, false)
129
128
  else
130
129
  const_set(subclass_name, Class.new(::Sequel::Model(options[:table_name]))).tap do |klass|
131
- klass.include ::Mobility::Sequel::ModelTranslation
130
+ klass.include Translation
132
131
  end
133
132
  end
134
133
 
@@ -155,10 +154,11 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
155
154
  end
156
155
  include callback_methods
157
156
 
158
- include Mobility::Sequel::ColumnChanges.new(attributes)
157
+ include(mod = Module.new)
158
+ backend.define_column_changes(mod, attributes)
159
159
  end
160
160
 
161
- def translation_for(locale, _)
161
+ def translation_for(locale, **)
162
162
  translation = model.send(association_name).find { |t| t.locale == locale.to_s }
163
163
  translation ||= translation_class.new(locale: locale)
164
164
  translation
@@ -174,6 +174,16 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
174
174
  end
175
175
  end
176
176
 
177
+ module Translation
178
+ def self.included(base)
179
+ base.plugin :validation_helpers
180
+ end
181
+
182
+ def validate
183
+ super
184
+ validates_presence [:locale]
185
+ end
186
+ end
177
187
  class CacheRequired < ::StandardError; end
178
188
  end
179
189
 
@@ -10,8 +10,9 @@ Stores attribute translation as rows on a model-specific translation table
10
10
  the table name for a model +Post+ with table +posts+ will be
11
11
  +post_translations+, and the translation class will be +Post::Translation+. The
12
12
  translation class is dynamically created when the backend is initialized on the
13
- model class, and subclasses {Mobility::ActiveRecord::ModelTranslation} (for AR
14
- models) or inherits {Mobility::Sequel::ModelTranslation} (for Sequel models).
13
+ model class, and subclasses
14
+ {Mobility::Backends::ActiveRecord::Table::Translation} (for AR models) or
15
+ inherits {Mobility::Backends::Sequel::Table::Translation} (for Sequel models).
15
16
 
16
17
  The backend expects the translations table (+post_translations+) to have:
17
18
 
@@ -140,17 +141,21 @@ set.
140
141
  end
141
142
 
142
143
  def clear_cache
143
- model_cache && model_cache.clear
144
+ cache.clear
144
145
  end
145
146
 
146
147
  private
147
148
 
148
149
  def cache
149
- model_cache || model.instance_variable_set(:"@__mobility_#{association_name}_cache", {})
150
+ if model.instance_variable_defined?(cache_name)
151
+ model.instance_variable_get(cache_name)
152
+ else
153
+ model.instance_variable_set(cache_name, {})
154
+ end
150
155
  end
151
156
 
152
- def model_cache
153
- model.instance_variable_get(:"@__mobility_#{association_name}_cache")
157
+ def cache_name
158
+ @cache_name ||= :"@__mobility_#{association_name}_cache"
154
159
  end
155
160
  end
156
161
  end
@@ -15,12 +15,15 @@ Plugin for ActiveRecord models.
15
15
  module ActiveRecord
16
16
  extend Plugin
17
17
 
18
+ requires :arel
19
+
18
20
  requires :active_record_backend, include: :after
19
21
  requires :active_record_dirty
20
22
  requires :active_record_cache
21
23
  requires :active_record_query
22
24
  requires :active_record_uniqueness_validation
23
25
 
26
+
24
27
  included_hook do |klass|
25
28
  unless active_record_class?(klass)
26
29
  name = klass.name || klass.to_s
@@ -1,3 +1,5 @@
1
+ # frozen-string-literal: true
2
+
1
3
  module Mobility
2
4
  module Plugins
3
5
  module ActiveRecord
@@ -199,7 +199,7 @@ enabled for any one attribute on the model.
199
199
  # Builds a translated relation for a given opts hash and optional
200
200
  # invert boolean.
201
201
  def _build(scope, opts, locale, invert)
202
- return yield if (mods = attribute_modules(scope)).empty?
202
+ return yield if (mods = translation_modules(scope)).empty?
203
203
 
204
204
  keys, predicates = opts.keys.map(&:to_s), []
205
205
 
@@ -222,7 +222,7 @@ enabled for any one attribute on the model.
222
222
  query_map[relation.where(predicates.inject(:and))]
223
223
  end
224
224
 
225
- def attribute_modules(scope)
225
+ def translation_modules(scope)
226
226
  scope.model.ancestors.grep(::Mobility::Translations)
227
227
  end
228
228
 
@@ -0,0 +1,125 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ =begin
6
+
7
+ Plugin for Mobility Arel customizations. Basically used as a namespace to store
8
+ Arel-specific classes and modules.
9
+
10
+ =end
11
+ module Arel
12
+ extend Plugin
13
+
14
+ module MobilityExpressions
15
+ include ::Arel::Expressions
16
+
17
+ # @note This is necessary in order to ensure that when a translated
18
+ # attribute is selected with an alias using +AS+, the resulting
19
+ # expression can still be counted without blowing up.
20
+ #
21
+ # Extending +::Arel::Expressions+ is necessary to convince ActiveRecord
22
+ # that this node should not be stringified, which otherwise would
23
+ # result in garbage SQL.
24
+ #
25
+ # @see https://github.com/rails/rails/blob/847342c25c61acaea988430dc3ab66a82e3ed486/activerecord/lib/active_record/relation/calculations.rb#L261
26
+ def as(*)
27
+ super
28
+ .extend(::Arel::Expressions)
29
+ .extend(Countable)
30
+ end
31
+
32
+ module Countable
33
+ # @note This allows expressions with selected translated attributes to
34
+ # be counted.
35
+ def count(*args)
36
+ left.count(*args)
37
+ end
38
+ end
39
+ end
40
+
41
+ class Attribute < ::Arel::Attributes::Attribute
42
+ include MobilityExpressions
43
+
44
+ attr_reader :backend_class
45
+ attr_reader :locale
46
+ attr_reader :attribute_name
47
+
48
+ def initialize(relation, column_name, locale, backend_class, attribute_name = nil)
49
+ @backend_class = backend_class
50
+ @locale = locale
51
+ @attribute_name = attribute_name || column_name
52
+ super(relation, column_name)
53
+ end
54
+ end
55
+
56
+ class Visitor < ::Arel::Visitors::Visitor
57
+ INNER_JOIN = ::Arel::Nodes::InnerJoin
58
+ OUTER_JOIN = ::Arel::Nodes::OuterJoin
59
+
60
+ attr_reader :backend_class, :locale
61
+
62
+ def initialize(backend_class, locale)
63
+ super()
64
+ @backend_class, @locale = backend_class, locale
65
+ end
66
+
67
+ private
68
+
69
+ def visit(*args)
70
+ super
71
+ rescue TypeError
72
+ visit_default(*args)
73
+ end
74
+
75
+ def visit_collection(_objects)
76
+ raise NotImplementedError
77
+ end
78
+ alias :visit_Array :visit_collection
79
+
80
+ def visit_Arel_Nodes_Unary(object)
81
+ visit(object.expr)
82
+ end
83
+
84
+ def visit_Arel_Nodes_Binary(object)
85
+ visit_collection([object.left, object.right])
86
+ end
87
+
88
+ def visit_Arel_Nodes_Function(object)
89
+ visit_collection(object.expressions)
90
+ end
91
+
92
+ def visit_Arel_Nodes_Case(object)
93
+ visit_collection([object.case, object.conditions, object.default])
94
+ end
95
+
96
+ def visit_Arel_Nodes_And(object)
97
+ visit_Array(object.children)
98
+ end
99
+
100
+ def visit_Arel_Nodes_Node(object)
101
+ visit_default(object)
102
+ end
103
+
104
+ def visit_Arel_Attributes_Attribute(object)
105
+ visit_default(object)
106
+ end
107
+
108
+ def visit_default(_object)
109
+ nil
110
+ end
111
+ end
112
+
113
+ module Nodes
114
+ class Binary < ::Arel::Nodes::Binary; end
115
+ class Grouping < ::Arel::Nodes::Grouping; end
116
+
117
+ ::Arel::Visitors::ToSql.class_eval do
118
+ alias :visit_Mobility_Plugins_Arel_Nodes_Grouping :visit_Arel_Nodes_Grouping
119
+ end
120
+ end
121
+ end
122
+
123
+ register_plugin(:arel, Arel)
124
+ end
125
+ end