mobility 1.0.0.beta2 → 1.0.3

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 (52) 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 +49 -4
  5. data/Gemfile +5 -16
  6. data/Gemfile.lock +20 -1
  7. data/README.md +23 -28
  8. data/lib/mobility.rb +62 -8
  9. data/lib/mobility/backends/active_record.rb +1 -1
  10. data/lib/mobility/backends/active_record/column.rb +1 -1
  11. data/lib/mobility/backends/active_record/container.rb +4 -4
  12. data/lib/mobility/backends/active_record/hstore.rb +3 -3
  13. data/lib/mobility/backends/active_record/json.rb +3 -3
  14. data/lib/mobility/backends/active_record/jsonb.rb +3 -3
  15. data/lib/mobility/backends/active_record/key_value.rb +27 -11
  16. data/lib/mobility/backends/active_record/table.rb +11 -6
  17. data/lib/mobility/backends/sequel.rb +32 -0
  18. data/lib/mobility/backends/sequel/container.rb +5 -3
  19. data/lib/mobility/backends/sequel/key_value.rb +79 -12
  20. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  21. data/lib/mobility/backends/sequel/table.rb +18 -8
  22. data/lib/mobility/backends/table.rb +11 -6
  23. data/lib/mobility/plugin.rb +2 -2
  24. data/lib/mobility/plugins/active_record.rb +3 -0
  25. data/lib/mobility/plugins/active_record/backend.rb +2 -0
  26. data/lib/mobility/plugins/active_record/query.rb +7 -7
  27. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +4 -0
  28. data/lib/mobility/plugins/arel.rb +125 -0
  29. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  30. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  31. data/lib/mobility/plugins/sequel/dirty.rb +1 -1
  32. data/lib/mobility/version.rb +2 -2
  33. data/lib/rails/generators/mobility/templates/create_string_translations.rb +0 -1
  34. data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
  35. data/lib/rails/generators/mobility/templates/initializer.rb +9 -1
  36. metadata +14 -20
  37. metadata.gz.sig +0 -0
  38. data/lib/mobility/active_record/model_translation.rb +0 -14
  39. data/lib/mobility/active_record/string_translation.rb +0 -10
  40. data/lib/mobility/active_record/text_translation.rb +0 -10
  41. data/lib/mobility/active_record/translation.rb +0 -14
  42. data/lib/mobility/arel.rb +0 -49
  43. data/lib/mobility/arel/nodes.rb +0 -13
  44. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  45. data/lib/mobility/arel/visitor.rb +0 -61
  46. data/lib/mobility/sequel/column_changes.rb +0 -28
  47. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  48. data/lib/mobility/sequel/model_translation.rb +0 -20
  49. data/lib/mobility/sequel/sql.rb +0 -16
  50. data/lib/mobility/sequel/string_translation.rb +0 -10
  51. data/lib/mobility/sequel/text_translation.rb +0 -10
  52. data/lib/mobility/sequel/translation.rb +0 -53
@@ -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
@@ -121,8 +121,8 @@ Also includes a +configure+ class method to apply plugins to a pluggable
121
121
  # Does this class include all plugins this plugin depends (directly) on?
122
122
  # @param [Class] klass Pluggable class
123
123
  def dependencies_satisfied?(klass)
124
- required_plugins = dependencies.keys.map { |name| Plugins.load_plugin(name) }
125
- (required_plugins - klass.included_modules).none?
124
+ plugin_keys = klass.included_plugins.map { |plugin| Plugins.lookup_name(plugin) }
125
+ (dependencies.keys - plugin_keys).none?
126
126
  end
127
127
 
128
128
  # Specifies a dependency of this plugin.
@@ -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
@@ -67,9 +67,9 @@ enabled for any one attribute on the model.
67
67
  end
68
68
  end
69
69
 
70
- # Internal class to create a "clean room" for manipulating translated
71
- # attribute nodes in an instance-eval'ed block. Inspired by Sequel's
72
- # (much more sophisticated) virtual rows.
70
+ # Creates a "clean room" for manipulating translated attribute nodes in
71
+ # an instance-eval'ed block. Inspired by Sequel's (much more
72
+ # sophisticated) virtual rows.
73
73
  class VirtualRow < BasicObject
74
74
  attr_reader :__backends
75
75
 
@@ -108,7 +108,7 @@ enabled for any one attribute on the model.
108
108
  end
109
109
  end
110
110
  end
111
- private_constant :QueryMethod, :VirtualRow
111
+ private_constant :QueryMethod
112
112
 
113
113
  module QueryExtension
114
114
  def where!(opts, *rest)
@@ -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
 
@@ -273,7 +273,7 @@ enabled for any one attribute on the model.
273
273
  end
274
274
  end
275
275
 
276
- private_constant :QueryExtension, :FindByMethods
276
+ private_constant :FindByMethods
277
277
  end
278
278
 
279
279
  class MissingBackend < Mobility::Error; end
@@ -10,6 +10,10 @@ module Mobility
10
10
  klass.class_eval do
11
11
  unless const_defined?(:UniquenessValidator, false)
12
12
  self.const_set(:UniquenessValidator, Class.new(UniquenessValidator))
13
+
14
+ def self.validates_uniqueness_of(*attr_names)
15
+ validates_with(UniquenessValidator, _merge_attributes(attr_names))
16
+ end
13
17
  end
14
18
  end
15
19
  end
@@ -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
@@ -0,0 +1,15 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ module Arel
5
+ module Nodes
6
+ class Binary < ::Arel::Nodes::Binary; end
7
+ class Grouping < ::Arel::Nodes::Grouping; end
8
+
9
+ ::Arel::Visitors::ToSql.class_eval do
10
+ alias :visit_Mobility_Plugins_Arel_Nodes_Grouping :visit_Arel_Nodes_Grouping
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,134 @@
1
+ # frozen-string-literal: true
2
+ require "mobility/plugins/arel"
3
+
4
+ module Mobility
5
+ module Plugins
6
+ module Arel
7
+ module Nodes
8
+ %w[
9
+ JsonDashArrow
10
+ JsonDashDoubleArrow
11
+ JsonbDashArrow
12
+ JsonbDashDoubleArrow
13
+ JsonbQuestion
14
+ HstoreDashArrow
15
+ HstoreQuestion
16
+ ].each do |name|
17
+ const_set name, (Class.new(Binary) do
18
+ include ::Arel::Predications
19
+ include ::Arel::OrderPredications
20
+ include ::Arel::AliasPredication
21
+ include MobilityExpressions
22
+
23
+ def lower
24
+ super self
25
+ end
26
+ end)
27
+ end
28
+
29
+ # Needed for AR 4.2, can be removed when support is deprecated
30
+ if ::ActiveRecord::VERSION::STRING < '5.0'
31
+ [JsonbDashDoubleArrow, HstoreDashArrow].each do |klass|
32
+ klass.class_eval do
33
+ def quoted_node other
34
+ other && super
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ class Jsonb < JsonbDashDoubleArrow
41
+ def to_dash_arrow
42
+ JsonbDashArrow.new left, right
43
+ end
44
+
45
+ def to_question
46
+ JsonbQuestion.new left, right
47
+ end
48
+
49
+ def eq other
50
+ case other
51
+ when NilClass
52
+ to_question.not
53
+ when Integer, Array, ::Hash
54
+ to_dash_arrow.eq other.to_json
55
+ when Jsonb
56
+ to_dash_arrow.eq other.to_dash_arrow
57
+ when JsonbDashArrow
58
+ to_dash_arrow.eq other
59
+ else
60
+ super
61
+ end
62
+ end
63
+ end
64
+
65
+ class Hstore < HstoreDashArrow
66
+ def to_question
67
+ HstoreQuestion.new left, right
68
+ end
69
+
70
+ def eq other
71
+ other.nil? ? to_question.not : super
72
+ end
73
+ end
74
+
75
+ class Json < JsonDashDoubleArrow; end
76
+
77
+ class JsonContainer < Json
78
+ def initialize column, locale, attr
79
+ super(Nodes::JsonDashArrow.new(column, locale), attr)
80
+ end
81
+ end
82
+
83
+ class JsonbContainer < Jsonb
84
+ def initialize column, locale, attr
85
+ @column, @locale = column, locale
86
+ super(JsonbDashArrow.new(column, locale), attr)
87
+ end
88
+
89
+ def eq other
90
+ other.nil? ? super.or(JsonbQuestion.new(@column, @locale).not) : super
91
+ end
92
+ end
93
+ end
94
+
95
+ module Visitors
96
+ def visit_Mobility_Plugins_Arel_Nodes_JsonDashArrow o, a
97
+ json_infix o, a, '->'
98
+ end
99
+
100
+ def visit_Mobility_Plugins_Arel_Nodes_JsonDashDoubleArrow o, a
101
+ json_infix o, a, '->>'
102
+ end
103
+
104
+ def visit_Mobility_Plugins_Arel_Nodes_JsonbDashArrow o, a
105
+ json_infix o, a, '->'
106
+ end
107
+
108
+ def visit_Mobility_Plugins_Arel_Nodes_JsonbDashDoubleArrow o, a
109
+ json_infix o, a, '->>'
110
+ end
111
+
112
+ def visit_Mobility_Plugins_Arel_Nodes_JsonbQuestion o, a
113
+ json_infix o, a, '?'
114
+ end
115
+
116
+ def visit_Mobility_Plugins_Arel_Nodes_HstoreDashArrow o, a
117
+ json_infix o, a, '->'
118
+ end
119
+
120
+ def visit_Mobility_Plugins_Arel_Nodes_HstoreQuestion o, a
121
+ json_infix o, a, '?'
122
+ end
123
+
124
+ private
125
+
126
+ def json_infix o, a, opr
127
+ visit(Nodes::Grouping.new(::Arel::Nodes::InfixOperation.new(opr, o.left, o.right)), a)
128
+ end
129
+ end
130
+
131
+ ::Arel::Visitors::PostgreSQL.include Visitors
132
+ end
133
+ end
134
+ end