mobility 1.0.0.alpha → 1.0.1

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 (76) 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 +54 -1
  5. data/Gemfile +5 -16
  6. data/Gemfile.lock +30 -82
  7. data/README.md +24 -29
  8. data/lib/mobility.rb +67 -9
  9. data/lib/mobility/backend.rb +8 -10
  10. data/lib/mobility/backends.rb +1 -1
  11. data/lib/mobility/backends/active_record.rb +1 -1
  12. data/lib/mobility/backends/active_record/column.rb +1 -1
  13. data/lib/mobility/backends/active_record/container.rb +6 -9
  14. data/lib/mobility/backends/active_record/hstore.rb +4 -4
  15. data/lib/mobility/backends/active_record/json.rb +3 -3
  16. data/lib/mobility/backends/active_record/jsonb.rb +3 -3
  17. data/lib/mobility/backends/active_record/key_value.rb +27 -11
  18. data/lib/mobility/backends/active_record/serialized.rb +4 -0
  19. data/lib/mobility/backends/active_record/table.rb +12 -7
  20. data/lib/mobility/backends/container.rb +10 -2
  21. data/lib/mobility/backends/hash_valued.rb +4 -0
  22. data/lib/mobility/backends/jsonb.rb +1 -1
  23. data/lib/mobility/backends/key_value.rb +12 -15
  24. data/lib/mobility/backends/sequel.rb +34 -2
  25. data/lib/mobility/backends/sequel/container.rb +8 -8
  26. data/lib/mobility/backends/sequel/hstore.rb +1 -1
  27. data/lib/mobility/backends/sequel/json.rb +1 -0
  28. data/lib/mobility/backends/sequel/key_value.rb +79 -12
  29. data/lib/mobility/backends/sequel/pg_hash.rb +6 -6
  30. data/lib/mobility/backends/sequel/serialized.rb +4 -0
  31. data/lib/mobility/backends/sequel/table.rb +18 -8
  32. data/lib/mobility/backends/table.rb +29 -29
  33. data/lib/mobility/pluggable.rb +21 -1
  34. data/lib/mobility/plugin.rb +2 -2
  35. data/lib/mobility/plugins.rb +2 -0
  36. data/lib/mobility/plugins/active_model/dirty.rb +11 -5
  37. data/lib/mobility/plugins/active_record.rb +3 -0
  38. data/lib/mobility/plugins/active_record/backend.rb +2 -0
  39. data/lib/mobility/plugins/active_record/query.rb +7 -7
  40. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +5 -1
  41. data/lib/mobility/plugins/arel.rb +125 -0
  42. data/lib/mobility/plugins/arel/nodes.rb +15 -0
  43. data/lib/mobility/plugins/arel/nodes/pg_ops.rb +134 -0
  44. data/lib/mobility/plugins/attribute_methods.rb +1 -0
  45. data/lib/mobility/plugins/attributes.rb +17 -15
  46. data/lib/mobility/plugins/backend.rb +45 -22
  47. data/lib/mobility/plugins/cache.rb +12 -5
  48. data/lib/mobility/plugins/default.rb +1 -1
  49. data/lib/mobility/plugins/fallbacks.rb +4 -4
  50. data/lib/mobility/plugins/fallthrough_accessors.rb +5 -6
  51. data/lib/mobility/plugins/locale_accessors.rb +2 -5
  52. data/lib/mobility/plugins/presence.rb +1 -1
  53. data/lib/mobility/plugins/reader.rb +2 -2
  54. data/lib/mobility/plugins/sequel/dirty.rb +2 -2
  55. data/lib/mobility/plugins/writer.rb +1 -1
  56. data/lib/mobility/version.rb +2 -2
  57. data/lib/rails/generators/mobility/templates/create_string_translations.rb +0 -1
  58. data/lib/rails/generators/mobility/templates/create_text_translations.rb +0 -1
  59. data/lib/rails/generators/mobility/templates/initializer.rb +11 -3
  60. metadata +14 -20
  61. metadata.gz.sig +0 -0
  62. data/lib/mobility/active_record/model_translation.rb +0 -14
  63. data/lib/mobility/active_record/string_translation.rb +0 -10
  64. data/lib/mobility/active_record/text_translation.rb +0 -10
  65. data/lib/mobility/active_record/translation.rb +0 -14
  66. data/lib/mobility/arel.rb +0 -49
  67. data/lib/mobility/arel/nodes.rb +0 -13
  68. data/lib/mobility/arel/nodes/pg_ops.rb +0 -132
  69. data/lib/mobility/arel/visitor.rb +0 -61
  70. data/lib/mobility/sequel/column_changes.rb +0 -28
  71. data/lib/mobility/sequel/hash_initializer.rb +0 -21
  72. data/lib/mobility/sequel/model_translation.rb +0 -20
  73. data/lib/mobility/sequel/sql.rb +0 -16
  74. data/lib/mobility/sequel/string_translation.rb +0 -10
  75. data/lib/mobility/sequel/text_translation.rb +0 -10
  76. data/lib/mobility/sequel/translation.rb +0 -53
@@ -17,6 +17,10 @@ Works with {Mobility::Plugin}. (Subclassed by {Mobility::Translations}.)
17
17
  Plugin.configure(self, defaults, &block)
18
18
  end
19
19
 
20
+ def included_plugins
21
+ included_modules.grep(Plugin)
22
+ end
23
+
20
24
  def defaults
21
25
  @defaults ||= {}
22
26
  end
@@ -28,9 +32,25 @@ Works with {Mobility::Plugin}. (Subclassed by {Mobility::Translations}.)
28
32
  end
29
33
 
30
34
  def initialize(*, **options)
31
- @options = self.class.defaults.merge(options)
35
+ initialize_options(options)
36
+ validate_options(@options)
32
37
  end
33
38
 
34
39
  attr_reader :options
40
+
41
+ private
42
+
43
+ def initialize_options(options)
44
+ @options = self.class.defaults.merge(options)
45
+ end
46
+
47
+ # This is overridden by backend plugin to exclude mixed-in backend options.
48
+ def validate_options(options)
49
+ plugin_keys = self.class.included_plugins.map { |p| Plugins.lookup_name(p) }
50
+ extra_keys = options.keys - plugin_keys
51
+ raise InvalidOptionKey, "No plugin configured for these keys: #{extra_keys.join(', ')}." unless extra_keys.empty?
52
+ end
53
+
54
+ class InvalidOptionKey < Error; end
35
55
  end
36
56
  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.
@@ -14,6 +14,8 @@ declared in any order (dependencies will be resolved).
14
14
  class << self
15
15
  # @param [Symbol] name Name of plugin to load.
16
16
  def load_plugin(name)
17
+ return name if Module === name || name.nil?
18
+
17
19
  unless (plugin = @plugins[name])
18
20
  require "mobility/plugins/#{name}"
19
21
  raise LoadError, "plugin #{name} did not register itself correctly in Mobility::Plugins" unless (plugin = @plugins[name])
@@ -80,7 +80,12 @@ the ActiveRecord dirty plugin for more information.
80
80
  attribute_names.each do |name|
81
81
  dirty_handler_methods.each_pattern(name) do |method_name, attribute_method|
82
82
  define_method(method_name) do |*args|
83
- mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args)
83
+ # for %s_changed?(from:, to:) pattern
84
+ if (kwargs = args.last).is_a?(Hash)
85
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args[0,-1], **kwargs)
86
+ else
87
+ mutations_from_mobility.send(attribute_method, Dirty.append_locale(name), *args)
88
+ end
84
89
  end
85
90
  end
86
91
 
@@ -130,11 +135,12 @@ the ActiveRecord dirty plugin for more information.
130
135
  public_patterns.each do |pattern|
131
136
  method_name = pattern % 'attribute'
132
137
 
138
+ kwargs = pattern == '%s_changed?' ? ', **kwargs' : ''
133
139
  module_eval <<-EOM, __FILE__, __LINE__ + 1
134
- def #{method_name}(attr_name, *rest)
140
+ def #{method_name}(attr_name, *rest#{kwargs})
135
141
  if (mutations_from_mobility.attribute_changed?(attr_name) ||
136
142
  mutations_from_mobility.attribute_previously_changed?(attr_name))
137
- mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest)
143
+ mutations_from_mobility.send(#{method_name.inspect}, attr_name, *rest#{kwargs})
138
144
  else
139
145
  super
140
146
  end
@@ -330,11 +336,11 @@ the ActiveRecord dirty plugin for more information.
330
336
  # @!group Backend Accessors
331
337
  # @!macro backend_writer
332
338
  # @param [Hash] options
333
- def write(locale, value, options = {})
339
+ def write(locale, value, **options)
334
340
  locale_accessor = Mobility.normalize_locale_accessor(attribute, locale)
335
341
  if model.changed_attributes.has_key?(locale_accessor) && model.changed_attributes[locale_accessor] == value
336
342
  mutations_from_mobility.restore_attribute!(locale_accessor)
337
- elsif read(locale, options.merge(locale: true)) != value
343
+ elsif read(locale, **options.merge(locale: true)) != value
338
344
  mutations_from_mobility.attribute_will_change!(locale_accessor)
339
345
  end
340
346
  super
@@ -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
@@ -35,7 +39,7 @@ module Mobility
35
39
  error_options = options.except(:case_sensitive, :scope, :conditions)
36
40
  error_options[:value] = value
37
41
 
38
- record.errors.add(attribute, :taken, error_options)
42
+ record.errors.add(attribute, :taken, **error_options)
39
43
  end
40
44
  else
41
45
  super
@@ -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