mobility 0.3.6 → 0.4.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 (51) 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 +21 -0
  5. data/Gemfile +11 -9
  6. data/Gemfile.lock +9 -6
  7. data/README.md +50 -16
  8. data/lib/mobility.rb +18 -4
  9. data/lib/mobility/active_record.rb +14 -7
  10. data/lib/mobility/active_record/uniqueness_validator.rb +17 -3
  11. data/lib/mobility/attributes.rb +39 -16
  12. data/lib/mobility/backends/active_record/column/query_methods.rb +12 -11
  13. data/lib/mobility/backends/active_record/container.rb +116 -0
  14. data/lib/mobility/backends/active_record/container/query_methods.rb +31 -0
  15. data/lib/mobility/backends/active_record/hstore/query_methods.rb +7 -54
  16. data/lib/mobility/backends/active_record/jsonb/query_methods.rb +5 -55
  17. data/lib/mobility/backends/active_record/key_value.rb +13 -9
  18. data/lib/mobility/backends/active_record/key_value/query_methods.rb +6 -6
  19. data/lib/mobility/backends/active_record/pg_query_methods.rb +115 -0
  20. data/lib/mobility/backends/active_record/query_methods.rb +4 -3
  21. data/lib/mobility/backends/active_record/serialized/query_methods.rb +6 -5
  22. data/lib/mobility/backends/active_record/table.rb +18 -3
  23. data/lib/mobility/backends/active_record/table/query_methods.rb +25 -14
  24. data/lib/mobility/backends/container.rb +25 -0
  25. data/lib/mobility/backends/hash_valued.rb +2 -2
  26. data/lib/mobility/backends/null.rb +2 -2
  27. data/lib/mobility/backends/sequel/column.rb +2 -2
  28. data/lib/mobility/backends/sequel/column/query_methods.rb +2 -2
  29. data/lib/mobility/backends/sequel/container.rb +99 -0
  30. data/lib/mobility/backends/sequel/container/query_methods.rb +41 -0
  31. data/lib/mobility/backends/sequel/hstore/query_methods.rb +17 -3
  32. data/lib/mobility/backends/sequel/jsonb/query_methods.rb +17 -3
  33. data/lib/mobility/backends/sequel/key_value.rb +2 -1
  34. data/lib/mobility/backends/sequel/key_value/query_methods.rb +2 -2
  35. data/lib/mobility/backends/sequel/pg_hash.rb +4 -7
  36. data/lib/mobility/backends/sequel/pg_query_methods.rb +85 -0
  37. data/lib/mobility/backends/sequel/query_methods.rb +4 -3
  38. data/lib/mobility/backends/sequel/serialized/query_methods.rb +4 -3
  39. data/lib/mobility/backends/sequel/table.rb +1 -1
  40. data/lib/mobility/backends/sequel/table/query_methods.rb +2 -2
  41. data/lib/mobility/backends/serialized.rb +5 -7
  42. data/lib/mobility/configuration.rb +34 -4
  43. data/lib/mobility/plugins/active_record/dirty.rb +1 -1
  44. data/lib/mobility/plugins/fallbacks.rb +20 -6
  45. data/lib/mobility/sequel/column_changes.rb +2 -5
  46. data/lib/mobility/sequel/hash_initializer.rb +19 -0
  47. data/lib/mobility/util.rb +0 -2
  48. data/lib/mobility/version.rb +1 -1
  49. metadata +11 -4
  50. metadata.gz.sig +0 -0
  51. data/lib/mobility/backends/sequel/postgres_query_methods.rb +0 -41
@@ -5,17 +5,10 @@ module Mobility
5
5
  class ActiveRecord::Column::QueryMethods < ActiveRecord::QueryMethods
6
6
  def initialize(attributes, _)
7
7
  super
8
- attributes_extractor = @attributes_extractor
9
- @opts_converter = opts_converter = lambda do |opts|
10
- if i18n_keys = attributes_extractor.call(opts)
11
- opts = opts.with_indifferent_access
12
- i18n_keys.each { |attr| opts[Column.column_name_for(attr)] = opts.delete(attr) }
13
- end
14
- return opts
15
- end
8
+ q = self
16
9
 
17
10
  define_method :where! do |opts, *rest|
18
- super(opts_converter.call(opts), *rest)
11
+ super(q.convert_opts(opts), *rest)
19
12
  end
20
13
 
21
14
  attributes.each do |attribute|
@@ -27,15 +20,23 @@ module Mobility
27
20
 
28
21
  def extended(relation)
29
22
  super
30
- opts_converter = @opts_converter
23
+ q = self
31
24
 
32
25
  mod = Module.new do
33
26
  define_method :not do |opts, *rest|
34
- super(opts_converter.call(opts), *rest)
27
+ super(q.convert_opts(opts), *rest)
35
28
  end
36
29
  end
37
30
  relation.mobility_where_chain.include(mod)
38
31
  end
32
+
33
+ def convert_opts(opts)
34
+ if i18n_keys = extract_attributes(opts)
35
+ opts = opts.with_indifferent_access
36
+ i18n_keys.each { |attr| opts[Column.column_name_for(attr)] = opts.delete(attr) }
37
+ end
38
+ opts
39
+ end
39
40
  end
40
41
  end
41
42
  end
@@ -0,0 +1,116 @@
1
+ module Mobility
2
+ module Backends
3
+ =begin
4
+
5
+ Implements the {Mobility::Backends::Container} backend for ActiveRecord models.
6
+
7
+ =end
8
+ class ActiveRecord::Container
9
+ include ActiveRecord
10
+
11
+ require 'mobility/backends/active_record/container/query_methods'
12
+
13
+ # @return [Symbol] name of container column
14
+ attr_reader :column_name
15
+
16
+ # @!macro backend_constructor
17
+ # @option options [Symbol] column_name Name of container column
18
+ def initialize(model, attribute, options = {})
19
+ super
20
+ @column_name = options[:column_name]
21
+ end
22
+
23
+ # @!group Backend Accessors
24
+ #
25
+ # @note Translation may be a string, integer, boolean, hash or array
26
+ # since value is stored on a JSON hash.
27
+ # @param [Symbol] locale Locale to read
28
+ # @param [Hash] options
29
+ # @return [String,Integer,Boolean] Value of translation
30
+ def read(locale, _ = nil)
31
+ model_translations(locale)[attribute]
32
+ end
33
+
34
+ # @note Translation may be a string, integer, boolean, hash or array
35
+ # since value is stored on a JSON hash.
36
+ # @param [Symbol] locale Locale to write
37
+ # @param [String,Integer,Boolean] value Value to write
38
+ # @param [Hash] options
39
+ # @return [String,Integer,Boolean] Updated value
40
+ def write(locale, value, _ = nil)
41
+ set_attribute_translation(locale, value)
42
+ model_translations(locale)[attribute]
43
+ end
44
+ # @!endgroup
45
+
46
+ # @!group Backend Configuration
47
+ # @option options [Symbol] column_name (:translations) Name of column on which to store translations
48
+ def self.configure(options)
49
+ options[:column_name] ||= :translations
50
+ end
51
+ # @!endgroup
52
+
53
+ # @!macro backend_iterator
54
+ def each_locale
55
+ model[column_name].each do |l, v|
56
+ yield l.to_sym if v.present?
57
+ end
58
+ end
59
+
60
+ setup do |_attributes, options|
61
+ store options[:column_name], coder: Coder
62
+
63
+ # Fix for duping depth-2 jsonb column in AR < 5.0
64
+ if ::ActiveRecord::VERSION::STRING < '5.0'
65
+ column_name = options[:column_name]
66
+ module_name = "MobilityArContainer#{column_name.to_s.camelcase}"
67
+ unless const_defined?(module_name)
68
+ dupable = Module.new do
69
+ class_eval <<-EOM, __FILE__, __LINE__ + 1
70
+ def initialize_dup(source)
71
+ super
72
+ self.#{column_name} = source.#{column_name}.deep_dup
73
+ end
74
+ EOM
75
+ end
76
+ include const_set(module_name, dupable)
77
+ end
78
+ end
79
+ end
80
+
81
+ setup_query_methods(QueryMethods)
82
+
83
+ private
84
+
85
+ def model_translations(locale)
86
+ model[column_name][locale] ||= {}
87
+ end
88
+
89
+ def set_attribute_translation(locale, value)
90
+ translations = model[column_name] || {}
91
+ translations[locale.to_s] ||= {}
92
+ translations[locale.to_s][attribute] = value
93
+ model[column_name] = translations
94
+ end
95
+
96
+ class Coder
97
+ def self.dump(obj)
98
+ if obj.is_a? Hash
99
+ obj.inject({}) do |translations, (locale, value)|
100
+ value.each do |k, v|
101
+ (translations[locale] ||= {})[k] = v if v.present?
102
+ end
103
+ translations
104
+ end
105
+ else
106
+ raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}"
107
+ end
108
+ end
109
+
110
+ def self.load(obj)
111
+ obj
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,31 @@
1
+ require "mobility/backends/active_record/jsonb"
2
+
3
+ module Mobility
4
+ module Backends
5
+ class ActiveRecord::Container::QueryMethods < ActiveRecord::QueryMethods
6
+ include ActiveRecord::PgQueryMethods
7
+ attr_reader :column_name, :column
8
+
9
+ def initialize(_attributes, options)
10
+ super
11
+ @column_name = options[:column_name]
12
+ @column = arel_table[@column_name]
13
+ end
14
+
15
+ private
16
+
17
+ def contains_value(key, value, locale)
18
+ build_infix(:'@>',
19
+ build_infix(:'->', column, quote(locale)),
20
+ quote({ key => value }.to_json))
21
+ end
22
+
23
+ def has_locale(key, locale)
24
+ build_infix(:'?', column, quote(locale)).and(
25
+ build_infix(:'?',
26
+ build_infix(:'->', column, quote(locale)),
27
+ quote(key)))
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,62 +1,15 @@
1
+ require 'mobility/backends/active_record/pg_query_methods'
2
+ require 'mobility/backends/active_record/query_methods'
3
+
1
4
  module Mobility
2
5
  module Backends
3
6
  class ActiveRecord::Hstore::QueryMethods < ActiveRecord::QueryMethods
4
- def initialize(attributes, _)
5
- super
6
- attributes_extractor = @attributes_extractor
7
-
8
- define_method :where! do |opts, *rest|
9
- if i18n_keys = attributes_extractor.call(opts)
10
- m = arel_table
11
- locale = Arel::Nodes.build_quoted(Mobility.locale.to_s)
12
- opts = opts.with_indifferent_access
13
- infix = Arel::Nodes::InfixOperation
14
-
15
- i18n_query = i18n_keys.map { |key|
16
- column = m[key.to_sym]
17
- value = opts.delete(key)
18
-
19
- if value.nil?
20
- infix.new(:'?', column, locale).not
21
- else
22
- infix.new(:'->', m[key.to_sym], locale).eq(value)
23
- end
24
- }.inject(&:and)
25
-
26
- opts.empty? ? super(i18n_query) : super(opts, *rest).where(i18n_query)
27
- else
28
- super(opts, *rest)
29
- end
30
- end
31
- end
32
-
33
- def extended(relation)
34
- super
35
- attributes_extractor = @attributes_extractor
36
- m = relation.model.arel_table
37
-
38
- mod = Module.new do
39
- define_method :not do |opts, *rest|
40
- if i18n_keys = attributes_extractor.call(opts)
41
- locale = Arel::Nodes.build_quoted(Mobility.locale.to_s)
42
- opts = opts.with_indifferent_access
43
- infix = Arel::Nodes::InfixOperation
7
+ include ActiveRecord::PgQueryMethods
44
8
 
45
- i18n_query = i18n_keys.map { |key|
46
- column = m[key.to_sym]
47
- value = Arel::Nodes.build_quoted(opts.delete(key).to_s)
48
- has_key = infix.new(:'?', column, locale)
49
- not_eq_value = infix.new(:'->', column, locale).not_eq(value)
50
- has_key.and(not_eq_value)
51
- }.inject(&:and)
9
+ private
52
10
 
53
- super(opts, *rest).where(i18n_query)
54
- else
55
- super(opts, *rest)
56
- end
57
- end
58
- end
59
- relation.mobility_where_chain.include(mod)
11
+ def contains_value(key, value, locale)
12
+ build_infix(:'->', arel_table[key], quote(locale)).eq(quote(value.to_s))
60
13
  end
61
14
  end
62
15
  end
@@ -1,65 +1,15 @@
1
+ require 'mobility/backends/active_record/pg_query_methods'
1
2
  require "mobility/backends/active_record/query_methods"
2
3
 
3
4
  module Mobility
4
5
  module Backends
5
6
  class ActiveRecord::Jsonb::QueryMethods < ActiveRecord::QueryMethods
6
- def initialize(attributes, _)
7
- super
8
- attributes_extractor = @attributes_extractor
7
+ include ActiveRecord::PgQueryMethods
9
8
 
10
- define_method :where! do |opts, *rest|
11
- if i18n_keys = attributes_extractor.call(opts)
12
- m = arel_table
13
- locale = Arel::Nodes.build_quoted(Mobility.locale.to_s)
14
- opts = opts.with_indifferent_access
15
- infix = Arel::Nodes::InfixOperation
9
+ private
16
10
 
17
- i18n_query = i18n_keys.map { |key|
18
- column = m[key.to_sym]
19
- value = opts.delete(key)
20
-
21
- if value.nil?
22
- infix.new(:'?', column, locale).not
23
- else
24
- predicate = Arel::Nodes.build_quoted({ Mobility.locale => value }.to_json)
25
- infix.new(:'@>', m[key.to_sym], predicate)
26
- end
27
- }.inject(&:and)
28
-
29
- opts.empty? ? super(i18n_query) : super(opts, *rest).where(i18n_query)
30
- else
31
- super(opts, *rest)
32
- end
33
- end
34
- end
35
-
36
- def extended(relation)
37
- super
38
- attributes_extractor = @attributes_extractor
39
- m = relation.model.arel_table
40
-
41
- mod = Module.new do
42
- define_method :not do |opts, *rest|
43
- if i18n_keys = attributes_extractor.call(opts)
44
- locale = Arel::Nodes.build_quoted(Mobility.locale.to_s)
45
- opts = opts.with_indifferent_access
46
- infix = Arel::Nodes::InfixOperation
47
-
48
- i18n_query = i18n_keys.map { |key|
49
- column = m[key.to_sym]
50
- has_key = infix.new(:'?', column, locale)
51
- predicate = Arel::Nodes.build_quoted({ Mobility.locale => opts.delete(key) }.to_json)
52
- not_eq_value = infix.new(:'@>', m[key.to_sym], predicate).not
53
- has_key.and(not_eq_value)
54
- }.inject(&:and)
55
-
56
- super(opts, *rest).where(i18n_query)
57
- else
58
- super(opts, *rest)
59
- end
60
- end
61
- end
62
- relation.mobility_where_chain.include(mod)
11
+ def contains_value(key, value, locale)
12
+ build_infix(:'@>', arel_table[key], quote({locale => value }.to_json))
63
13
  end
64
14
  end
65
15
  end
@@ -83,15 +83,7 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
83
83
  include const_set(module_name, callback_methods)
84
84
  end
85
85
 
86
- private
87
-
88
- # Clean up *all* leftover translations of this model, only once.
89
- def mobility_destroy_key_value_translations
90
- [:string, :text].freeze.each do |type|
91
- Mobility::ActiveRecord.const_get("#{type.capitalize}Translation".freeze).
92
- where(translatable: self).destroy_all
93
- end
94
- end unless private_instance_methods(false).include?(:mobility_destroy_key_value_translations)
86
+ include DestroyKeyValueTranslations
95
87
  end
96
88
 
97
89
  setup_query_methods(QueryMethods)
@@ -104,6 +96,18 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
104
96
  translation ||= translations.build(locale: locale, key: attribute)
105
97
  translation
106
98
  end
99
+
100
+ module DestroyKeyValueTranslations
101
+ private
102
+
103
+ # Clean up *all* leftover translations of this model, only once.
104
+ def mobility_destroy_key_value_translations
105
+ [:string, :text].freeze.each do |type|
106
+ Mobility::ActiveRecord.const_get("#{type.capitalize}Translation".freeze).
107
+ where(translatable: self).destroy_all
108
+ end
109
+ end
110
+ end
107
111
  end
108
112
  end
109
113
  end
@@ -19,12 +19,12 @@ module Mobility
19
19
 
20
20
  def extended(relation)
21
21
  super
22
- association_name = @association_name
23
- attributes_extractor = @attributes_extractor
22
+ association_name = @association_name
23
+ q = self
24
24
 
25
25
  mod = Module.new do
26
26
  define_method :not do |opts, *rest|
27
- if i18n_keys = attributes_extractor.call(opts)
27
+ if i18n_keys = q.extract_attributes(opts)
28
28
  opts = opts.with_indifferent_access
29
29
  i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
30
30
  super(opts, *rest).send(:"join_#{association_name}", *i18n_keys)
@@ -54,12 +54,12 @@ module Mobility
54
54
  end
55
55
 
56
56
  def define_query_methods(association_name)
57
- attributes_extractor = @attributes_extractor
57
+ q = self
58
58
 
59
59
  define_method :where! do |opts, *rest|
60
- if i18n_keys = attributes_extractor.call(opts)
60
+ if i18n_keys = q.extract_attributes(opts)
61
61
  opts = opts.with_indifferent_access
62
- i18n_nulls = i18n_keys.select { |key| opts[key].nil? }
62
+ i18n_nulls = i18n_keys.reject { |key| opts[key] && Array.wrap(opts[key]).all? }
63
63
  i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
64
64
  super(opts, *rest).
65
65
  send("join_#{association_name}", *(i18n_keys - i18n_nulls)).
@@ -0,0 +1,115 @@
1
+ module Mobility
2
+ module Backends
3
+ module ActiveRecord
4
+ =begin
5
+
6
+ Defines query methods for Postgres backends. Including class must define a
7
+ single method, +contains_value+, which accepts a column, value and locale to
8
+ match, and returns an Arel node.
9
+
10
+ This module avoids 99% duplication between hstore and jsonb backend querying
11
+ code.
12
+
13
+ @see Mobility::Backends::ActiveRecord::Jsonb::QueryMethods
14
+ @see Mobility::Backends::ActiveRecord::Hstore::QueryMethods
15
+
16
+ =end
17
+ module PgQueryMethods
18
+ attr_reader :arel_table
19
+
20
+ def initialize(attributes, options)
21
+ super
22
+ @arel_table = options[:model_class].arel_table
23
+
24
+ q = self
25
+
26
+ define_method :where! do |opts, *rest|
27
+ if i18n_keys = q.extract_attributes(opts)
28
+ opts = opts.with_indifferent_access
29
+ query = q.create_where_query!(opts, i18n_keys)
30
+
31
+ opts.empty? ? super(query) : super(opts, *rest).where(query)
32
+ else
33
+ super(opts, *rest)
34
+ end
35
+ end
36
+ end
37
+
38
+ def extended(relation)
39
+ super
40
+ q = self
41
+
42
+ mod = Module.new do
43
+ define_method :not do |opts, *rest|
44
+ if i18n_keys = q.extract_attributes(opts)
45
+ opts = opts.with_indifferent_access
46
+ query = q.create_not_query!(opts, i18n_keys)
47
+
48
+ super(opts, *rest).where(query)
49
+ else
50
+ super(opts, *rest)
51
+ end
52
+ end
53
+ end
54
+ relation.mobility_where_chain.include(mod)
55
+ end
56
+
57
+ # Create +where+ query for options hash, translated keys and arel_table
58
+ # @note This is a destructive operation, it will modify +opts+.
59
+ #
60
+ # @param [Hash] opts Hash of attribute/value pairs
61
+ # @param [Array] keys Translated attribute names
62
+ # @return [Arel::Node] Arel node to pass to +where+
63
+ def create_where_query!(opts, keys)
64
+ locale = Mobility.locale
65
+ keys.map { |key|
66
+ values = opts.delete(key)
67
+
68
+ next has_locale(key, locale).not if values.nil?
69
+
70
+ Array.wrap(values).map { |value|
71
+ value.nil? ?
72
+ has_locale(key, locale).not :
73
+ contains_value(key, value, locale)
74
+ }.inject(&:or)
75
+ }.inject(&:and)
76
+ end
77
+
78
+ # Create +not+ query for options hash and translated keys
79
+ # @note This is a destructive operation, it will modify +opts+.
80
+ #
81
+ # @param [Hash] opts Hash of attribute/value pairs
82
+ # @param [Array] keys Translated attribute names
83
+ # @return [Arel::Node] Arel node to pass to +where+
84
+ def create_not_query!(opts, keys)
85
+ locale = Mobility.locale
86
+ keys.map { |key|
87
+ values = opts.delete(key)
88
+
89
+ Array.wrap(values).map { |value|
90
+ contains_value(key, value, locale).not
91
+ }.inject(has_locale(key, locale), &:and)
92
+ }.inject(&:and)
93
+ end
94
+
95
+ private
96
+
97
+ def contains_value(_key, _value, _locale)
98
+ raise NotImplementedError
99
+ end
100
+
101
+ def has_locale(key, locale)
102
+ build_infix(:'?', arel_table[key], quote(locale))
103
+ end
104
+
105
+ def build_infix(*args)
106
+ Arel::Nodes::InfixOperation.new(*args)
107
+ end
108
+
109
+ def quote(value)
110
+ Arel::Nodes.build_quoted(value.to_s)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end