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
@@ -20,8 +20,9 @@ Resets backend cache when reset events occur.
20
20
  # @raise [ArgumentError] if no block is provided.
21
21
  def initialize(attribute_names, &block)
22
22
  raise ArgumentError, "block required" unless block_given?
23
+ names = attribute_names.map(&:to_sym)
23
24
  @model_reset_method = Proc.new do
24
- attribute_names.each do |name|
25
+ names.each do |name|
25
26
  if @mobility_backends && @mobility_backends[name]
26
27
  @mobility_backends[name].instance_eval(&block)
27
28
  end
@@ -1,19 +1,39 @@
1
1
  module Mobility
2
2
  module Backends
3
3
  module ActiveRecord
4
- def setup_query_methods(query_methods)
5
- setup do |attributes, options|
6
- extend(Module.new do
7
- define_method ::Mobility.query_method do
8
- super().extending(query_methods.new(attributes, options))
9
- end
10
- end)
11
- end
12
- end
13
-
14
4
  def self.included(backend_class)
15
5
  backend_class.include(Backend)
16
- backend_class.extend(self)
6
+ backend_class.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # @param [Symbol] name Attribute name
11
+ # @param [Symbol] locale Locale
12
+ def [](name, locale)
13
+ build_node(name.to_s, locale)
14
+ end
15
+
16
+ # @param [String] _attr Attribute name
17
+ # @param [Symbol] _locale Locale
18
+ # @return Arel node for this translated attribute
19
+ def build_node(_attr, _locale)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # @param [ActiveRecord::Relation] relation Relation to scope
24
+ # @param [Object] predicate Arel predicate
25
+ # @param [Symbol] locale Locale
26
+ # @option [Boolean] invert
27
+ # @return [ActiveRecord::Relation] Relation with scope added
28
+ def apply_scope(relation, _predicate, _locale, invert: false)
29
+ relation
30
+ end
31
+
32
+ private
33
+
34
+ def build_quoted(value)
35
+ ::Arel::Nodes.build_quoted(value)
36
+ end
17
37
  end
18
38
  end
19
39
  end
@@ -34,8 +34,6 @@ or locales.)
34
34
  include ActiveRecord
35
35
  include Column
36
36
 
37
- require 'mobility/backends/active_record/column/query_methods'
38
-
39
37
  # @!group Backend Accessors
40
38
  # @!macro backend_reader
41
39
  def read(locale, _ = {})
@@ -53,7 +51,13 @@ or locales.)
53
51
  available_locales.each { |l| yield(l) if present?(l) }
54
52
  end
55
53
 
56
- setup_query_methods(QueryMethods)
54
+ # @param [String] attr Attribute name
55
+ # @param [Symbol] locale Locale
56
+ # @return [Arel::Attributes::Attribute] Arel node for translation column
57
+ # on model table
58
+ def self.build_node(attr, locale)
59
+ model_class.arel_table[Column.column_name_for(attr, locale)]
60
+ end
57
61
 
58
62
  private
59
63
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require "mobility/backends/active_record"
3
+ require "mobility/arel/nodes/pg_ops"
3
4
 
4
5
  module Mobility
5
6
  module Backends
@@ -11,18 +12,14 @@ Implements the {Mobility::Backends::Container} backend for ActiveRecord models.
11
12
  class ActiveRecord::Container
12
13
  include ActiveRecord
13
14
 
14
- require 'mobility/backends/active_record/container/json_query_methods'
15
- require 'mobility/backends/active_record/container/jsonb_query_methods'
15
+ # @!method column_name
16
+ # Returns name of json or jsonb column used to store translations
17
+ # @return [Symbol] (:translations) Name of translations column
18
+ option_reader :column_name
16
19
 
17
- # @return [Symbol] name of container column
18
- attr_reader :column_name
19
-
20
- # @!macro backend_constructor
21
- # @option options [Symbol] column_name Name of container column
22
- def initialize(model, attribute, options = {})
23
- super
24
- @column_name = options[:column_name]
25
- end
20
+ # @!method column_type
21
+ # @return [Symbol] Either :json or :jsonb
22
+ option_reader :column_type
26
23
 
27
24
  # @!group Backend Accessors
28
25
  #
@@ -60,6 +57,20 @@ Implements the {Mobility::Backends::Container} backend for ActiveRecord models.
60
57
  end
61
58
  # @!endgroup
62
59
 
60
+ # @param [String] attr Attribute name
61
+ # @param [Symbol] locale Locale
62
+ # @return [Mobility::Arel::Nodes::Json,Mobility::Arel::Nodes::Jsonb] Arel
63
+ # node for attribute on json or jsonb column
64
+ def self.build_node(attr, locale)
65
+ column = model_class.arel_table[column_name]
66
+ case column_type
67
+ when :json
68
+ Arel::Nodes::JsonContainer.new(column, build_quoted(locale), build_quoted(attr))
69
+ when :jsonb
70
+ Arel::Nodes::JsonbContainer.new(column, build_quoted(locale), build_quoted(attr))
71
+ end
72
+ end
73
+
63
74
  # @!macro backend_iterator
64
75
  def each_locale
65
76
  model[column_name].each do |l, v|
@@ -67,9 +78,7 @@ Implements the {Mobility::Backends::Container} backend for ActiveRecord models.
67
78
  end
68
79
  end
69
80
 
70
- backend_class = self
71
-
72
- setup do |attributes, options|
81
+ setup do |_attributes, options|
73
82
  store options[:column_name], coder: Coder
74
83
 
75
84
  # Fix for duping depth-2 jsonb column in AR < 5.0
@@ -88,13 +97,6 @@ Implements the {Mobility::Backends::Container} backend for ActiveRecord models.
88
97
  include const_set(module_name, dupable)
89
98
  end
90
99
  end
91
-
92
- query_methods = backend_class.const_get("#{options[:column_type].capitalize}QueryMethods")
93
- extend(Module.new do
94
- define_method ::Mobility.query_method do
95
- super().extending(query_methods.new(attributes, options))
96
- end
97
- end)
98
100
  end
99
101
 
100
102
  private
@@ -1,4 +1,5 @@
1
1
  require 'mobility/backends/active_record/pg_hash'
2
+ require 'mobility/arel/nodes/pg_ops'
2
3
 
3
4
  module Mobility
4
5
  module Backends
@@ -6,25 +7,29 @@ module Mobility
6
7
 
7
8
  Implements the {Mobility::Backends::Hstore} backend for ActiveRecord models.
8
9
 
9
- @see Mobility::Backends::ActiveRecord::HashValued
10
+ @see Mobility::Backends::HashValued
10
11
 
11
12
  =end
12
13
  module ActiveRecord
13
14
  class Hstore < PgHash
14
- require 'mobility/backends/active_record/hstore/query_methods'
15
-
16
15
  # @!group Backend Accessors
17
16
  # @!macro backend_reader
18
- # @!method read(locale, **options)
17
+ # @!method read(locale, options = {})
19
18
 
20
- # @!group Backend Accessors
21
19
  # @!macro backend_writer
22
20
  def write(locale, value, options = {})
23
21
  super(locale, value && value.to_s, options)
24
22
  end
25
23
  # @!endgroup
26
24
 
27
- setup_query_methods(QueryMethods)
25
+ # @param [String] attr Attribute name
26
+ # @param [Symbol] locale Locale
27
+ # @return [Mobility::Arel::Nodes::Hstore] Arel node for value of
28
+ # attribute key on hstore column
29
+ def self.build_node(attr, locale)
30
+ column_name = column_affix % attr
31
+ Arel::Nodes::Hstore.new(model_class.arel_table[column_name], build_quoted(locale))
32
+ end
28
33
  end
29
34
  end
30
35
  end
@@ -1,4 +1,5 @@
1
1
  require 'mobility/backends/active_record/pg_hash'
2
+ require 'mobility/arel/nodes/pg_ops'
2
3
 
3
4
  module Mobility
4
5
  module Backends
@@ -6,32 +7,37 @@ module Mobility
6
7
 
7
8
  Implements the {Mobility::Backends::Json} backend for ActiveRecord models.
8
9
 
9
- @see Mobility::Backends::ActiveRecord::HashValued
10
+ @see Mobility::Backends::HashValued
10
11
 
11
12
  =end
12
13
  module ActiveRecord
13
14
  class Json < PgHash
14
- require 'mobility/backends/active_record/json/query_methods'
15
-
16
15
  # @!group Backend Accessors
17
16
  #
18
- # @note Translation may be string, integer or boolean-valued since
19
- # value is stored on a JSON hash.
20
- # @param [Symbol] locale Locale to read
21
- # @param [Hash] options
22
- # @return [String,Integer,Boolean] Value of translation
23
17
  # @!method read(locale, **options)
18
+ # @note Translation may be string, integer or boolean-valued since
19
+ # value is stored on a JSON hash.
20
+ # @param [Symbol] locale Locale to read
21
+ # @param [Hash] options
22
+ # @return [String,Integer,Boolean] Value of translation
24
23
 
25
- # @!group Backend Accessors
26
- # @note Translation may be string, integer or boolean-valued since
27
- # value is stored on a JSON hash.
28
- # @param [Symbol] locale Locale to write
29
- # @param [String,Integer,Boolean] value Value to write
30
- # @param [Hash] options
31
- # @return [String,Integer,Boolean] Updated value
32
24
  # @!method write(locale, value, **options)
25
+ # @note Translation may be string, integer or boolean-valued since
26
+ # value is stored on a JSON hash.
27
+ # @param [Symbol] locale Locale to write
28
+ # @param [String,Integer,Boolean] value Value to write
29
+ # @param [Hash] options
30
+ # @return [String,Integer,Boolean] Updated value
31
+ # @!endgroup
33
32
 
34
- setup_query_methods(QueryMethods)
33
+ # @param [String] attr Attribute name
34
+ # @param [Symbol] locale Locale
35
+ # @return [Mobility::Arel::Nodes::Json] Arel node for value of
36
+ # attribute key on jsonb column
37
+ def self.build_node(attr, locale)
38
+ column_name = column_affix % attr
39
+ Arel::Nodes::Json.new(model_class.arel_table[column_name], build_quoted(locale))
40
+ end
35
41
  end
36
42
  end
37
43
  end
@@ -1,4 +1,5 @@
1
1
  require 'mobility/backends/active_record/pg_hash'
2
+ require 'mobility/arel/nodes/pg_ops'
2
3
 
3
4
  module Mobility
4
5
  module Backends
@@ -6,32 +7,37 @@ module Mobility
6
7
 
7
8
  Implements the {Mobility::Backends::Jsonb} backend for ActiveRecord models.
8
9
 
9
- @see Mobility::Backends::ActiveRecord::HashValued
10
+ @see Mobility::Backends::HashValued
10
11
 
11
12
  =end
12
13
  module ActiveRecord
13
14
  class Jsonb < PgHash
14
- require 'mobility/backends/active_record/jsonb/query_methods'
15
-
16
15
  # @!group Backend Accessors
17
16
  #
18
- # @note Translation may be any json type, but querying will only work on
19
- # string-typed values.
20
- # @param [Symbol] locale Locale to read
21
- # @param [Hash] options
22
- # @return [String,Integer,Boolean] Value of translation
23
17
  # @!method read(locale, **options)
18
+ # @note Translation may be any json type, but querying will only work on
19
+ # string-typed values.
20
+ # @param [Symbol] locale Locale to read
21
+ # @param [Hash] options
22
+ # @return [String,Integer,Boolean] Value of translation
24
23
 
25
- # @!group Backend Accessors
26
- # @note Translation may be any json type, but querying will only work on
27
- # string-typed values.
28
- # @param [Symbol] locale Locale to write
29
- # @param [String,Integer,Boolean] value Value to write
30
- # @param [Hash] options
31
- # @return [String,Integer,Boolean] Updated value
32
24
  # @!method write(locale, value, **options)
25
+ # @note Translation may be any json type, but querying will only work on
26
+ # string-typed values.
27
+ # @param [Symbol] locale Locale to write
28
+ # @param [String,Integer,Boolean] value Value to write
29
+ # @param [Hash] options
30
+ # @return [String,Integer,Boolean] Updated value
31
+ # @!endgroup
33
32
 
34
- setup_query_methods(QueryMethods)
33
+ # @param [String] attr Attribute name
34
+ # @param [Symbol] locale Locale
35
+ # @return [Mobility::Arel::Nodes::Jsonb] Arel node for value of
36
+ # attribute key on jsonb column
37
+ def self.build_node(attr, locale)
38
+ column_name = column_affix % attr
39
+ Arel::Nodes::Jsonb.new(model_class.arel_table[column_name], build_quoted(locale))
40
+ end
35
41
  end
36
42
  end
37
43
  end
@@ -11,7 +11,7 @@ module Mobility
11
11
  Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
12
12
 
13
13
  @example
14
- class Post < ActiveRecord::Base
14
+ class Post < ApplicationRecord
15
15
  extend Mobility
16
16
  translates :title, backend: :key_value, association_name: :translations, type: :string
17
17
  end
@@ -29,21 +29,131 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
29
29
  include ActiveRecord
30
30
  include KeyValue
31
31
 
32
- require 'mobility/backends/active_record/key_value/query_methods'
32
+ option_reader :table_alias_affix
33
33
 
34
- # @!group Backend Configuration
35
- # @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
36
- # @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
37
- def self.configure(options)
38
- super
39
- if type = options[:type]
40
- options[:association_name] ||= :"#{options[:type]}_translations"
41
- options[:class_name] ||= Mobility::ActiveRecord.const_get("#{type.capitalize}Translation")
34
+ class << self
35
+ # @!group Backend Configuration
36
+ # @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
37
+ # @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
38
+ def configure(options)
39
+ super
40
+ if type = options[:type]
41
+ options[:association_name] ||= :"#{options[:type]}_translations"
42
+ options[:class_name] ||= Mobility::ActiveRecord.const_get("#{type.capitalize}Translation")
43
+ end
44
+ options[:table_alias_affix] = "#{options[:model_class]}_%s_#{options[:association_name]}"
45
+ rescue NameError
46
+ raise ArgumentError, "You must define a Mobility::ActiveRecord::#{type.capitalize}Translation class."
47
+ end
48
+ # @!endgroup
49
+
50
+ # @param [String] attr Attribute name
51
+ # @param [Symbol] _locale Locale
52
+ # @return [Mobility::Arel::Attribute] Arel attribute for aliased
53
+ # translation table value column
54
+ def build_node(attr, locale)
55
+ aliased_table = class_name.arel_table.alias(table_alias(attr, locale))
56
+ Arel::Attribute.new(aliased_table, :value, locale, self, attribute_name: attr.to_sym)
57
+ end
58
+
59
+ # Joins translations using either INNER/OUTER join appropriate to the query.
60
+ # @param [ActiveRecord::Relation] relation Relation to scope
61
+ # @param [Object] predicate Arel predicate
62
+ # @param [Symbol] locale Locale
63
+ # @option [Boolean] invert
64
+ # @return [ActiveRecord::Relation] relation Relation with joins applied (if needed)
65
+ def apply_scope(relation, predicate, locale, invert: false)
66
+ visitor = Visitor.new(self, locale)
67
+ visitor.accept(predicate).inject(relation) do |rel, (attr, join_type)|
68
+ join_type &&= ::Arel::Nodes::InnerJoin if invert
69
+ join_translations(rel, attr, locale, join_type)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def table_alias(attr, locale)
76
+ table_alias_affix % "#{attr}_#{locale}"
77
+ end
78
+
79
+ def join_translations(relation, key, locale, join_type)
80
+ return relation if already_joined?(relation, key, locale, join_type)
81
+ m = model_class.arel_table
82
+ t = class_name.arel_table.alias(table_alias(key, locale))
83
+ relation.joins(m.join(t, join_type).
84
+ on(t[:key].eq(key).
85
+ and(t[:locale].eq(locale).
86
+ and(t[:translatable_type].eq(model_class.base_class.name).
87
+ and(t[:translatable_id].eq(m[:id]))))).join_sources)
88
+ end
89
+
90
+ def already_joined?(relation, name, locale, join_type)
91
+ if join = get_join(relation, name, locale)
92
+ return true if (join_type == ::Arel::Nodes::OuterJoin) || (::Arel::Nodes::InnerJoin === join)
93
+ relation.joins_values = relation.joins_values - [join]
94
+ end
95
+ false
96
+ end
97
+
98
+ def get_join(relation, name, locale)
99
+ relation.joins_values.find do |v|
100
+ (::Arel::Nodes::Join === v) && (v.left.name == (table_alias(name, locale)))
101
+ end
102
+ end
103
+ end
104
+
105
+ # Internal class used to visit all nodes in a predicate clause and
106
+ # return a hash of key/value pairs corresponding to attributes (keys)
107
+ # and the respective join type (values) required for each attribute.
108
+ #
109
+ # Example:
110
+ #
111
+ # class Post < ApplicationRecord
112
+ # extend Mobility
113
+ # translates :title, :content, backend: :key_value
114
+ # end
115
+ #
116
+ # backend_class = Post.mobility_backend_class(:title)
117
+ # visitor = Mobility::Backends::ActiveRecord::KeyValue::Visitor.new(backend_class)
118
+ #
119
+ # visitor.accept(title.eq("foo").and(content.eq(nil)))
120
+ # #=> { title: Arel::Nodes::InnerJoin, content: Arel::Nodes::OuterJoin }
121
+ #
122
+ # The title predicate has a non-nil value, so we can use an INNER JOIN,
123
+ # whereas we are searching for nil content, which requires an OUTER JOIN.
124
+ #
125
+ class Visitor < Arel::Visitor
126
+ private
127
+
128
+ def visit_Arel_Nodes_Equality(object)
129
+ nils, nodes = [object.left, object.right].partition(&:nil?)
130
+ if hash = visit_collection(nodes)
131
+ hash.transform_values { nils.empty? ? INNER_JOIN : OUTER_JOIN }
132
+ end
133
+ end
134
+
135
+ def visit_collection(objects)
136
+ objects.map(&method(:visit)).compact.inject do |hash, visited|
137
+ visited.merge(hash) { |_, old, new| old == INNER_JOIN ? old : new }
138
+ end
139
+ end
140
+ alias :visit_Array :visit_collection
141
+
142
+ def visit_Arel_Nodes_Or(object)
143
+ [object.left, object.right].map(&method(:visit)).compact.inject(&:merge).
144
+ transform_values { OUTER_JOIN }
145
+ end
146
+
147
+ def visit_Mobility_Arel_Attribute(object)
148
+ if object.backend_class == backend_class && object.locale == locale
149
+ { object.attribute_name => INNER_JOIN }
150
+ end
151
+ end
152
+
153
+ def visit_default(_)
154
+ {}
42
155
  end
43
- rescue NameError
44
- raise ArgumentError, "You must define a Mobility::ActiveRecord::#{type.capitalize}Translation class."
45
156
  end
46
- # @!endgroup
47
157
 
48
158
  setup do |attributes, options|
49
159
  association_name = options[:association_name]
@@ -85,8 +195,6 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
85
195
  include DestroyKeyValueTranslations
86
196
  end
87
197
 
88
- setup_query_methods(QueryMethods)
89
-
90
198
  # Returns translation for a given locale, or builds one if none is present.
91
199
  # @param [Symbol] locale
92
200
  # @return [Mobility::ActiveRecord::TextTranslation,Mobility::ActiveRecord::StringTranslation]