mobility 1.0.7 → 1.1.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.
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