mobility 1.0.7 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ddbf8662456ee4b547ed3ead85fd44a3af486cbe94d85ad77679f811a7a5b98d
4
- data.tar.gz: 62fed63232e0bf182db5fdf9994ca3d2f50b61872b9e3c0bf4d1b4c7cf739698
3
+ metadata.gz: b22675841d6b6330a4db417e3576767b42a81614b80cf376a208088ad9432de4
4
+ data.tar.gz: 1b2c44b38c87fc0b1e8ca5e463e9c265c09f0d307d0d663ba3aa39b6b5ca771b
5
5
  SHA512:
6
- metadata.gz: 449d4f885917546ffe9dab2678e39d597ecc340028e0e09293073487b8f8269ee097cea0b1ee28232cf04c05a1598d1e65be4d0be72b63b27f7e5cc543ab49bb
7
- data.tar.gz: 19f561713598217a2f8cf18aee0d906076086adf254995d7ddaccbd3e4d5b3db27f3d402f189d935538b1ef84d8e1fe33471af746a7c8d502095baa8f9cadd12
6
+ metadata.gz: 708d6ba16ec279e6a114abc811d107aa759590c711ca389d7f4b86407e100fbaed30a38b7b787ef98008291c480715949b0d4562efc2d5120eba73587fc375db
7
+ data.tar.gz: bd8f79dd23f7c482292c011bb800d7db20dc3b1fb06797b53fdf91ecdb9c9895c1634e3b1da06e90cff4e7b32e4aa5ebdf8f5d8b5c76f1fafbc08ca891ac99dc
checksums.yaml.gz.sig CHANGED
Binary file
data.tar.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Mobility Changelog
2
2
 
3
- ### Unreleased
3
+ ## 1.1
4
+
5
+ ### 1.1.0
4
6
  - Remove `Mobility::Plugins::Attributes#each`
5
7
  ([#475](https://github.com/shioyama/mobility/pull/475))
6
8
  - Add public method `Mobility::Plugins::ActiveRecord::Query.build_query`
data/Gemfile.lock CHANGED
@@ -1,13 +1,24 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- mobility (1.0.6)
4
+ mobility (1.0.7)
5
5
  i18n (>= 0.6.10, < 2)
6
6
  request_store (~> 1.0)
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
+ activemodel (6.0.3.4)
12
+ activesupport (= 6.0.3.4)
13
+ activerecord (6.0.3.4)
14
+ activemodel (= 6.0.3.4)
15
+ activesupport (= 6.0.3.4)
16
+ activesupport (6.0.3.4)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.2, >= 2.2.2)
11
22
  benchmark-ips (2.8.4)
12
23
  byebug (11.1.3)
13
24
  coderay (1.1.3)
@@ -37,6 +48,7 @@ GEM
37
48
  rb-inotify (~> 0.9, >= 0.9.10)
38
49
  lumberjack (1.2.8)
39
50
  method_source (1.0.0)
51
+ minitest (5.14.3)
40
52
  nenv (0.3.0)
41
53
  notiffany (0.1.3)
42
54
  nenv (~> 0.1)
@@ -68,15 +80,19 @@ GEM
68
80
  diff-lcs (>= 1.2.0, < 2.0)
69
81
  rspec-support (~> 3.10.0)
70
82
  rspec-support (3.10.2)
71
- sequel (5.41.0)
72
83
  shellany (0.0.1)
73
84
  thor (1.1.0)
85
+ thread_safe (0.3.6)
86
+ tzinfo (1.2.9)
87
+ thread_safe (~> 0.1)
74
88
  yard (0.9.26)
89
+ zeitwerk (2.4.2)
75
90
 
76
91
  PLATFORMS
77
92
  ruby
78
93
 
79
94
  DEPENDENCIES
95
+ activerecord (~> 6.0.0)
80
96
  benchmark-ips
81
97
  database_cleaner (~> 1.5, >= 1.5.3)
82
98
  guard-rspec
@@ -85,7 +101,6 @@ DEPENDENCIES
85
101
  pry-byebug
86
102
  rake (~> 12, >= 12.2.1)
87
103
  rspec (~> 3.0)
88
- sequel (~> 5.0)
89
104
  yard (~> 0.9.0)
90
105
 
91
106
  BUNDLED WITH
data/README.md CHANGED
@@ -52,17 +52,16 @@ section of the wiki.
52
52
  Installation
53
53
  ------------
54
54
 
55
- To use the latest pre-version of Mobility 1.0, add this line to your
56
- application's Gemfile:
55
+ Add this line to your application's Gemfile:
57
56
 
58
57
  ```ruby
59
- gem 'mobility', '~> 1.0.7'
58
+ gem 'mobility', '~> 1.1.0'
60
59
  ```
61
60
 
62
61
  ### ActiveRecord (Rails)
63
62
 
64
63
  Requirements:
65
- - ActiveRecord >= 5.0 (including 6.0)
64
+ - ActiveRecord >= 5.0 (including 6.x)
66
65
 
67
66
  (Support for most backends and features is also supported with
68
67
  ActiveRecord/Rails 4.2, but there are some tests still failing. To see exactly
data/lib/mobility.rb CHANGED
@@ -31,8 +31,9 @@ So the above example is equivalent to:
31
31
  `Mobility.translations_class` is a subclass of `Mobility::Translations` created
32
32
  when `Mobility.configure` is called to configure Mobility. In fact, when you
33
33
  call `Mobility.configure`, it is the subclass of `Mobility::Translations` which
34
- is passed to the block as `config`. Plugins and plugin configuration is all
35
- applied to the same `Mobility.translations_class`.
34
+ is passed to the block as `config` (or as `self` if no argument is passed to
35
+ the block). Plugins and plugin configuration is all applied to the same
36
+ `Mobility.translations_class`.
36
37
 
37
38
  There is another way to use Mobility, which is to create your own subclass or
38
39
  subclasses of +Mobility::Translations+ and include them explicitly, without
@@ -83,8 +84,6 @@ module Mobility
83
84
  require "mobility/plugins"
84
85
  require "mobility/translations"
85
86
 
86
- # General error for version compatibility conflicts
87
- class VersionNotSupportedError < ArgumentError; end
88
87
  CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
89
88
  private_constant :CALL_COMPILABLE_REGEXP
90
89
 
@@ -201,6 +200,14 @@ module Mobility
201
200
  end
202
201
  end
203
202
 
203
+ # Check that a non-nil locale is valid. (Does not actually parse locale to
204
+ # check its format.)
205
+ # @raise [InvalidLocale] if locale is not a Symbol or not available
206
+ def validate_locale!(locale)
207
+ raise Mobility::InvalidLocale.new(locale) unless Symbol === locale
208
+ enforce_available_locales!(locale) if I18n.enforce_available_locales
209
+ end
210
+
204
211
  # Raises InvalidLocale exception if the locale passed in is present but not available.
205
212
  # @param [String,Symbol] locale
206
213
  # @raise [InvalidLocale] if locale is present but not available
@@ -217,7 +224,7 @@ module Mobility
217
224
  # simply default to +I18n.available_locales+, we may define many more
218
225
  # methods (in LocaleAccessors) than is really necessary.
219
226
  def available_locales
220
- if defined?(Rails) && Rails.application
227
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
221
228
  Rails.application.config.i18n.available_locales&.map(&:to_sym) || I18n.available_locales
222
229
  else
223
230
  I18n.available_locales
@@ -231,8 +238,8 @@ module Mobility
231
238
  end
232
239
 
233
240
  def set_locale(locale)
234
- locale = locale.to_sym if locale
235
- enforce_available_locales!(locale) if I18n.enforce_available_locales
241
+ locale = locale.to_sym if String === locale
242
+ validate_locale!(locale) if locale
236
243
  storage[:mobility_locale] = locale
237
244
  end
238
245
  end
@@ -17,8 +17,10 @@ On top of this, a backend will normally:
17
17
  - implement a +write+ instance method to write to the backend
18
18
  - implement an +each_locale+ instance method to iterate through available locales
19
19
  (used to define other +Enumerable+ traversal and search methods)
20
+ - implement a +valid_keys+ class method returning an array of symbols
21
+ corresponding to valid keys for configuring this backend.
20
22
  - implement a +configure+ class method to apply any normalization to the
21
- options hash
23
+ keys on the options hash included in +valid_keys+
22
24
  - call the +setup+ method yielding attributes and options to configure the
23
25
  model class
24
26
 
@@ -37,7 +37,6 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
37
37
  options[:association_name] ||= :"#{options[:type]}_translations"
38
38
  options[:class_name] ||= const_get("#{type.capitalize}Translation")
39
39
  end
40
- options[:table_alias_affix] = "#{model_class}_%s_#{options[:association_name]}"
41
40
  rescue NameError
42
41
  raise ArgumentError, "You must define a Mobility::Backends::ActiveRecord::KeyValue::#{type.capitalize}Translation class."
43
42
  end
@@ -49,7 +48,7 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
49
48
  # translation table value column
50
49
  def build_node(attr, locale)
51
50
  aliased_table = class_name.arel_table.alias(table_alias(attr, locale))
52
- Plugins::Arel::Attribute.new(aliased_table, :value, locale, self, attr.to_sym)
51
+ Plugins::Arel::Attribute.new(aliased_table, value_column, locale, self, attr.to_sym)
53
52
  end
54
53
 
55
54
  # Joins translations using either INNER/OUTER join appropriate to the query.
@@ -73,10 +72,10 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
73
72
  m = model_class.arel_table
74
73
  t = class_name.arel_table.alias(table_alias(key, locale))
75
74
  relation.joins(m.join(t, join_type).
76
- on(t[:key].eq(key).
75
+ on(t[key_column].eq(key).
77
76
  and(t[:locale].eq(locale).
78
- and(t[:translatable_type].eq(model_class.base_class.name).
79
- and(t[:translatable_id].eq(m[:id]))))).join_sources)
77
+ and(t[:"#{belongs_to}_type"].eq(model_class.base_class.name).
78
+ and(t[:"#{belongs_to}_id"].eq(m[:id]))))).join_sources)
80
79
  end
81
80
 
82
81
  def already_joined?(relation, name, locale, join_type)
@@ -151,8 +150,11 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
151
150
  end
152
151
 
153
152
  setup do |attributes, options|
154
- association_name = options[:association_name]
155
- translations_class = options[:class_name]
153
+ association_name = options[:association_name]
154
+ translation_class = options[:class_name]
155
+ key_column = options[:key_column]
156
+ value_column = options[:value_column]
157
+ belongs_to = options[:belongs_to]
156
158
 
157
159
  # Track all attributes for this association, so that we can limit the scope
158
160
  # of keys for the association to only these attributes. We need to track the
@@ -163,13 +165,13 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
163
165
  association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
164
166
  instance_variable_set(:"@#{attrs_method_name}", association_attributes)
165
167
 
166
- has_many association_name, ->{ where key: association_attributes },
167
- as: :translatable,
168
- class_name: translations_class.name,
169
- inverse_of: :translatable,
168
+ has_many association_name, ->{ where key_column => association_attributes },
169
+ as: belongs_to,
170
+ class_name: translation_class.name,
171
+ inverse_of: belongs_to,
170
172
  autosave: true
171
173
  before_save do
172
- send(association_name).select { |t| t.value.blank? }.each do |translation|
174
+ send(association_name).select { |t| t.send(value_column).blank? }.each do |translation|
173
175
  send(association_name).destroy(translation)
174
176
  end
175
177
  end
@@ -181,33 +183,32 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
181
183
  super(source)
182
184
  self.send("#{association_name}=", source.send(association_name).map(&:dup))
183
185
  # Set inverse on associations
184
- send(association_name).each { |translation| translation.translatable = self }
186
+ send(association_name).each do |translation|
187
+ translation.send(:"#{belongs_to}=", self)
188
+ end
185
189
  end
186
190
  end
187
191
  include const_set(module_name, callback_methods)
188
192
  end
189
193
 
190
- include DestroyKeyValueTranslations
194
+ # Ensure we only call after destroy hook once per translations class
195
+ translation_classes = [translation_class, *Mobility::Backends::ActiveRecord::KeyValue::Translation.descendants].uniq
196
+ after_destroy do
197
+ @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes)
198
+ (translation_classes - @mobility_after_destroy_translation_classes).each { |klass| klass.where(belongs_to => self).destroy_all }
199
+ @mobility_after_destroy_translation_classes += translation_classes
200
+ end
191
201
  end
192
202
 
193
203
  # Returns translation for a given locale, or builds one if none is present.
194
204
  # @param [Symbol] locale
195
205
  # @return [Mobility::Backends::ActiveRecord::KeyValue::TextTranslation,Mobility::Backends::ActiveRecord::KeyValue::StringTranslation]
196
206
  def translation_for(locale, **)
197
- translation = translations.find { |t| t.key == attribute && t.locale == locale.to_s }
198
- translation ||= translations.build(locale: locale, key: attribute)
199
- translation
200
- end
201
-
202
- module DestroyKeyValueTranslations
203
- def self.included(model_class)
204
- model_class.after_destroy do
205
- [:string, :text].each do |type|
206
- Mobility::Backends::ActiveRecord::KeyValue.const_get("#{type.capitalize}Translation").
207
- where(translatable: self).destroy_all
208
- end
209
- end
207
+ translation = translations.find do |t|
208
+ t.send(key_column) == attribute && t.locale == locale.to_s
210
209
  end
210
+ translation ||= translations.build(locale: locale, key_column => attribute)
211
+ translation
211
212
  end
212
213
 
213
214
  class Translation < ::ActiveRecord::Base
@@ -55,18 +55,18 @@ other backends on model (otherwise one will overwrite the other).
55
55
  # @!group Backend Accessors
56
56
  # @!macro backend_reader
57
57
  def read(locale, **options)
58
- translation_for(locale, **options).value
58
+ translation_for(locale, **options).send(value_column)
59
59
  end
60
60
 
61
61
  # @!macro backend_writer
62
62
  def write(locale, value, **options)
63
- translation_for(locale, **options).value = value
63
+ translation_for(locale, **options).send(:"#{value_column}=", value)
64
64
  end
65
65
  # @!endgroup
66
66
 
67
67
  # @!macro backend_iterator
68
68
  def each_locale
69
- translations.each { |t| yield(t.locale.to_sym) if t.key == attribute }
69
+ translations.each { |t| yield(t.locale.to_sym) if t.send(key_column) == attribute }
70
70
  end
71
71
 
72
72
  private
@@ -79,12 +79,14 @@ other backends on model (otherwise one will overwrite the other).
79
79
  backend_class.extend ClassMethods
80
80
  backend_class.option_reader :association_name
81
81
  backend_class.option_reader :class_name
82
- backend_class.option_reader :table_alias_affix
82
+ backend_class.option_reader :key_column
83
+ backend_class.option_reader :value_column
84
+ backend_class.option_reader :belongs_to
83
85
  end
84
86
 
85
87
  module ClassMethods
86
88
  def valid_keys
87
- [:type, :association_name, :class_name]
89
+ [:type, :association_name, :class_name, :key_column, :value_column, :belongs_to]
88
90
  end
89
91
 
90
92
  # @!group Backend Configuration
@@ -99,6 +101,9 @@ other backends on model (otherwise one will overwrite the other).
99
101
  options[:type] &&= options[:type].to_sym
100
102
  options[:association_name] &&= options[:association_name].to_sym
101
103
  options[:class_name] &&= Util.constantize(options[:class_name])
104
+ options[:key_column] ||= :key
105
+ options[:value_column] ||= :value
106
+ options[:belongs_to] ||= :translatable
102
107
  if !(options[:type] || (options[:class_name] && options[:association_name]))
103
108
  raise ArgumentError, "KeyValue backend requires an explicit type option, either text or string."
104
109
  end
@@ -110,7 +115,7 @@ other backends on model (otherwise one will overwrite the other).
110
115
  end
111
116
 
112
117
  def table_alias(attr, locale)
113
- table_alias_affix % "#{attr}_#{Mobility.normalize_locale(locale)}"
118
+ "#{model_class}_#{attr}_#{Mobility.normalize_locale(locale)}_#{options[:association_name]}"
114
119
  end
115
120
  end
116
121
 
@@ -32,14 +32,13 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
32
32
  options[:association_name] ||= :"#{options[:type]}_translations"
33
33
  options[:class_name] ||= const_get("#{type.capitalize}Translation")
34
34
  end
35
- options[:table_alias_affix] = "#{model_class}_%s_#{options[:association_name]}"
36
35
  rescue NameError
37
36
  raise ArgumentError, "You must define a Mobility::Sequel::#{type.capitalize}Translation class."
38
37
  end
39
38
  # @!endgroup
40
39
 
41
40
  def build_op(attr, locale)
42
- QualifiedIdentifier.new(table_alias(attr, locale), :value, locale, self, attr)
41
+ QualifiedIdentifier.new(table_alias(attr, locale), value_column, locale, self, attr)
43
42
  end
44
43
 
45
44
  # @param [Sequel::Dataset] dataset Dataset to prepare
@@ -58,10 +57,10 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
58
57
  dataset.join_table(join_type,
59
58
  class_name.table_name,
60
59
  {
61
- key: attr.to_s,
62
- locale: locale.to_s,
63
- translatable_type: model_class.name,
64
- translatable_id: ::Sequel[:"#{model_class.table_name}"][:id]
60
+ key_column => attr.to_s,
61
+ :locale => locale.to_s,
62
+ :"#{belongs_to}_type" => model_class.name,
63
+ :"#{belongs_to}_id" => ::Sequel[:"#{model_class.table_name}"][:id]
65
64
  },
66
65
  table_alias: table_alias(attr, locale))
67
66
  end
@@ -127,27 +126,37 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
127
126
  backend = self
128
127
 
129
128
  setup do |attributes, options|
130
- association_name = options[:association_name]
131
- translations_class = options[:class_name]
132
-
129
+ association_name = options[:association_name]
130
+ translation_class = options[:class_name]
131
+ key_column = options[:key_column]
132
+ value_column = options[:value_column]
133
+ belongs_to = options[:belongs_to]
134
+ belongs_to_id = :"#{belongs_to}_id"
135
+ belongs_to_type = :"#{belongs_to}_type"
136
+
137
+ # Track all attributes for this association, so that we can limit the scope
138
+ # of keys for the association to only these attributes. We need to track the
139
+ # attributes assigned to the association in case this setup code is called
140
+ # multiple times, so we don't "forget" earlier attributes.
141
+ #
133
142
  attrs_method_name = :"#{association_name}_attributes"
134
143
  association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
135
144
  instance_variable_set(:"@#{attrs_method_name}", association_attributes)
136
145
 
137
146
  one_to_many association_name,
138
- reciprocal: :translatable,
139
- key: :translatable_id,
147
+ reciprocal: belongs_to,
148
+ key: belongs_to_id,
140
149
  reciprocal_type: :one_to_many,
141
- conditions: { translatable_type: self.to_s, key: association_attributes },
142
- adder: proc { |translation| translation.update(translatable_id: pk, translatable_type: self.class.to_s) },
143
- remover: proc { |translation| translation.update(translatable_id: nil, translatable_type: nil) },
144
- clearer: proc { send(:"#{association_name}_dataset").update(translatable_id: nil, translatable_type: nil) },
145
- class: translations_class
150
+ conditions: { belongs_to_type => self.to_s, key_column => association_attributes },
151
+ adder: proc { |translation| translation.update(belongs_to_id => pk, belongs_to_type => self.class.to_s) },
152
+ remover: proc { |translation| translation.update(belongs_to_id => nil, belongs_to_type => nil) },
153
+ clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) },
154
+ class: translation_class
146
155
 
147
156
  callback_methods = Module.new do
148
157
  define_method :before_save do
149
158
  super()
150
- send(association_name).select { |t| attributes.include?(t.key) && Util.blank?(t.value) }.each(&:destroy)
159
+ send(association_name).select { |t| attributes.include?(t.__send__(key_column)) && Util.blank?(t.__send__(value_column)) }.each(&:destroy)
151
160
  end
152
161
  define_method :after_save do
153
162
  super()
@@ -156,7 +165,17 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
156
165
  end
157
166
  include callback_methods
158
167
 
159
- include DestroyKeyValueTranslations
168
+ # Clean up *all* leftover translations of this model, only once.
169
+ translation_classes = [translation_class, *Mobility::Backends::Sequel::KeyValue::Translation.descendants].uniq
170
+ define_method :after_destroy do
171
+ super()
172
+
173
+ @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes)
174
+ (translation_classes - @mobility_after_destroy_translation_classes).each do |klass|
175
+ klass.where(belongs_to_id => id, belongs_to_type => self.class.name).destroy
176
+ end
177
+ @mobility_after_destroy_translation_classes += translation_classes
178
+ end
160
179
  include(mod = Module.new)
161
180
  backend.define_column_changes(mod, attributes)
162
181
  end
@@ -165,30 +184,19 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
165
184
  # @param [Symbol] locale
166
185
  # @return [Mobility::Backends::Sequel::KeyValue::TextTranslation,Mobility::Backends::Sequel::KeyValue::StringTranslation]
167
186
  def translation_for(locale, **)
168
- translation = model.send(association_name).find { |t| t.key == attribute && t.locale == locale.to_s }
169
- translation ||= class_name.new(locale: locale, key: attribute)
187
+ translation = model.send(association_name).find { |t| t.__send__(key_column) == attribute && t.locale == locale.to_s }
188
+ translation ||= class_name.new(locale: locale, key_column => attribute)
170
189
  translation
171
190
  end
172
191
 
173
192
  # Saves translation which have been built and which have non-blank values.
174
193
  def save_translations
175
194
  cache.each_value do |translation|
176
- next unless present?(translation.value)
195
+ next unless present?(translation.__send__ value_column)
177
196
  translation.id ? translation.save : model.send("add_#{singularize(association_name)}", translation)
178
197
  end
179
198
  end
180
199
 
181
- # Clean up *all* leftover translations of this model, only once.
182
- module DestroyKeyValueTranslations
183
- def after_destroy
184
- super
185
- [:string, :text].freeze.each do |type|
186
- Mobility::Backends::Sequel::KeyValue.const_get("#{type.capitalize}Translation").
187
- where(translatable_id: id, translatable_type: self.class.name).destroy
188
- end
189
- end
190
- end
191
-
192
200
  class CacheRequired < ::StandardError; end
193
201
 
194
202
  module Cache
@@ -212,8 +220,32 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
212
220
  end
213
221
  end
214
222
 
215
- module Translation
216
- def self.included(base)
223
+ class Translatable < Module
224
+ attr_reader :key_column, :value_column, :belongs_to, :id_column, :type_column
225
+
226
+ def initialize(key_column, value_column, belongs_to)
227
+ @key_column = key_column
228
+ @value_column = value_column
229
+ @belongs_to = belongs_to
230
+ @id_column = :"#{belongs_to}_id"
231
+ @type_column = :"#{belongs_to}_type"
232
+ end
233
+
234
+ # Strictly these are not "descendants", but to keep terminology
235
+ # consistent with ActiveRecord KeyValue backend.
236
+ def descendants
237
+ @descendants ||= Set.new
238
+ end
239
+
240
+ def included(base)
241
+ @descendants ||= Set.new
242
+ @descendants << base
243
+
244
+ mod = self
245
+ key_column = mod.key_column
246
+ id_column = mod.id_column
247
+ type_column = mod.type_column
248
+
217
249
  base.class_eval do
218
250
  plugin :validation_helpers
219
251
 
@@ -221,16 +253,16 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
221
253
  #
222
254
  model = underscore(self.to_s)
223
255
  plural_model = pluralize(model)
224
- many_to_one :translatable,
256
+ many_to_one mod.belongs_to,
225
257
  reciprocal: plural_model.to_sym,
226
258
  reciprocal_type: :many_to_one,
227
259
  setter: (proc do |able_instance|
228
- self[:translatable_id] = (able_instance.pk if able_instance)
229
- self[:translatable_type] = (able_instance.class.name if able_instance)
260
+ self[id_column] = (able_instance.pk if able_instance)
261
+ self[type_column] = (able_instance.class.name if able_instance)
230
262
  end),
231
263
  dataset: (proc do
232
- translatable_type = send :translatable_type
233
- translatable_id = send :translatable_id
264
+ translatable_type = send type_column
265
+ translatable_id = send id_column
234
266
  return if translatable_type.nil? || translatable_id.nil?
235
267
  klass = self.class.send(:constantize, translatable_type)
236
268
  klass.where(klass.primary_key => translatable_id)
@@ -238,29 +270,30 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
238
270
  eager_loader: (proc do |eo|
239
271
  id_map = {}
240
272
  eo[:rows].each do |model|
241
- model_able_type = model.send :translatable_type
242
- model_able_id = model.send :translatable_id
243
- model.associations[:translatable] = nil
273
+ model_able_type = model.send type_column
274
+ model_able_id = model.send id_column
275
+ model.associations[belongs_to] = nil
244
276
  ((id_map[model_able_type] ||= {})[model_able_id] ||= []) << model if !model_able_type.nil? && !model_able_id.nil?
245
277
  end
246
278
  id_map.each do |klass_name, id_map|
247
279
  klass = constantize(camelize(klass_name))
248
280
  klass.where(klass.primary_key=>id_map.keys).all do |related_obj|
249
281
  id_map[related_obj.pk].each do |model|
250
- model.associations[:translatable] = related_obj
282
+ model.associations[belongs_to] = related_obj
251
283
  end
252
284
  end
253
285
  end
254
286
  end)
255
287
 
256
- def validate
257
- super
258
- validates_presence [:locale, :key, :translatable_id, :translatable_type]
259
- validates_unique [:locale, :key, :translatable_id, :translatable_type]
288
+ define_method :validate do
289
+ super()
290
+ validates_presence [:locale, key_column, id_column, type_column]
291
+ validates_unique [:locale, key_column, id_column, type_column]
260
292
  end
261
293
  end
262
294
  end
263
295
  end
296
+ Translation = Translatable.new(:key, :value, :translatable)
264
297
 
265
298
  class TextTranslation < ::Sequel::Model(:mobility_text_translations)
266
299
  include Translation
@@ -8,7 +8,9 @@ require_relative "./active_record/uniqueness_validation"
8
8
  module Mobility
9
9
  =begin
10
10
 
11
- Plugin for ActiveRecord models.
11
+ Plugin for ActiveRecord models. This plugin automatically requires activerecord
12
+ related plugins, which are not actually "active" unless their base plugin (e.g.
13
+ dirty for active_record_dirty) is also enabled.
12
14
 
13
15
  =end
14
16
  module Plugins
@@ -3,6 +3,11 @@
3
3
  module Mobility
4
4
  module Plugins
5
5
  module ActiveRecord
6
+ =begin
7
+
8
+ Maps backend names to ActiveRecord namespaced backends.
9
+
10
+ =end
6
11
  module Backend
7
12
  extend Plugin
8
13
 
@@ -29,7 +29,9 @@ enabled for any one attribute on the model.
29
29
  klass.class_eval do
30
30
  extend QueryMethod
31
31
  extend FindByMethods.new(*plugin.names)
32
- singleton_class.send :alias_method, plugin.query_method, :__mobility_query_scope__
32
+ singleton_class.define_method(plugin.query_method) do |locale: Mobility.locale, &block|
33
+ Query.build_query(self, locale, &block)
34
+ end
33
35
  end
34
36
  backend_class.include BackendMethods
35
37
  end
@@ -39,6 +41,14 @@ enabled for any one attribute on the model.
39
41
  def attribute_alias(attribute, locale = Mobility.locale)
40
42
  "__mobility_%s_%s__" % [attribute, ::Mobility.normalize_locale(locale)]
41
43
  end
44
+
45
+ def build_query(klass, locale = Mobility.locale, &block)
46
+ if block_given?
47
+ VirtualRow.build_query(klass, locale, &block)
48
+ else
49
+ klass.all.extending(QueryExtension)
50
+ end
51
+ end
42
52
  end
43
53
 
44
54
  module BackendMethods
@@ -57,13 +67,9 @@ enabled for any one attribute on the model.
57
67
  end
58
68
 
59
69
  module QueryMethod
60
- # This is required for UniquenessValidator.
61
70
  def __mobility_query_scope__(locale: Mobility.locale, &block)
62
- if block_given?
63
- VirtualRow.build_query(self, locale, &block)
64
- else
65
- all.extending(QueryExtension)
66
- end
71
+ warn '__mobility_query_scope__ is an internal method and will be deprecated in the next release.'
72
+ Query.build_query(self, locale, &block)
67
73
  end
68
74
  end
69
75
 
@@ -71,18 +77,21 @@ enabled for any one attribute on the model.
71
77
  # an instance-eval'ed block. Inspired by Sequel's (much more
72
78
  # sophisticated) virtual rows.
73
79
  class VirtualRow < BasicObject
74
- attr_reader :__backends
80
+ attr_reader :backends, :locales
75
81
 
76
- def initialize(model_class, locale)
77
- @model_class, @locale, @__backends = model_class, locale, []
82
+ def initialize(klass, global_locale)
83
+ @klass, @global_locale, @locales, @backends = klass, global_locale, [], []
78
84
  end
79
85
 
80
- def method_missing(m, *)
81
- if @model_class.mobility_attribute?(m)
82
- @__backends |= [@model_class.mobility_backend_class(m)]
83
- @model_class.mobility_backend_class(m).build_node(m, @locale)
84
- elsif @model_class.column_names.include?(m.to_s)
85
- @model_class.arel_table[m]
86
+ def method_missing(m, *args)
87
+ if @klass.mobility_attribute?(m)
88
+ @backends |= [@klass.mobility_backend_class(m)]
89
+ ::Mobility.validate_locale!(args[0]) if args[0]
90
+ locale = args[0] || @global_locale
91
+ @locales |= [locale]
92
+ @klass.mobility_backend_class(m).build_node(m, locale)
93
+ elsif @klass.column_names.include?(m.to_s)
94
+ @klass.arel_table[m]
86
95
  else
87
96
  super
88
97
  end
@@ -90,21 +99,27 @@ enabled for any one attribute on the model.
90
99
 
91
100
  class << self
92
101
  def build_query(klass, locale, &block)
102
+ ::Mobility.validate_locale!(locale)
103
+
93
104
  row = new(klass, locale)
94
105
  query = block.arity.zero? ? row.instance_eval(&block) : block.call(row)
95
106
 
96
107
  if ::ActiveRecord::Relation === query
97
108
  predicates = query.arel.constraints
98
- apply_scopes(klass.all, row.__backends, locale, predicates).merge(query)
109
+ apply_scopes(klass.all, row.backends, row.locales, predicates).merge(query)
99
110
  else
100
- apply_scopes(klass.all, row.__backends, locale, query).where(query)
111
+ apply_scopes(klass.all, row.backends, row.locales, query).where(query)
101
112
  end
102
113
  end
103
114
 
104
115
  private
105
116
 
106
- def apply_scopes(scope, backends, locale, predicates)
107
- backends.inject(scope) { |r, b| b.apply_scope(r, predicates, locale) }
117
+ def apply_scopes(scope, backends, locales, predicates)
118
+ backends.inject(scope) do |scope_, b|
119
+ locales.inject(scope_) do |r, locale|
120
+ b.apply_scope(r, predicates, locale)
121
+ end
122
+ end
108
123
  end
109
124
  end
110
125
  end
@@ -27,7 +27,7 @@ module Mobility
27
27
 
28
28
  if ([*options[:scope]] + [attribute]).any? { |name| klass.mobility_attribute?(name) }
29
29
  return unless value.present?
30
- relation = klass.unscoped.__mobility_query_scope__ do |m|
30
+ relation = Plugins::ActiveRecord::Query.build_query(klass.unscoped, Mobility.locale) do |m|
31
31
  node = m.__send__(attribute)
32
32
  options[:case_sensitive] == false ? node.lower.eq(value.downcase) : node.eq(value)
33
33
  end
@@ -50,7 +50,7 @@ module Mobility
50
50
 
51
51
  def mobility_scope_relation(record, relation)
52
52
  [*options[:scope]].inject(relation) do |scoped_relation, scope_item|
53
- scoped_relation.__mobility_query_scope__ do |m|
53
+ Plugins::ActiveRecord::Query.build_query(scoped_relation, Mobility.locale) do |m|
54
54
  m.__send__(scope_item).eq(record.send(scope_item))
55
55
  end
56
56
  end
@@ -19,12 +19,6 @@ for aggregating attributes.
19
19
  @names = names.map(&:to_s).freeze
20
20
  end
21
21
 
22
- # Yield each attribute name to block
23
- # @yieldparam [String] Attribute
24
- def each &block
25
- names.each(&block)
26
- end
27
-
28
22
  # Show useful information about this module.
29
23
  # @return [String]
30
24
  def inspect
@@ -1,5 +1,4 @@
1
1
  require "sequel"
2
- raise VersionNotSupportedError, "Mobility is only compatible with Sequel 4.0 and greater" if ::Sequel::MAJOR < 4
3
2
  require "sequel/plugins/mobility"
4
3
  unless defined?(ActiveSupport::Inflector)
5
4
  # TODO: avoid automatically including the inflector extension
@@ -13,6 +12,13 @@ require_relative "./sequel/query"
13
12
 
14
13
  module Mobility
15
14
  module Plugins
15
+ =begin
16
+
17
+ Plugin for Sequel models. This plugin automatically requires sequel related
18
+ plugins, which are not actually "active" unless their base plugin (e.g. dirty
19
+ for sequel_dirty) is also enabled.
20
+
21
+ =end
16
22
  module Sequel
17
23
  extend Plugin
18
24
 
@@ -1,6 +1,11 @@
1
1
  module Mobility
2
2
  module Plugins
3
3
  module Sequel
4
+ =begin
5
+
6
+ Maps backend names to Sequel namespaced backends.
7
+
8
+ =end
4
9
  module Backend
5
10
  extend Plugin
6
11
 
@@ -3,7 +3,8 @@ module Mobility
3
3
  module Plugins
4
4
  =begin
5
5
 
6
- See ActiveRecord::Query plugin.
6
+ Supports querying on Sequel model translated attributes. Similar API to the
7
+ ActiveRecord query plugin.
7
8
 
8
9
  =end
9
10
  module Sequel
@@ -19,35 +20,47 @@ See ActiveRecord::Query plugin.
19
20
 
20
21
  klass.class_eval do
21
22
  extend QueryMethod
22
- singleton_class.send :alias_method, plugin.query_method, :__mobility_query_dataset__
23
+ singleton_class.define_method(plugin.query_method) do |locale: Mobility.locale, &block|
24
+ Query.build_query(self, locale, &block)
25
+ end
23
26
  end
24
27
  end
25
28
  end
26
29
 
27
- module QueryMethod
28
- def __mobility_query_dataset__(locale: Mobility.locale, &block)
30
+ class << self
31
+ def build_query(klass, locale = Mobility.locale, &block)
29
32
  if block_given?
30
- VirtualRow.build_query(self, locale, &block)
33
+ VirtualRow.build_query(klass, locale, &block)
31
34
  else
32
- dataset.with_extend(QueryExtension)
35
+ klass.dataset.with_extend(QueryExtension)
33
36
  end
34
37
  end
35
38
  end
36
39
 
40
+ module QueryMethod
41
+ def __mobility_query_dataset__(locale: Mobility.locale, &block)
42
+ warn '__mobility_query_dataset__ is an internal method and will be deprecated in the next release.'
43
+ Query.build_query(self, locale, &block)
44
+ end
45
+ end
46
+
37
47
  # Internal class to create a "clean room" for manipulating translated
38
48
  # attribute nodes in an instance-eval'ed block. Inspired by Sequel's
39
49
  # (much more sophisticated) virtual rows.
40
50
  class VirtualRow < BasicObject
41
- attr_reader :__backends
51
+ attr_reader :backends, :locales
42
52
 
43
- def initialize(model_class, locale)
44
- @model_class, @locale, @__backends = model_class, locale, []
53
+ def initialize(model_class, global_locale)
54
+ @model_class, @global_locale, @backends, @locales = model_class, global_locale, [], []
45
55
  end
46
56
 
47
- def method_missing(m, *)
57
+ def method_missing(m, *args)
48
58
  if @model_class.mobility_attribute?(m)
49
- @__backends |= [@model_class.mobility_backend_class(m)]
50
- @model_class.mobility_backend_class(m).build_op(m.to_s, @locale)
59
+ @backends |= [@model_class.mobility_backend_class(m)]
60
+ ::Mobility.validate_locale!(args[0]) if args[0]
61
+ locale = args[0] || @global_locale
62
+ @locales |= [locale]
63
+ @model_class.mobility_backend_class(m).build_op(m.to_s, locale)
51
64
  elsif @model_class.columns.include?(m.to_s)
52
65
  ::Sequel::SQL::QualifiedIdentifier.new(@model_class.table_name, m)
53
66
  else
@@ -57,21 +70,27 @@ See ActiveRecord::Query plugin.
57
70
 
58
71
  class << self
59
72
  def build_query(klass, locale, &block)
73
+ ::Mobility.validate_locale!(locale)
74
+
60
75
  row = new(klass, locale)
61
76
  query = block.arity.zero? ? row.instance_eval(&block) : block.call(row)
62
77
 
63
78
  if ::Sequel::Dataset === query
64
79
  predicates = query.opts[:where]
65
- prepare_datasets(query, row.__backends, locale, predicates)
80
+ prepare_datasets(query, row.backends, row.locales, predicates)
66
81
  else
67
- prepare_datasets(klass.dataset, row.__backends, locale, query).where(query)
82
+ prepare_datasets(klass.dataset, row.backends, row.locales, query).where(query)
68
83
  end
69
84
  end
70
85
 
71
86
  private
72
87
 
73
- def prepare_datasets(dataset, backends, locale, predicates)
74
- backends.inject(dataset) { |ds, b| b.prepare_dataset(ds, predicates, locale) }
88
+ def prepare_datasets(dataset, backends, locales, predicates)
89
+ backends.inject(dataset) do |dataset_, b|
90
+ locales.inject(dataset_) do |ds, locale|
91
+ b.prepare_dataset(ds, predicates, locale)
92
+ end
93
+ end
75
94
  end
76
95
  end
77
96
  end
@@ -7,8 +7,8 @@ module Mobility
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 1
10
- MINOR = 0
11
- TINY = 7
10
+ MINOR = 1
11
+ TINY = 0
12
12
  PRE = nil
13
13
 
14
14
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mobility
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Salzberg
@@ -34,7 +34,7 @@ cert_chain:
34
34
  gSQml7TqcC6dZRsZRwYqzD9kUwdAJoCqno2CBUKs2l0yQAjFT36lRrVJznb7uWwa
35
35
  xpPFnsrtyaZW6Dty8TSG3qzmeGpmpIotA8x1VA==
36
36
  -----END CERTIFICATE-----
37
- date: 2021-02-04 00:00:00.000000000 Z
37
+ date: 2021-02-06 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: request_store
metadata.gz.sig CHANGED
Binary file