mobility 0.5.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -2
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +41 -1
  5. data/Gemfile.lock +3 -58
  6. data/README.md +22 -21
  7. data/lib/mobility.rb +3 -2
  8. data/lib/mobility/accumulator.rb +1 -2
  9. data/lib/mobility/active_model/backend_resetter.rb +1 -1
  10. data/lib/mobility/active_record.rb +12 -9
  11. data/lib/mobility/active_record/backend_resetter.rb +6 -7
  12. data/lib/mobility/active_record/uniqueness_validator.rb +12 -2
  13. data/lib/mobility/adapter.rb +1 -0
  14. data/lib/mobility/attributes.rb +3 -13
  15. data/lib/mobility/backends/active_record/column.rb +1 -0
  16. data/lib/mobility/backends/active_record/column/query_methods.rb +25 -20
  17. data/lib/mobility/backends/active_record/container/json_query_methods.rb +22 -16
  18. data/lib/mobility/backends/active_record/container/jsonb_query_methods.rb +19 -19
  19. data/lib/mobility/backends/active_record/hstore.rb +14 -12
  20. data/lib/mobility/backends/active_record/hstore/query_methods.rb +14 -5
  21. data/lib/mobility/backends/active_record/json.rb +21 -19
  22. data/lib/mobility/backends/active_record/json/query_methods.rb +16 -11
  23. data/lib/mobility/backends/active_record/jsonb.rb +21 -19
  24. data/lib/mobility/backends/active_record/jsonb/query_methods.rb +14 -5
  25. data/lib/mobility/backends/active_record/key_value.rb +9 -9
  26. data/lib/mobility/backends/active_record/key_value/query_methods.rb +53 -46
  27. data/lib/mobility/backends/active_record/pg_hash.rb +29 -25
  28. data/lib/mobility/backends/active_record/pg_query_methods.rb +76 -40
  29. data/lib/mobility/backends/active_record/query_methods.rb +17 -10
  30. data/lib/mobility/backends/active_record/serialized.rb +4 -2
  31. data/lib/mobility/backends/active_record/serialized/query_methods.rb +18 -15
  32. data/lib/mobility/backends/active_record/table.rb +21 -12
  33. data/lib/mobility/backends/active_record/table/query_methods.rb +82 -83
  34. data/lib/mobility/backends/hash_valued.rb +19 -0
  35. data/lib/mobility/backends/hstore.rb +3 -1
  36. data/lib/mobility/backends/json.rb +3 -1
  37. data/lib/mobility/backends/jsonb.rb +3 -1
  38. data/lib/mobility/backends/key_value.rb +32 -15
  39. data/lib/mobility/backends/sequel/column/query_methods.rb +16 -12
  40. data/lib/mobility/backends/sequel/container/json_query_methods.rb +25 -18
  41. data/lib/mobility/backends/sequel/container/jsonb_query_methods.rb +25 -18
  42. data/lib/mobility/backends/sequel/hstore.rb +14 -12
  43. data/lib/mobility/backends/sequel/hstore/query_methods.rb +18 -11
  44. data/lib/mobility/backends/sequel/json.rb +21 -19
  45. data/lib/mobility/backends/sequel/json/query_methods.rb +18 -11
  46. data/lib/mobility/backends/sequel/jsonb.rb +21 -19
  47. data/lib/mobility/backends/sequel/jsonb/query_methods.rb +18 -11
  48. data/lib/mobility/backends/sequel/key_value.rb +10 -11
  49. data/lib/mobility/backends/sequel/key_value/query_methods.rb +39 -34
  50. data/lib/mobility/backends/sequel/pg_hash.rb +37 -25
  51. data/lib/mobility/backends/sequel/pg_query_methods.rb +45 -20
  52. data/lib/mobility/backends/sequel/query_methods.rb +5 -0
  53. data/lib/mobility/backends/sequel/serialized.rb +18 -13
  54. data/lib/mobility/backends/sequel/serialized/query_methods.rb +10 -7
  55. data/lib/mobility/backends/sequel/table.rb +1 -1
  56. data/lib/mobility/backends/sequel/table/query_methods.rb +40 -35
  57. data/lib/mobility/plugins/cache/translation_cacher.rb +15 -15
  58. data/lib/mobility/plugins/default.rb +0 -7
  59. data/lib/mobility/plugins/fallbacks.rb +4 -0
  60. data/lib/mobility/sequel.rb +11 -5
  61. data/lib/mobility/sequel/backend_resetter.rb +6 -7
  62. data/lib/mobility/sequel/column_changes.rb +4 -4
  63. data/lib/mobility/version.rb +1 -1
  64. data/lib/rails/generators/mobility/backend_generators/base.rb +4 -0
  65. data/lib/rails/generators/mobility/backend_generators/table_backend.rb +0 -12
  66. data/lib/rails/generators/mobility/templates/column_translations.rb +2 -2
  67. data/lib/rails/generators/mobility/templates/create_string_translations.rb +5 -5
  68. data/lib/rails/generators/mobility/templates/create_text_translations.rb +5 -5
  69. data/lib/rails/generators/mobility/templates/initializer.rb +8 -0
  70. data/lib/rails/generators/mobility/templates/table_migration.rb +2 -3
  71. data/lib/rails/generators/mobility/templates/table_translations.rb +3 -4
  72. data/lib/rails/generators/mobility/translations_generator.rb +6 -5
  73. metadata +2 -3
  74. metadata.gz.sig +0 -0
  75. data/lib/mobility/backend/stringify_locale.rb +0 -18
@@ -12,6 +12,7 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
12
12
 
13
13
  @example
14
14
  class Post < ActiveRecord::Base
15
+ extend Mobility
15
16
  translates :title, backend: :key_value, association_name: :translations, type: :string
16
17
  end
17
18
 
@@ -31,17 +32,16 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
31
32
  require 'mobility/backends/active_record/key_value/query_methods'
32
33
 
33
34
  # @!group Backend Configuration
34
- # @option options [Symbol] type (:text) Column type to use
35
- # @option options [Symbol] association_name (:text_translations) Name of association method
36
- # @option options [String,Class] class_name ({Mobility::ActiveRecord::TextTranslation}) Translation class
37
- # @raise [ArgumentError] if type is not either :text or :string
35
+ # @option (see Mobility::Backends::KeyValue::ClassMethods#configure)
36
+ # @raise (see Mobility::Backends::KeyValue::ClassMethods#configure)
38
37
  def self.configure(options)
39
38
  super
40
- type = options[:type]
41
- options[:class_name] ||= Mobility::ActiveRecord.const_get("#{type.capitalize}Translation")
42
- options[:class_name] = options[:class_name].constantize if options[:class_name].is_a?(String)
43
- options[:association_name] ||= :"#{options[:type]}_translations"
44
- %i[type association_name].each { |key| options[key] = options[key].to_sym }
39
+ if type = options[:type]
40
+ options[:association_name] ||= :"#{options[:type]}_translations"
41
+ options[:class_name] ||= Mobility::ActiveRecord.const_get("#{type.capitalize}Translation")
42
+ end
43
+ rescue NameError
44
+ raise ArgumentError, "You must define a Mobility::ActiveRecord::#{type.capitalize}Translation class."
45
45
  end
46
46
  # @!endgroup
47
47
 
@@ -3,67 +3,74 @@ require "mobility/backends/active_record/query_methods"
3
3
 
4
4
  module Mobility
5
5
  module Backends
6
- class ActiveRecord::KeyValue::QueryMethods < ActiveRecord::QueryMethods
7
- def initialize(attributes, association_name: nil, class_name: nil, **)
8
- super
9
- @association_name = association_name
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
10
11
 
11
- define_join_method(association_name, class_name)
12
- define_query_methods(association_name)
13
- end
12
+ define_join_method(association_name, class_name)
13
+ define_query_methods(association_name)
14
+ end
14
15
 
15
- def extended(relation)
16
- super
17
- association_name = @association_name
18
- q = self
16
+ def extended(relation)
17
+ super
18
+ association_name = @association_name
19
+ q = self
19
20
 
20
- mod = Module.new do
21
- define_method :not do |opts, *rest|
22
- if i18n_keys = q.extract_attributes(opts)
23
- opts = opts.with_indifferent_access
24
- i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
25
- super(opts, *rest).send(:"join_#{association_name}", *i18n_keys)
26
- else
27
- super(opts, *rest)
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
28
32
  end
29
33
  end
34
+ relation.mobility_where_chain.include(mod)
30
35
  end
31
- relation.mobility_where_chain.include(mod)
32
- end
33
36
 
34
- private
37
+ private
35
38
 
36
- def define_join_method(association_name, translation_class)
37
- define_method :"join_#{association_name}" do |*attributes, **options|
38
- attributes.inject(self) do |relation, attribute|
39
- t = translation_class.arel_table.alias(:"#{attribute}_#{association_name}")
40
- m = arel_table
41
- join_type = options[:outer_join] ? Arel::Nodes::OuterJoin : Arel::Nodes::InnerJoin
42
- relation.joins(m.join(t, join_type).
43
- on(t[:key].eq(attribute).
44
- and(t[:locale].eq(Mobility.locale).
45
- and(t[:translatable_type].eq(base_class.name).
46
- and(t[:translatable_id].eq(m[:id]))))).join_sources)
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
47
51
  end
48
52
  end
49
- end
50
53
 
51
- def define_query_methods(association_name)
52
- q = self
54
+ def define_query_methods(association_name)
55
+ q = self
53
56
 
54
- define_method :where! do |opts, *rest|
55
- if i18n_keys = q.extract_attributes(opts)
56
- opts = opts.with_indifferent_access
57
- i18n_nulls = i18n_keys.reject { |key| opts[key] && Array(opts[key]).all? }
58
- i18n_keys.each { |attr| opts["#{attr}_#{association_name}"] = { value: opts.delete(attr) }}
59
- super(opts, *rest).
60
- send("join_#{association_name}", *(i18n_keys - i18n_nulls)).
61
- send("join_#{association_name}", *i18n_nulls, outer_join: true)
62
- else
63
- super(opts, *rest)
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
64
70
  end
65
71
  end
66
72
  end
73
+ KeyValue.private_constant :QueryMethods
67
74
  end
68
75
  end
69
76
  end
@@ -10,39 +10,43 @@ Internal class used by ActiveRecord backends backed by a Postgres data type
10
10
  (hstore, jsonb).
11
11
 
12
12
  =end
13
- class ActiveRecord::PgHash
14
- include ActiveRecord
15
- include HashValued
16
-
17
- # @!macro backend_iterator
18
- def each_locale
19
- super { |l| yield l.to_sym }
20
- end
13
+ module ActiveRecord
14
+ class PgHash
15
+ include ActiveRecord
16
+ include HashValued
17
+
18
+ # @!macro backend_iterator
19
+ def each_locale
20
+ super { |l| yield l.to_sym }
21
+ end
21
22
 
22
- def translations
23
- model.read_attribute(attribute)
24
- end
23
+ def translations
24
+ model.read_attribute(column_name)
25
+ end
25
26
 
26
- setup do |attributes|
27
- attributes.each { |attribute| store attribute, coder: Coder }
28
- end
27
+ setup do |attributes, options = {}|
28
+ affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
29
+ attributes.each { |attribute| store (affix % attribute), coder: Coder }
30
+ end
29
31
 
30
- class Coder
31
- def self.dump(obj)
32
- if obj.is_a? Hash
33
- obj.inject({}) do |translations, (locale, value)|
34
- translations[locale] = value if value.present?
35
- translations
32
+ class Coder
33
+ def self.dump(obj)
34
+ if obj.is_a? Hash
35
+ obj.inject({}) do |translations, (locale, value)|
36
+ translations[locale] = value if value.present?
37
+ translations
38
+ end
39
+ else
40
+ raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}"
36
41
  end
37
- else
38
- raise ArgumentError, "Attribute is supposed to be a Hash, but was a #{obj.class}. -- #{obj.inspect}"
39
42
  end
40
- end
41
43
 
42
- def self.load(obj)
43
- obj
44
+ def self.load(obj)
45
+ obj
46
+ end
44
47
  end
45
48
  end
49
+ private_constant :PgHash
46
50
  end
47
51
  end
48
52
  end
@@ -1,11 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Mobility
2
4
  module Backends
3
5
  module ActiveRecord
4
6
  =begin
5
7
 
6
- Defines query methods for Postgres backends. Including class must define a
7
- single method, +matches+, which accepts a column, value and locale to
8
- match, and returns an Arel node.
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+.
9
21
 
10
22
  This module avoids a lot of duplication between hstore/json/jsonb/container
11
23
  backend querying code.
@@ -18,18 +30,19 @@ backend querying code.
18
30
 
19
31
  =end
20
32
  module PgQueryMethods
21
- attr_reader :arel_table
33
+ attr_reader :arel_table, :column_affix
22
34
 
23
35
  def initialize(attributes, options)
24
36
  super
25
- @arel_table = options[:model_class].arel_table
37
+ @arel_table = options[:model_class].arel_table
38
+ @column_affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
26
39
 
27
40
  q = self
28
41
 
29
42
  define_method :where! do |opts, *rest|
30
43
  if i18n_keys = q.extract_attributes(opts)
31
44
  opts = opts.with_indifferent_access
32
- query = q.create_where_query!(opts, i18n_keys)
45
+ query = q.create_query!(opts, i18n_keys)
33
46
 
34
47
  opts.empty? ? super(query) : super(opts, *rest).where(query)
35
48
  else
@@ -46,7 +59,7 @@ backend querying code.
46
59
  define_method :not do |opts, *rest|
47
60
  if i18n_keys = q.extract_attributes(opts)
48
61
  opts = opts.with_indifferent_access
49
- query = q.create_not_query!(opts, i18n_keys)
62
+ query = q.create_query!(opts, i18n_keys, inverse: true)
50
63
 
51
64
  super(opts, *rest).where(query)
52
65
  else
@@ -57,62 +70,85 @@ backend querying code.
57
70
  relation.mobility_where_chain.include(mod)
58
71
  end
59
72
 
60
- # Create +where+ query for options hash, translated keys and arel_table
61
- # @note This is a destructive operation, it will modify +opts+.
73
+ # Create +where+ query for specified key and value
62
74
  #
75
+ # @note This is a destructive operation, it will modify +opts+.
63
76
  # @param [Hash] opts Hash of attribute/value pairs
64
77
  # @param [Array] keys Translated attribute names
78
+ # @option [Boolean] inverse (false) If true, create a +not+ query
79
+ # instead of a +where+ query
65
80
  # @return [Arel::Node] Arel node to pass to +where+
66
- def create_where_query!(opts, keys)
67
- locale = Mobility.locale
81
+ def create_query!(opts, keys, inverse: false)
68
82
  keys.map { |key|
69
- values = opts.delete(key)
70
-
71
- next has_locale(key, locale).not if values.nil?
72
-
73
- Array.wrap(values).map { |value|
74
- value.nil? ?
75
- has_locale(key, locale).not :
76
- matches(key, value, locale)
77
- }.inject(&:or)
83
+ values = Array.wrap(opts.delete(key)).uniq
84
+ send(inverse ? :not_query : :where_query, key, values, Mobility.locale)
78
85
  }.inject(&:and)
79
86
  end
80
87
 
81
- # Create +not+ query for options hash and translated keys
82
- # @note This is a destructive operation, it will modify +opts+.
83
- #
84
- # @param [Hash] opts Hash of attribute/value pairs
85
- # @param [Array] keys Translated attribute names
86
- # @return [Arel::Node] Arel node to pass to +where+
87
- def create_not_query!(opts, keys)
88
- locale = Mobility.locale
89
- keys.map { |key|
90
- values = opts.delete(key)
91
-
92
- Array.wrap(values).map { |value|
93
- matches(key, value, locale).not
94
- }.inject(has_locale(key, locale), &:and)
95
- }.inject(&:and)
88
+ def matches(_key, _locale)
89
+ raise NotImplementedError
96
90
  end
97
91
 
98
- private
92
+ def exists(_key, _locale)
93
+ raise NotImplementedError
94
+ end
99
95
 
100
- def matches(_key, _value, _locale)
96
+ def quote(_value)
101
97
  raise NotImplementedError
102
98
  end
103
99
 
104
- def has_locale(key, locale)
105
- build_infix(:'?', arel_table[key], quote(locale))
100
+ def absent(key, locale)
101
+ exists(key, locale).not
106
102
  end
107
103
 
104
+ private
105
+
108
106
  def build_infix(*args)
109
107
  arel_table.grouping(Arel::Nodes::InfixOperation.new(*args))
110
108
  end
111
109
 
112
- def quote(value)
110
+ def build_quoted(value)
113
111
  Arel::Nodes.build_quoted(value.to_s)
114
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
115
150
  end
151
+ private_constant :PgQueryMethods
116
152
  end
117
153
  end
118
154
  end
@@ -21,23 +21,30 @@ models. For details see backend-specific subclasses.
21
21
 
22
22
  # @param [ActiveRecord::Relation] relation Relation being extended
23
23
  # @note Only want to define this once, even if multiple QueryMethods
24
- # modules are included, so define it here in extended method
24
+ # modules are included, so include it here into the singleton class.
25
25
  def extended(relation)
26
- unless relation.methods(false).include?(:mobility_where_chain)
27
- relation.define_singleton_method(:mobility_where_chain) do
28
- @mobility_where_chain ||= Class.new(::ActiveRecord::QueryMethods::WhereChain)
29
- end
30
-
31
- relation.define_singleton_method :where do |opts = :chain, *rest|
32
- opts == :chain ? mobility_where_chain.new(spawn) : super(opts, *rest)
33
- end
34
- end
26
+ relation.singleton_class.include WhereChainable
35
27
  end
36
28
 
37
29
  def extract_attributes(opts)
38
30
  opts.is_a?(Hash) && (opts.keys.map(&:to_s) & @attributes).presence
39
31
  end
32
+
33
+ def collapse(value)
34
+ value.is_a?(Array) ? value.uniq : value
35
+ end
36
+ end
37
+
38
+ module WhereChainable
39
+ def where(opts = :chain, *rest)
40
+ opts == :chain ? mobility_where_chain.new(spawn) : super
41
+ end
42
+
43
+ def mobility_where_chain
44
+ @mobility_where_chain ||= Class.new(::ActiveRecord::QueryMethods::WhereChain)
45
+ end
40
46
  end
47
+ private_constant :QueryMethods, :WhereChainable
41
48
  end
42
49
  end
43
50
  end
@@ -11,6 +11,7 @@ Implements {Mobility::Backends::Serialized} backend for ActiveRecord models.
11
11
 
12
12
  @example Define attribute with serialized backend
13
13
  class Post < ActiveRecord::Base
14
+ extend Mobility
14
15
  translates :title, backend: :serialized, format: :yaml
15
16
  end
16
17
 
@@ -42,7 +43,8 @@ Implements {Mobility::Backends::Serialized} backend for ActiveRecord models.
42
43
 
43
44
  setup do |attributes, options|
44
45
  coder = { yaml: YAMLCoder, json: JSONCoder }[options[:format]]
45
- attributes.each { |attribute| serialize attribute, coder }
46
+ column_affix = "#{options[:column_prefix]}%s#{options[:column_suffix]}"
47
+ attributes.each { |attribute| serialize (column_affix % attribute), coder }
46
48
  end
47
49
 
48
50
  setup_query_methods(QueryMethods)
@@ -51,7 +53,7 @@ Implements {Mobility::Backends::Serialized} backend for ActiveRecord models.
51
53
  # Returns column value as a hash
52
54
  # @return [Hash]
53
55
  def translations
54
- model.read_attribute(attribute)
56
+ model.read_attribute(column_name)
55
57
  end
56
58
 
57
59
  %w[yaml json].each do |format|