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
@@ -1,25 +0,0 @@
1
- require 'mobility/backends/active_record/pg_query_methods'
2
- require 'mobility/backends/active_record/query_methods'
3
-
4
- module Mobility
5
- module Backends
6
- module ActiveRecord
7
- class Hstore::QueryMethods < QueryMethods
8
- include PgQueryMethods
9
-
10
- def matches(key, locale)
11
- build_infix(:'->', arel_table[column_name(key)], build_quoted(locale))
12
- end
13
-
14
- def exists(key, locale)
15
- build_infix(:'?', arel_table[column_name(key)], build_quoted(locale))
16
- end
17
-
18
- def quote(value)
19
- build_quoted(value)
20
- end
21
- end
22
- Hstore.private_constant :QueryMethods
23
- end
24
- end
25
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'mobility/backends/active_record/pg_query_methods'
3
- require "mobility/backends/active_record/query_methods"
4
-
5
- module Mobility
6
- module Backends
7
- module ActiveRecord
8
- class Json::QueryMethods < QueryMethods
9
- include PgQueryMethods
10
-
11
- def matches(key, locale)
12
- build_infix(:'->>', arel_table[column_name(key)], build_quoted(locale))
13
- end
14
-
15
- def exists(key, locale)
16
- absent(key, locale).not
17
- end
18
-
19
- def absent(key, locale)
20
- matches(key, locale).eq(nil)
21
- end
22
-
23
- def quote(value)
24
- value.to_s
25
- end
26
- end
27
- Json.private_constant :QueryMethods
28
- end
29
- end
30
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
- require 'mobility/backends/active_record/pg_query_methods'
3
- require "mobility/backends/active_record/query_methods"
4
-
5
- module Mobility
6
- module Backends
7
- module ActiveRecord
8
- class Jsonb::QueryMethods < QueryMethods
9
- include PgQueryMethods
10
-
11
- def matches(key, locale)
12
- build_infix(:'->', arel_table[column_name(key)], build_quoted(locale))
13
- end
14
-
15
- def exists(key, locale)
16
- build_infix(:'?', arel_table[column_name(key)], build_quoted(locale))
17
- end
18
-
19
- def quote(value)
20
- build_quoted(value.to_json)
21
- end
22
- end
23
- Jsonb.private_constant :QueryMethods
24
- end
25
- end
26
- end
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/backends/active_record/query_methods"
3
-
4
- module Mobility
5
- module Backends
6
- module ActiveRecord
7
- class KeyValue::QueryMethods < QueryMethods
8
- def initialize(attributes, association_name: nil, class_name: nil, **)
9
- super
10
- @association_name = association_name
11
-
12
- define_join_method(association_name, class_name)
13
- define_query_methods(association_name)
14
- end
15
-
16
- def extended(relation)
17
- super
18
- association_name = @association_name
19
- q = self
20
-
21
- mod = Module.new do
22
- define_method :not do |opts, *rest|
23
- if i18n_keys = q.extract_attributes(opts)
24
- opts = opts.with_indifferent_access
25
- i18n_keys.each do |attr|
26
- opts["#{attr}_#{association_name}"] = { value: q.collapse(opts.delete(attr)) }
27
- end
28
- super(opts, *rest).send(:"join_#{association_name}", *i18n_keys)
29
- else
30
- super(opts, *rest)
31
- end
32
- end
33
- end
34
- relation.mobility_where_chain.include(mod)
35
- end
36
-
37
- private
38
-
39
- def define_join_method(association_name, translation_class)
40
- define_method :"join_#{association_name}" do |*attributes, **options|
41
- attributes.inject(self) do |relation, attribute|
42
- t = translation_class.arel_table.alias("#{attribute}_#{association_name}")
43
- m = arel_table
44
- join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
45
- relation.joins(m.join(t, join_type).
46
- on(t[:key].eq(attribute).
47
- and(t[:locale].eq(Mobility.locale).
48
- and(t[:translatable_type].eq(base_class.name).
49
- and(t[:translatable_id].eq(m[:id]))))).join_sources)
50
- end
51
- end
52
- end
53
-
54
- def define_query_methods(association_name)
55
- q = self
56
-
57
- define_method :where! do |opts, *rest|
58
- if i18n_keys = q.extract_attributes(opts)
59
- opts = opts.with_indifferent_access
60
- i18n_nulls = i18n_keys.reject { |key| opts[key] && [*opts[key]].all? }
61
- i18n_keys.each do |attr|
62
- opts["#{attr}_#{association_name}"] = { value: q.collapse(opts.delete(attr)) }
63
- end
64
- super(opts, *rest).
65
- send("join_#{association_name}", *(i18n_keys - i18n_nulls)).
66
- send("join_#{association_name}", *i18n_nulls, outer_join: true)
67
- else
68
- super(opts, *rest)
69
- end
70
- end
71
- end
72
- end
73
- KeyValue.private_constant :QueryMethods
74
- end
75
- end
76
- end
@@ -1,154 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mobility
4
- module Backends
5
- module ActiveRecord
6
- =begin
7
-
8
- Internal module builder defining query methods for Postgres backends. Including
9
- class must define the following methods:
10
-
11
- - a method +matches+ which takes an attribute, and a locale to match, and
12
- returns an Arel node which is used to check that the attribute has the
13
- specified value in the specified locale
14
- - a method +exists+ which takes an attribute and a locale and
15
- returns an Arel node checking that a value exists for the attribute in the
16
- specified locale
17
- - a method +quote+ which quotes the value to be matched
18
- - an optional method +absent+ which takes an attribute and a locale and returns
19
- an Arel node checking that the value for the attribute does not exist in the
20
- specified locale. Defaults to +exists(key, locale).not+.
21
-
22
- This module avoids a lot of duplication between hstore/json/jsonb/container
23
- backend querying code.
24
-
25
- @see Mobility::Backends::ActiveRecord::Json::QueryMethods
26
- @see Mobility::Backends::ActiveRecord::Jsonb::QueryMethods
27
- @see Mobility::Backends::ActiveRecord::Hstore::QueryMethods
28
- @see Mobility::Backends::ActiveRecord::Container::JsonQueryMethods
29
- @see Mobility::Backends::ActiveRecord::Container::JsonbQueryMethods
30
-
31
- =end
32
- module PgQueryMethods
33
- attr_reader :arel_table, :column_affix
34
-
35
- def initialize(attributes, options)
36
- super
37
- @arel_table = options[:model_class].arel_table
38
- @column_affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
39
-
40
- q = self
41
-
42
- define_method :where! do |opts, *rest|
43
- if i18n_keys = q.extract_attributes(opts)
44
- opts = opts.with_indifferent_access
45
- query = q.create_query!(opts, i18n_keys)
46
-
47
- opts.empty? ? super(query) : super(opts, *rest).where(query)
48
- else
49
- super(opts, *rest)
50
- end
51
- end
52
- end
53
-
54
- def extended(relation)
55
- super
56
- q = self
57
-
58
- mod = Module.new do
59
- define_method :not do |opts, *rest|
60
- if i18n_keys = q.extract_attributes(opts)
61
- opts = opts.with_indifferent_access
62
- query = q.create_query!(opts, i18n_keys, inverse: true)
63
-
64
- super(opts, *rest).where(query)
65
- else
66
- super(opts, *rest)
67
- end
68
- end
69
- end
70
- relation.mobility_where_chain.include(mod)
71
- end
72
-
73
- # Create +where+ query for specified key and value
74
- #
75
- # @note This is a destructive operation, it will modify +opts+.
76
- # @param [Hash] opts Hash of attribute/value pairs
77
- # @param [Array] keys Translated attribute names
78
- # @option [Boolean] inverse (false) If true, create a +not+ query
79
- # instead of a +where+ query
80
- # @return [Arel::Node] Arel node to pass to +where+
81
- def create_query!(opts, keys, inverse: false)
82
- keys.map { |key|
83
- values = Array.wrap(opts.delete(key)).uniq
84
- send(inverse ? :not_query : :where_query, key, values, Mobility.locale)
85
- }.inject(&:and)
86
- end
87
-
88
- def matches(_key, _locale)
89
- raise NotImplementedError
90
- end
91
-
92
- def exists(_key, _locale)
93
- raise NotImplementedError
94
- end
95
-
96
- def quote(_value)
97
- raise NotImplementedError
98
- end
99
-
100
- def absent(key, locale)
101
- exists(key, locale).not
102
- end
103
-
104
- private
105
-
106
- def build_infix(*args)
107
- arel_table.grouping(Arel::Nodes::InfixOperation.new(*args))
108
- end
109
-
110
- def build_quoted(value)
111
- Arel::Nodes.build_quoted(value.to_s)
112
- end
113
-
114
- def column_name(attribute)
115
- column_affix % attribute
116
- end
117
-
118
- # Create +where+ query for specified key and values
119
- #
120
- # @param [String] key Translated attribute name
121
- # @param [Array] values Values to match
122
- # @param [Symbol] locale Locale to query for
123
- # @return [Arel::Node] Arel node to pass to +where+
124
- def where_query(key, values, locale)
125
- nils, vals = values.partition(&:nil?)
126
-
127
- return absent(key, locale) if vals.empty?
128
-
129
- node = matches(key, locale)
130
- vals = vals.map(&method(:quote))
131
-
132
- query = vals.size == 1 ? node.eq(vals.first) : node.in(vals)
133
- query = query.or(absent(key, locale)) unless nils.empty?
134
- query
135
- end
136
-
137
- # Create +not+ query for specified key and values
138
- #
139
- # @param [String] key Translated attribute name
140
- # @param [Array] values Values to match
141
- # @param [Symbol] locale Locale to query for
142
- # @return [Arel::Node] Arel node to pass to +where+
143
- def not_query(key, values, locale)
144
- vals = values.map(&method(:quote))
145
- node = matches(key, locale)
146
-
147
- query = vals.size == 1 ? node.eq(vals.first) : node.in(vals)
148
- query.not.and(exists(key, locale))
149
- end
150
- end
151
- private_constant :PgQueryMethods
152
- end
153
- end
154
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/backends/active_record/query_methods"
3
-
4
- module Mobility
5
- module Backends
6
- module ActiveRecord
7
- class Serialized::QueryMethods < QueryMethods
8
- include Backends::Serialized
9
-
10
- def initialize(attributes, _)
11
- super
12
- q = self
13
-
14
- define_method :where! do |opts, *rest|
15
- q.check_opts(opts) || super(opts, *rest)
16
- end
17
- end
18
-
19
- def extended(relation)
20
- super
21
- q = self
22
-
23
- mod = Module.new do
24
- define_method :not do |opts, *rest|
25
- q.check_opts(opts) || super(opts, *rest)
26
- end
27
- end
28
- relation.mobility_where_chain.include(mod)
29
- end
30
- end
31
- Serialized.private_constant :QueryMethods
32
- end
33
- end
34
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
- require "mobility/backends/active_record/query_methods"
3
-
4
- module Mobility
5
- module Backends
6
- module ActiveRecord
7
- class Table::QueryMethods < QueryMethods
8
- def initialize(attributes, association_name: nil, model_class: nil, subclass_name: nil, **options)
9
- super
10
-
11
- @association_name = association_name
12
- @translation_class = translation_class = model_class.const_get(subclass_name)
13
-
14
- define_join_method(association_name, translation_class, **options)
15
- define_query_methods(association_name, translation_class, **options)
16
- end
17
-
18
- def extended(relation)
19
- super
20
- association_name = @association_name
21
- translation_class = @translation_class
22
- q = self
23
-
24
- mod = Module.new do
25
- define_method :not do |opts, *rest|
26
- if i18n_keys = q.extract_attributes(opts)
27
- opts = opts.with_indifferent_access
28
- i18n_keys.each do |attr|
29
- opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
30
- end
31
- super(opts, *rest).send("join_#{association_name}")
32
- else
33
- super(opts, *rest)
34
- end
35
- end
36
- end
37
- relation.mobility_where_chain.include(mod)
38
- end
39
-
40
- private
41
-
42
- def define_join_method(association_name, translation_class, foreign_key: nil, table_name: nil, **)
43
- define_method :"join_#{association_name}" do |**options|
44
- if join = joins_values.find { |v| (Arel::Nodes::Join === v) && (v.left.name == table_name.to_s) }
45
- return self if (options[:outer_join] || Arel::Nodes::InnerJoin === join)
46
- self.joins_values = joins_values - [join]
47
- end
48
- t = translation_class.arel_table
49
- m = arel_table
50
- join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
51
- joins(m.join(t, join_type).
52
- on(t[foreign_key].eq(m[:id]).
53
- and(t[:locale].eq(Mobility.locale))).join_sources)
54
- end
55
- end
56
-
57
- def define_query_methods(association_name, translation_class, **)
58
- q = self
59
-
60
- # Note that Mobility will try to use inner/outer joins appropriate to the query,
61
- # so for example:
62
- #
63
- # Article.where(title: nil, content: nil) #=> OUTER JOIN (all nils)
64
- # Article.where(title: "foo", content: nil) #=> INNER JOIN (one non-nil)
65
- #
66
- # In the first case, if we are in (say) the "en" locale, then we should match articles
67
- # that have *no* article_translations with English locales (since no translation is
68
- # equivalent to a nil value). If we used an inner join in the first case, an article
69
- # with no English translations would be filtered out, so we use an outer join.
70
- #
71
- # When deciding whether to use an outer or inner join, array-valued
72
- # conditions are treated as nil if they have any values.
73
- #
74
- # Article.where(title: nil, content: ["foo", nil]) #=> OUTER JOIN (all nil or array with nil)
75
- # Article.where(title: "foo", content: ["foo", nil]) #=> INNER JOIN (one non-nil)
76
- # Article.where(title: ["foo", "bar"], content: ["foo", nil]) #=> INNER JOIN (one non-nil array)
77
- #
78
- # The logic also applies when a query has more than one where clause.
79
- #
80
- # Article.where(title: nil).where(content: nil) #=> OUTER JOIN (all nils)
81
- # Article.where(title: nil).where(content: "foo") #=> INNER JOIN (one non-nil)
82
- # Article.where(title: "foo").where(content: nil) #=> INNER JOIN (one non-nil)
83
- #
84
- define_method :where! do |opts, *rest|
85
- if i18n_keys = q.extract_attributes(opts)
86
- opts = opts.with_indifferent_access
87
- options = {
88
- # We only need an OUTER JOIN if every value is either nil, or an
89
- # array with at least one nil value.
90
- outer_join: opts.values_at(*i18n_keys).compact.all? { |v| ![*v].all? }
91
- }
92
- i18n_keys.each do |attr|
93
- opts["#{translation_class.table_name}.#{attr}"] = q.collapse opts.delete(attr)
94
- end
95
- super(opts, *rest).send("join_#{association_name}", options)
96
- else
97
- super(opts, *rest)
98
- end
99
- end
100
- end
101
- end
102
- Table.private_constant :QueryMethods
103
- end
104
- end
105
- end