mobility 0.6.0 → 0.7.0

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