mobility 1.0.0.beta2 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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