mobility 0.3.6 → 0.4.0

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