mobility 1.1.2 → 1.2.2

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: f5879d6ea43de5adf3be544ef11aaa634adabd6cad56609122450a1c648875f3
4
- data.tar.gz: 7b0a1ed6187a0bb4aa75729c7e894c5673908e2443b50ba0da32f905ba4bf51f
3
+ metadata.gz: bfb0cb0bd2a8efd18e8fab29abf7512a765086b4d6e3d27234d8c8d6085fd5d1
4
+ data.tar.gz: b143906e032fe332dbb169badc3cc3cd8dd876e4d45f2556ed1a8ae5d4cfb1ab
5
5
  SHA512:
6
- metadata.gz: 860741ebf12f984401bac6b03726ac4b10a5c64befbd60a199130154768e6c2c34567009999487dcdaba49f41c12fd73f9fcb86441d9e7ad71b7d59adb4976a6
7
- data.tar.gz: 27f2da6d8118a5b582035d721437f71520b2d4bcb81f8dba63c81f77b8c2d2e3badc6ec98349086e9c7fa651d62df38255c495cc76dccc0bb2bb6a988458e007
6
+ metadata.gz: 35d4c26eb09f5facee434046bb1c447fb43ff4a19ab0f911414239a3b8d758f2c41fb4ae857027e8f50041fc56caa64b3c7291c05b0e80491542d72e1c88c2e3
7
+ data.tar.gz: 04d437ce889463ef6049d2f8b3cb1695a22e444a3f4057cb62be6c9fee8cd4516210dc18be4bbcfdd4b7945fdd9bb95c4960cee3e7413d2130a02b188c4f87e2
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,7 +1,38 @@
1
1
  # Mobility Changelog
2
2
 
3
+ ## Unreleased
4
+ - Simplify Fallbacks plugin
5
+ ([#531](https://github.com/shioyama/mobility/pull/531))
6
+
7
+ ## 1.2
8
+
9
+ ### 1.2.2
10
+ - Make models work with `Marshal.dump`
11
+ ([#532](https://github.com/shioyama/mobility/pull/532))
12
+
13
+ ### 1.2.1
14
+ - Refactor ColumnFallback plugin
15
+ ([#530](https://github.com/shioyama/mobility/pull/530))
16
+
17
+ ### 1.2.0
18
+ - Add ColumnFallback plugin
19
+ ([#512](https://github.com/shioyama/mobility/pull/512))
20
+ - Fix Sequel querying on untranslated attributes in `i18n` block
21
+ ([#529](https://github.com/shioyama/mobility/pull/529))
22
+ - Allow passing configured backend class as third argument to setup
23
+ ([#528](https://github.com/shioyama/mobility/pull/528))
24
+ - Clearly distinguish backend classes from their configured subclasses
25
+ ([#527](https://github.com/shioyama/mobility/pull/527))
26
+
3
27
  ## 1.1
4
28
 
29
+ ### 1.1.3
30
+ - Do not swallow keyword args on ruby 3 in fallthrough accessors
31
+ ([#520](https://github.com/shioyama/mobility/pull/520)) thanks
32
+ [doits](https://github.com/doits)!
33
+ - Assign blank values in pg hash backends
34
+ ([#516](https://github.com/shioyama/mobility/pull/516))
35
+
5
36
  ### 1.1.2
6
37
  - Check whether class responds to mobility_attribute?
7
38
  ([#515](https://github.com/shioyama/mobility/pull/515))
data/Gemfile CHANGED
@@ -12,8 +12,8 @@ group :development, :test do
12
12
  case orm_version
13
13
  when '4.2', '5.0', '5.1', '5.2', '6.0', '6.1'
14
14
  gem 'activerecord', "~> #{orm_version}.0"
15
- when '6.2'
16
- git 'https://github.com/rails/rails.git' do
15
+ when '7.0'
16
+ git 'https://github.com/rails/rails.git', branch: 'main' do
17
17
  gem 'activerecord'
18
18
  gem 'activesupport'
19
19
  end
data/Gemfile.lock CHANGED
@@ -1,45 +1,28 @@
1
- GIT
2
- remote: https://github.com/rails/rails.git
3
- revision: ac3910791d0294f6721e5a39e0063aa005535f10
4
- branch: main
5
- specs:
6
- activemodel (7.0.0.alpha)
7
- activesupport (= 7.0.0.alpha)
8
- activerecord (7.0.0.alpha)
9
- activemodel (= 7.0.0.alpha)
10
- activesupport (= 7.0.0.alpha)
11
- activesupport (7.0.0.alpha)
12
- concurrent-ruby (~> 1.0, >= 1.0.2)
13
- i18n (>= 1.6, < 2)
14
- minitest (>= 5.1)
15
- tzinfo (~> 2.0)
16
- zeitwerk (~> 2.3)
17
-
18
1
  PATH
19
2
  remote: .
20
3
  specs:
21
- mobility (1.1.1)
4
+ mobility (1.2.1)
22
5
  i18n (>= 0.6.10, < 2)
23
6
  request_store (~> 1.0)
24
7
 
25
8
  GEM
26
9
  remote: https://rubygems.org/
27
10
  specs:
28
- benchmark-ips (2.8.4)
11
+ benchmark-ips (2.9.1)
29
12
  byebug (11.1.3)
30
13
  coderay (1.1.3)
31
- concurrent-ruby (1.1.8)
14
+ concurrent-ruby (1.1.9)
32
15
  database_cleaner (1.99.0)
33
16
  diff-lcs (1.4.4)
34
- ffi (1.15.0)
35
- formatador (0.2.5)
36
- guard (2.16.2)
17
+ ffi (1.15.4)
18
+ formatador (0.3.0)
19
+ guard (2.18.0)
37
20
  formatador (>= 0.2.4)
38
21
  listen (>= 2.7, < 4.0)
39
22
  lumberjack (>= 1.0.12, < 2.0)
40
23
  nenv (~> 0.1)
41
24
  notiffany (~> 0.0)
42
- pry (>= 0.9.12)
25
+ pry (>= 0.13.0)
43
26
  shellany (~> 0.0)
44
27
  thor (>= 0.18.1)
45
28
  guard-compat (1.2.1)
@@ -49,12 +32,11 @@ GEM
49
32
  rspec (>= 2.99.0, < 4.0)
50
33
  i18n (1.8.10)
51
34
  concurrent-ruby (~> 1.0)
52
- listen (3.5.1)
35
+ listen (3.7.0)
53
36
  rb-fsevent (~> 0.10, >= 0.10.3)
54
37
  rb-inotify (~> 0.9, >= 0.9.10)
55
38
  lumberjack (1.2.8)
56
39
  method_source (1.0.0)
57
- minitest (5.14.4)
58
40
  nenv (0.3.0)
59
41
  notiffany (0.1.3)
60
42
  nenv (~> 0.1)
@@ -68,7 +50,7 @@ GEM
68
50
  pry (~> 0.13.0)
69
51
  rack (2.2.3)
70
52
  rake (12.3.3)
71
- rb-fsevent (0.10.4)
53
+ rb-fsevent (0.11.0)
72
54
  rb-inotify (0.10.1)
73
55
  ffi (~> 1.0)
74
56
  request_store (1.5.0)
@@ -86,19 +68,15 @@ GEM
86
68
  diff-lcs (>= 1.2.0, < 2.0)
87
69
  rspec-support (~> 3.10.0)
88
70
  rspec-support (3.10.2)
71
+ sequel (5.47.0)
89
72
  shellany (0.0.1)
90
73
  thor (1.1.0)
91
- tzinfo (2.0.4)
92
- concurrent-ruby (~> 1.0)
93
74
  yard (0.9.26)
94
- zeitwerk (2.4.2)
95
75
 
96
76
  PLATFORMS
97
77
  ruby
98
78
 
99
79
  DEPENDENCIES
100
- activerecord!
101
- activesupport!
102
80
  benchmark-ips
103
81
  database_cleaner (~> 1.5, >= 1.5.3)
104
82
  guard-rspec
@@ -107,6 +85,7 @@ DEPENDENCIES
107
85
  pry-byebug
108
86
  rake (~> 12, >= 12.2.1)
109
87
  rspec (~> 3.0)
88
+ sequel (= 5.47)
110
89
  yard (~> 0.9.0)
111
90
 
112
91
  BUNDLED WITH
data/README.md CHANGED
@@ -55,7 +55,7 @@ Installation
55
55
  Add this line to your application's Gemfile:
56
56
 
57
57
  ```ruby
58
- gem 'mobility', '~> 1.1.2'
58
+ gem 'mobility', '~> 1.2.2'
59
59
  ```
60
60
 
61
61
  ### ActiveRecord (Rails)
@@ -21,8 +21,8 @@ On top of this, a backend will normally:
21
21
  corresponding to valid keys for configuring this backend.
22
22
  - implement a +configure+ class method to apply any normalization to the
23
23
  keys on the options hash included in +valid_keys+
24
- - call the +setup+ method yielding attributes and options to configure the
25
- model class
24
+ - call the +setup+ method yielding attributes and options (and optionally the
25
+ configured backend class) to configure the model class
26
26
 
27
27
  @example Defining a Backend
28
28
  class MyBackend
@@ -47,6 +47,13 @@ On top of this, a backend will normally:
47
47
  setup do |attributes, options|
48
48
  # Do something with attributes and options in context of model class.
49
49
  end
50
+
51
+ # The block can optionally take the configured backend class as its third
52
+ # argument:
53
+ #
54
+ # setup do |attributes, options, backend_class|
55
+ # ...
56
+ # end
50
57
  end
51
58
 
52
59
  @see Mobility::Translations
@@ -70,6 +77,12 @@ On top of this, a backend will normally:
70
77
  @attribute = args[1]
71
78
  end
72
79
 
80
+ def ==(backend)
81
+ backend.class == self.class &&
82
+ backend.attribute == attribute &&
83
+ backend.model == model
84
+ end
85
+
73
86
  # @!macro [new] backend_reader
74
87
  # Gets the translated value for provided locale from configured backend.
75
88
  # @param [Symbol] locale Locale to read
@@ -117,7 +130,6 @@ On top of this, a backend will normally:
117
130
  # Extend included class with +setup+ method and other class methods
118
131
  def self.included(base)
119
132
  base.extend ClassMethods
120
- base.singleton_class.attr_reader :options, :model_class
121
133
  end
122
134
 
123
135
  # Defines setup hooks for backend to customize model class.
@@ -136,9 +148,11 @@ On top of this, a backend will normally:
136
148
  def setup &block
137
149
  if @setup_block
138
150
  setup_block = @setup_block
139
- @setup_block = lambda do |*args|
140
- class_exec(*args, &setup_block)
141
- class_exec(*args, &block)
151
+ exec_setup_block = method(:exec_setup_block)
152
+ @setup_block = lambda do |attributes, options, backend_class|
153
+ [setup_block, block].each do |blk|
154
+ exec_setup_block.call(self, attributes, options, backend_class, &blk)
155
+ end
142
156
  end
143
157
  else
144
158
  @setup_block = block
@@ -147,17 +161,6 @@ On top of this, a backend will normally:
147
161
 
148
162
  def inherited(subclass)
149
163
  subclass.instance_variable_set(:@setup_block, @setup_block)
150
- subclass.instance_variable_set(:@options, @options)
151
- subclass.instance_variable_set(:@model_class, @model_class)
152
- end
153
-
154
- # Call setup block on a class with attributes and options.
155
- # @param model_class Class to be setup-ed
156
- # @param [Array<String>] attribute_names
157
- # @param [Hash] options
158
- def setup_model(model_class, attribute_names)
159
- return unless setup_block = @setup_block
160
- model_class.class_exec(attribute_names, options, &setup_block)
161
164
  end
162
165
 
163
166
  # Build a subclass of this backend class for a given set of options
@@ -167,11 +170,7 @@ On top of this, a backend will normally:
167
170
  # @param [Hash] options
168
171
  # @return [Class] backend subclass
169
172
  def build_subclass(model_class, options)
170
- Class.new(self) do
171
- @model_class = model_class
172
- configure(options) if respond_to?(:configure)
173
- @options = options.freeze
174
- end
173
+ ConfiguredBackend.build(self, model_class, options)
175
174
  end
176
175
 
177
176
  # Create instance and class methods to access value on options hash
@@ -188,10 +187,30 @@ On top of this, a backend will normally:
188
187
  EOM
189
188
  end
190
189
 
191
- # Show useful information about this backend class, if it has no name.
192
- # @return [String]
193
- def inspect
194
- name ? super : "#<#{superclass.name}>"
190
+ def options
191
+ raise_unconfigured!(:options)
192
+ end
193
+
194
+ def model_class
195
+ raise_unconfigured!(:model_class)
196
+ end
197
+
198
+ def setup_model(_model_class, _attributes)
199
+ raise_unconfigured!(:setup_model)
200
+ end
201
+
202
+ private
203
+
204
+ def raise_unconfigured!(method_name)
205
+ raise UnconfiguredError, "You are calling #{method_name} on an unconfigured backend class."
206
+ end
207
+
208
+ def exec_setup_block(model_class, *args, &block)
209
+ if block.arity == 3
210
+ model_class.class_exec(*args[0..2], &block)
211
+ else
212
+ model_class.class_exec(*args[0..1], &block)
213
+ end
195
214
  end
196
215
  end
197
216
 
@@ -204,5 +223,48 @@ On top of this, a backend will normally:
204
223
  backend.write(locale, value, options)
205
224
  end
206
225
  end
226
+
227
+ class ConfiguredError < StandardError; end
228
+ class UnconfiguredError < StandardError; end
229
+ =begin
230
+
231
+ Module included in configured backend classes, which in addition to methods on
232
+ the parent backend class also have a +model_class+ and set of +options+.
233
+
234
+ =end
235
+ module ConfiguredBackend
236
+ def self.build(backend_class, model_class, options)
237
+ Class.new(backend_class) do
238
+ extend ConfiguredBackend
239
+
240
+ @model_class = model_class
241
+ configure(options) if respond_to?(:configure)
242
+ @options = options.freeze
243
+ end
244
+ end
245
+
246
+ def self.extended(klass)
247
+ klass.singleton_class.attr_reader :options, :model_class
248
+ end
249
+
250
+ # Call setup block on a class with attributes and options.
251
+ # @param model_class Class to be setup-ed
252
+ # @param [Array<String>] attribute_names
253
+ # @param [Hash] options
254
+ def setup_model(model_class, attribute_names)
255
+ return unless setup_block = @setup_block
256
+ exec_setup_block(model_class, attribute_names, options, self, &setup_block)
257
+ end
258
+
259
+ def inherited(_)
260
+ raise ConfiguredError, "Configured backends cannot be subclassed."
261
+ end
262
+
263
+ # Show subclassed backend class name, if it has one.
264
+ # @return [String]
265
+ def inspect
266
+ (name = superclass.name) ? "#<#{name}>" : super
267
+ end
268
+ end
207
269
  end
208
270
  end
@@ -65,6 +65,69 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
65
65
  end
66
66
  end
67
67
 
68
+ # Called from setup block. Can be overridden to customize behaviour.
69
+ def define_has_many_association(klass, attributes)
70
+ # Track all attributes for this association, so that we can limit the scope
71
+ # of keys for the association to only these attributes. We need to track the
72
+ # attributes assigned to the association in case this setup code is called
73
+ # multiple times, so we don't "forget" earlier attributes.
74
+ #
75
+ attrs_method_name = :"__#{association_name}_attributes"
76
+ association_attributes = (klass.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
77
+ klass.instance_variable_set(:"@#{attrs_method_name}", association_attributes)
78
+
79
+ b = self
80
+
81
+ klass.has_many association_name, ->{ where b.key_column => association_attributes },
82
+ as: belongs_to,
83
+ class_name: class_name.name,
84
+ inverse_of: belongs_to,
85
+ autosave: true
86
+ end
87
+
88
+ # Called from setup block. Can be overridden to customize behaviour.
89
+ def define_initialize_dup(klass)
90
+ b = self
91
+ module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}"
92
+ unless const_defined?(module_name)
93
+ callback_methods = Module.new do
94
+ define_method :initialize_dup do |source|
95
+ super(source)
96
+ self.send("#{b.association_name}=", source.send(b.association_name).map(&:dup))
97
+ # Set inverse on associations
98
+ send(b.association_name).each do |translation|
99
+ translation.send(:"#{b.belongs_to}=", self)
100
+ end
101
+ end
102
+ end
103
+ klass.include const_set(module_name, callback_methods)
104
+ end
105
+ end
106
+
107
+ # Called from setup block. Can be overridden to customize behaviour.
108
+ def define_before_save_callback(klass)
109
+ b = self
110
+ klass.before_save do
111
+ send(b.association_name).select { |t| t.send(b.value_column).blank? }.each do |translation|
112
+ send(b.association_name).destroy(translation)
113
+ end
114
+ end
115
+ end
116
+
117
+ # Called from setup block. Can be overridden to customize behaviour.
118
+ def define_after_destroy_callback(klass)
119
+ # Ensure we only call after destroy hook once per translations class
120
+ b = self
121
+ translation_classes = [class_name, *Mobility::Backends::ActiveRecord::KeyValue::Translation.descendants].uniq
122
+ klass.after_destroy do
123
+ @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes)
124
+ (translation_classes - @mobility_after_destroy_translation_classes).each do |translation_class|
125
+ translation_class.where(b.belongs_to => self).destroy_all
126
+ end
127
+ @mobility_after_destroy_translation_classes += translation_classes
128
+ end
129
+ end
130
+
68
131
  private
69
132
 
70
133
  def join_translations(relation, key, locale, join_type)
@@ -149,55 +212,11 @@ Implements the {Mobility::Backends::KeyValue} backend for ActiveRecord models.
149
212
  end
150
213
  end
151
214
 
152
- setup do |attributes, options|
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]
158
-
159
- # Track all attributes for this association, so that we can limit the scope
160
- # of keys for the association to only these attributes. We need to track the
161
- # attributes assigned to the association in case this setup code is called
162
- # multiple times, so we don't "forget" earlier attributes.
163
- #
164
- attrs_method_name = :"__#{association_name}_attributes"
165
- association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
166
- instance_variable_set(:"@#{attrs_method_name}", association_attributes)
167
-
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,
172
- autosave: true
173
- before_save do
174
- send(association_name).select { |t| t.send(value_column).blank? }.each do |translation|
175
- send(association_name).destroy(translation)
176
- end
177
- end
178
-
179
- module_name = "MobilityArKeyValue#{association_name.to_s.camelcase}"
180
- unless const_defined?(module_name)
181
- callback_methods = Module.new do
182
- define_method :initialize_dup do |source|
183
- super(source)
184
- self.send("#{association_name}=", source.send(association_name).map(&:dup))
185
- # Set inverse on associations
186
- send(association_name).each do |translation|
187
- translation.send(:"#{belongs_to}=", self)
188
- end
189
- end
190
- end
191
- include const_set(module_name, callback_methods)
192
- end
193
-
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
215
+ setup do |attributes, _options, backend_class|
216
+ backend_class.define_has_many_association(self, attributes)
217
+ backend_class.define_initialize_dup(self)
218
+ backend_class.define_before_save_callback(self)
219
+ backend_class.define_after_destroy_callback(self)
201
220
  end
202
221
 
203
222
  # Returns translation for a given locale, or builds one if none is present.
@@ -32,7 +32,7 @@ Internal class used by ActiveRecord backends backed by a Postgres data type
32
32
  def self.dump(obj)
33
33
  if obj.is_a? ::Hash
34
34
  obj.inject({}) do |translations, (locale, value)|
35
- translations[locale] = value if value.present?
35
+ translations[locale] = value unless value.nil?
36
36
  translations
37
37
  end
38
38
  else
@@ -57,9 +57,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
57
57
  end
58
58
  end
59
59
 
60
- backend = self
61
-
62
- setup do |attributes, options|
60
+ setup do |attributes, options, backend_class|
63
61
  column_name = options[:column_name]
64
62
  mod = Module.new do
65
63
  define_method :before_validation do
@@ -71,7 +69,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
71
69
  end
72
70
  end
73
71
  include mod
74
- backend.define_hash_initializer(mod, [column_name])
72
+ backend_class.define_hash_initializer(mod, [column_name])
75
73
 
76
74
  plugin :defaults_setter
77
75
  attributes.each { |attribute| default_values[attribute.to_sym] = {} }
@@ -102,7 +100,7 @@ Implements the {Mobility::Backends::Container} backend for Sequel models.
102
100
  # @return [Mobility::Backends::Sequel::Container::JSONOp,Mobility::Backends::Sequel::Container::JSONBOp]
103
101
  def self.build_op(attr, locale)
104
102
  klass = const_get("#{options[:column_type].upcase}Op")
105
- klass.new(klass.new(column_name.to_sym)[locale.to_s]).get_text(attr)
103
+ klass.new(klass.new(column_name.to_sym).get(locale.to_s)).get_text(attr)
106
104
  end
107
105
 
108
106
  class JSONOp < ::Sequel::Postgres::JSONOp; end
@@ -51,6 +51,63 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
51
51
  end
52
52
  end
53
53
 
54
+ # Called from setup block. Can be overridden to customize behaviour.
55
+ def define_one_to_many_association(klass, attributes)
56
+ belongs_to_id = :"#{belongs_to}_id"
57
+ belongs_to_type = :"#{belongs_to}_type"
58
+
59
+ # Track all attributes for this association, so that we can limit the scope
60
+ # of keys for the association to only these attributes. We need to track the
61
+ # attributes assigned to the association in case this setup code is called
62
+ # multiple times, so we don't "forget" earlier attributes.
63
+ #
64
+ attrs_method_name = :"#{association_name}_attributes"
65
+ association_attributes = (klass.instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
66
+ klass.instance_variable_set(:"@#{attrs_method_name}", association_attributes)
67
+
68
+ klass.one_to_many association_name,
69
+ reciprocal: belongs_to,
70
+ key: belongs_to_id,
71
+ reciprocal_type: :one_to_many,
72
+ conditions: { belongs_to_type => klass.to_s, key_column => association_attributes },
73
+ adder: proc { |translation| translation.update(belongs_to_id => pk, belongs_to_type => self.class.to_s) },
74
+ remover: proc { |translation| translation.update(belongs_to_id => nil, belongs_to_type => nil) },
75
+ clearer: proc { send_(:"#{association_name}_dataset").update(belongs_to_id => nil, belongs_to_type => nil) },
76
+ class: class_name
77
+ end
78
+
79
+ # Called from setup block. Can be overridden to customize behaviour.
80
+ def define_save_callbacks(klass, attributes)
81
+ b = self
82
+ callback_methods = Module.new do
83
+ define_method :before_save do
84
+ super()
85
+ send(b.association_name).select { |t| attributes.include?(t.__send__(b.key_column)) && Util.blank?(t.__send__(b.value_column)) }.each(&:destroy)
86
+ end
87
+ define_method :after_save do
88
+ super()
89
+ attributes.each { |attribute| mobility_backends[attribute].save_translations }
90
+ end
91
+ end
92
+ klass.include callback_methods
93
+ end
94
+
95
+ # Called from setup block. Can be overridden to customize behaviour.
96
+ def define_after_destroy_callback(klass)
97
+ # Clean up *all* leftover translations of this model, only once.
98
+ b = self
99
+ translation_classes = [class_name, *Mobility::Backends::Sequel::KeyValue::Translation.descendants].uniq
100
+ klass.define_method :after_destroy do
101
+ super()
102
+
103
+ @mobility_after_destroy_translation_classes = [] unless defined?(@mobility_after_destroy_translation_classes)
104
+ (translation_classes - @mobility_after_destroy_translation_classes).each do |translation_class|
105
+ translation_class.where(:"#{b.belongs_to}_id" => id, :"#{b.belongs_to}_type" => self.class.name).destroy
106
+ end
107
+ @mobility_after_destroy_translation_classes += translation_classes
108
+ end
109
+ end
110
+
54
111
  private
55
112
 
56
113
  def join_translations(dataset, attr, locale, join_type)
@@ -74,7 +131,7 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
74
131
  visit_sql_identifier(predicate, locale)
75
132
  when ::Sequel::SQL::BooleanExpression
76
133
  visit_boolean(predicate, locale)
77
- when ::Sequel::SQL::Expression
134
+ when ::Sequel::SQL::ComplexExpression
78
135
  visit(predicate.args, locale)
79
136
  else
80
137
  {}
@@ -123,61 +180,13 @@ Implements the {Mobility::Backends::KeyValue} backend for Sequel models.
123
180
  end
124
181
  end
125
182
 
126
- backend = self
127
-
128
- setup do |attributes, options|
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
- #
142
- attrs_method_name = :"#{association_name}_attributes"
143
- association_attributes = (instance_variable_get(:"@#{attrs_method_name}") || []) + attributes
144
- instance_variable_set(:"@#{attrs_method_name}", association_attributes)
145
-
146
- one_to_many association_name,
147
- reciprocal: belongs_to,
148
- key: belongs_to_id,
149
- reciprocal_type: :one_to_many,
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
155
-
156
- callback_methods = Module.new do
157
- define_method :before_save do
158
- super()
159
- send(association_name).select { |t| attributes.include?(t.__send__(key_column)) && Util.blank?(t.__send__(value_column)) }.each(&:destroy)
160
- end
161
- define_method :after_save do
162
- super()
163
- attributes.each { |attribute| mobility_backends[attribute].save_translations }
164
- end
165
- end
166
- include callback_methods
167
-
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()
183
+ setup do |attributes, _options, backend_class|
184
+ backend_class.define_one_to_many_association(self, attributes)
185
+ backend_class.define_save_callbacks(self, attributes)
186
+ backend_class.define_after_destroy_callback(self)
172
187
 
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
179
188
  include(mod = Module.new)
180
- backend.define_column_changes(mod, attributes)
189
+ backend_class.define_column_changes(mod, attributes)
181
190
  end
182
191
 
183
192
  # Returns translation for a given locale, or initializes one if none is present.
@@ -33,22 +33,20 @@ jsonb).
33
33
  model[column_name.to_sym]
34
34
  end
35
35
 
36
- backend = self
37
-
38
- setup do |attributes, options|
36
+ setup do |attributes, options, backend_class|
39
37
  columns = attributes.map { |attribute| (options[:column_affix] % attribute).to_sym }
40
38
 
41
39
  mod = Module.new do
42
40
  define_method :before_validation do
43
41
  columns.each do |column|
44
- self[column].delete_if { |_, v| Util.blank?(v) }
42
+ self[column].delete_if { |_, v| v.nil? }
45
43
  end
46
44
  super()
47
45
  end
48
46
  end
49
47
  include mod
50
- backend.define_hash_initializer(mod, columns)
51
- backend.define_column_changes(mod, attributes, column_affix: options[:column_affix])
48
+ backend_class.define_hash_initializer(mod, columns)
49
+ backend_class.define_column_changes(mod, attributes, column_affix: options[:column_affix])
52
50
 
53
51
  plugin :defaults_setter
54
52
  columns.each { |column| default_values[column] = {} }
@@ -84,7 +84,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
84
84
  visit_sql_identifier(predicate, locale)
85
85
  when ::Sequel::SQL::BooleanExpression
86
86
  visit_boolean(predicate, locale)
87
- when ::Sequel::SQL::Expression
87
+ when ::Sequel::SQL::ComplexExpression
88
88
  visit(predicate.args, locale)
89
89
  else
90
90
  nil
@@ -116,9 +116,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
116
116
  end
117
117
  end
118
118
 
119
- backend = self
120
-
121
- setup do |attributes, options|
119
+ setup do |attributes, options, backend_class|
122
120
  association_name = options[:association_name]
123
121
  subclass_name = options[:subclass_name]
124
122
 
@@ -155,7 +153,7 @@ Implements the {Mobility::Backends::Table} backend for Sequel models.
155
153
  include callback_methods
156
154
 
157
155
  include(mod = Module.new)
158
- backend.define_column_changes(mod, attributes)
156
+ backend_class.define_column_changes(mod, attributes)
159
157
  end
160
158
 
161
159
  def translation_for(locale, **)
@@ -40,7 +40,7 @@ Format for serialization. Either +:yaml+ (default) or +:json+.
40
40
  return if obj.nil?
41
41
  if obj.is_a? ::Hash
42
42
  obj = obj.inject({}) do |translations, (locale, value)|
43
- translations[locale] = value.to_s if Util.present?(value)
43
+ translations[locale] = value.to_s unless value.nil?
44
44
  translations
45
45
  end
46
46
  else
@@ -0,0 +1,66 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ =begin
6
+
7
+ Plugin to use an original column for a given locale, and otherwise use the backend.
8
+
9
+ =end
10
+ module ActiveRecord
11
+ module ColumnFallback
12
+ extend Plugin
13
+
14
+ requires :column_fallback, include: false
15
+
16
+ included_hook do |_, backend_class|
17
+ backend_class.include BackendInstanceMethods
18
+ backend_class.extend BackendClassMethods
19
+ end
20
+
21
+ def self.use_column_fallback?(options, locale)
22
+ case column_fallback = options[:column_fallback]
23
+ when TrueClass
24
+ locale == I18n.default_locale
25
+ when Array
26
+ column_fallback.include?(locale)
27
+ when Proc
28
+ column_fallback.call(locale)
29
+ else
30
+ false
31
+ end
32
+ end
33
+
34
+ module BackendInstanceMethods
35
+ def read(locale, **)
36
+ if ColumnFallback.use_column_fallback?(options, locale)
37
+ model.read_attribute(attribute)
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def write(locale, value, **)
44
+ if ColumnFallback.use_column_fallback?(options, locale)
45
+ model.send(:write_attribute, attribute, value)
46
+ else
47
+ super
48
+ end
49
+ end
50
+ end
51
+
52
+ module BackendClassMethods
53
+ def build_node(attr, locale)
54
+ if ColumnFallback.use_column_fallback?(options, locale)
55
+ model_class.arel_table[attr]
56
+ else
57
+ super
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ register_plugin(:active_record_column_fallback, ActiveRecord::ColumnFallback)
65
+ end
66
+ end
@@ -4,6 +4,7 @@ require_relative "./active_record/dirty"
4
4
  require_relative "./active_record/cache"
5
5
  require_relative "./active_record/query"
6
6
  require_relative "./active_record/uniqueness_validation"
7
+ require_relative "./active_record/column_fallback"
7
8
 
8
9
  module Mobility
9
10
  =begin
@@ -24,6 +25,7 @@ dirty for active_record_dirty) is also enabled.
24
25
  requires :active_record_cache
25
26
  requires :active_record_query
26
27
  requires :active_record_uniqueness_validation
28
+ requires :active_record_column_fallback
27
29
 
28
30
 
29
31
  included_hook do |klass|
@@ -114,15 +114,33 @@ Defines:
114
114
  defaults[key] = [backend, backend_options] if backend
115
115
  end
116
116
 
117
+ class MobilityBackends < Hash
118
+ def initialize(model)
119
+ @model = model
120
+ super()
121
+ end
122
+
123
+ def [](name)
124
+ return fetch(name) if has_key?(name)
125
+ return self[name.to_sym] if String === name
126
+ self[name] = @model.class.mobility_backend_class(name).new(@model, name.to_s)
127
+ end
128
+
129
+ def marshal_dump
130
+ @model
131
+ end
132
+
133
+ def marshal_load(model)
134
+ @model = model
135
+ end
136
+ end
137
+
117
138
  module InstanceMethods
118
139
  # Return a new backend for an attribute name.
119
140
  # @return [Hash] Hash of attribute names and backend instances
120
141
  # @api private
121
142
  def mobility_backends
122
- @mobility_backends ||= ::Hash.new do |hash, name|
123
- next hash[name.to_sym] if String === name
124
- hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
125
- end
143
+ @mobility_backends ||= MobilityBackends.new(self)
126
144
  end
127
145
 
128
146
  def initialize_dup(other)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mobility
4
+ module Plugins
5
+ module ColumnFallback
6
+ extend Plugin
7
+
8
+ default false
9
+
10
+ requires :backend, include: :before
11
+ end
12
+
13
+ register_plugin(:column_fallback, ColumnFallback)
14
+ end
15
+ end
@@ -117,8 +117,10 @@ the current locale was +nil+.
117
117
  # Applies fallbacks plugin to attributes. Completely disables fallbacks
118
118
  # on model if option is +false+.
119
119
  included_hook do |_, backend_class|
120
- fallbacks = options[:fallbacks]
121
- backend_class.include(BackendReader.new(fallbacks, method(:generate_fallbacks))) unless fallbacks == false
120
+ backend_class.include(BackendInstanceMethods) unless options[:fallbacks] == false
121
+ # This is weird. We need to find a better way to allow customization of
122
+ # rarely-customized code like this.
123
+ backend_class.define_method(:generate_fallbacks, &method(:generate_fallbacks))
122
124
  end
123
125
 
124
126
  private
@@ -134,33 +136,26 @@ the current locale was +nil+.
134
136
  end
135
137
  end
136
138
 
137
- class BackendReader < Module
138
- def initialize(fallbacks_option, fallbacks_generator)
139
- @fallbacks_generator = fallbacks_generator
140
- define_read(convert_option_to_fallbacks(fallbacks_option))
141
- end
142
-
143
- private
144
-
145
- def define_read(fallbacks)
146
- define_method :read do |locale, fallback: true, **options|
147
- return super(locale, **options) if !fallback || options[:locale]
139
+ module BackendInstanceMethods
140
+ def read(locale, fallback: true, **accessor_options)
141
+ return super(locale, **options) if !fallback || accessor_options[:locale]
148
142
 
149
- locales = fallback == true ? fallbacks[locale] : [locale, *fallback]
150
- locales.each do |fallback_locale|
151
- value = super(fallback_locale, **options)
152
- return value if Util.present?(value)
153
- end
154
-
155
- super(locale, **options)
143
+ locales = fallback == true ? fallbacks[locale] : [locale, *fallback]
144
+ locales.each do |fallback_locale|
145
+ value = super(fallback_locale, **accessor_options)
146
+ return value if Util.present?(value)
156
147
  end
148
+
149
+ super(locale, **options)
157
150
  end
158
151
 
159
- def convert_option_to_fallbacks(option)
160
- if option.is_a?(::Hash)
161
- @fallbacks_generator[option]
162
- elsif option == true
163
- @fallbacks_generator[{}]
152
+ private
153
+
154
+ def fallbacks
155
+ if options[:fallbacks].is_a?(Hash)
156
+ generate_fallbacks(options[:fallbacks])
157
+ elsif options[:fallbacks] == true
158
+ generate_fallbacks({})
164
159
  else
165
160
  ::Hash.new { [] }
166
161
  end
@@ -55,6 +55,13 @@ model class is generated.
55
55
  end
56
56
  end
57
57
 
58
+ # Following is needed in order to not swallow `kwargs` on ruby >= 3.0.
59
+ # Otherwise `kwargs` are not passed by `super` to a possible other
60
+ # `method_missing` defined like this:
61
+ #
62
+ # def method_missing(name, *args, **kwargs, &block); end
63
+ ruby2_keywords :method_missing
64
+
58
65
  define_method :respond_to_missing? do |method_name, include_private = false|
59
66
  (method_name =~ method_name_regex) || super(method_name, include_private)
60
67
  end
@@ -0,0 +1,66 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Mobility
4
+ =begin
5
+
6
+ Plugin to use an original column for a given locale, and otherwise use the backend.
7
+
8
+ =end
9
+ module Plugins
10
+ module Sequel
11
+ module ColumnFallback
12
+ extend Plugin
13
+
14
+ requires :column_fallback, include: false
15
+
16
+ included_hook do |_, backend_class|
17
+ backend_class.include BackendInstanceMethods
18
+ backend_class.extend BackendClassMethods
19
+ end
20
+
21
+ def self.use_column_fallback?(options, locale)
22
+ case column_fallback = options[:column_fallback]
23
+ when TrueClass
24
+ locale == I18n.default_locale
25
+ when Array
26
+ column_fallback.include?(locale)
27
+ when Proc
28
+ column_fallback.call(locale)
29
+ else
30
+ false
31
+ end
32
+ end
33
+
34
+ module BackendInstanceMethods
35
+ def read(locale, **)
36
+ if ColumnFallback.use_column_fallback?(options, locale)
37
+ model[attribute.to_sym]
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def write(locale, value, **)
44
+ if ColumnFallback.use_column_fallback?(options, locale)
45
+ model[attribute.to_sym] = value
46
+ else
47
+ super
48
+ end
49
+ end
50
+ end
51
+
52
+ module BackendClassMethods
53
+ def build_op(attr, locale)
54
+ if ColumnFallback.use_column_fallback?(options, locale)
55
+ ::Sequel::SQL::QualifiedIdentifier.new(model_class.table_name, attr.to_sym)
56
+ else
57
+ super
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ register_plugin(:sequel_column_fallback, Sequel::ColumnFallback)
65
+ end
66
+ end
@@ -61,7 +61,7 @@ ActiveRecord query plugin.
61
61
  locale = args[0] || @global_locale
62
62
  @locales |= [locale]
63
63
  @model_class.mobility_backend_class(m).build_op(m.to_s, locale)
64
- elsif @model_class.columns.include?(m.to_s)
64
+ elsif @model_class.columns.include?(m)
65
65
  ::Sequel::SQL::QualifiedIdentifier.new(@model_class.table_name, m)
66
66
  else
67
67
  super
@@ -9,6 +9,7 @@ require_relative "./sequel/backend"
9
9
  require_relative "./sequel/dirty"
10
10
  require_relative "./sequel/cache"
11
11
  require_relative "./sequel/query"
12
+ require_relative "./sequel/column_fallback"
12
13
 
13
14
  module Mobility
14
15
  module Plugins
@@ -26,6 +27,7 @@ for sequel_dirty) is also enabled.
26
27
  requires :sequel_dirty
27
28
  requires :sequel_cache
28
29
  requires :sequel_query
30
+ requires :sequel_column_fallback
29
31
 
30
32
  included_hook do |klass|
31
33
  unless sequel_class?(klass)
@@ -7,7 +7,7 @@ module Mobility
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 1
10
- MINOR = 1
10
+ MINOR = 2
11
11
  TINY = 2
12
12
  PRE = nil
13
13
 
data/lib/mobility.rb CHANGED
@@ -73,6 +73,9 @@ fallbacks plugin, whereas +Post+ uses +Translations+ which does not have that
73
73
  plugin enabled.
74
74
 
75
75
  =end
76
+
77
+ def ruby2_keywords(*); end unless respond_to?(:ruby2_keywords, true)
78
+
76
79
  module Mobility
77
80
  # A generic exception used by Mobility.
78
81
  class Error < StandardError
data.tar.gz.sig CHANGED
Binary file
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.1.2
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Salzberg
@@ -34,7 +34,7 @@ cert_chain:
34
34
  eBMcZq0d1tbtv1M1UXND9mOfhLZ31YvoSTPkrJiRpljUNgD0+ugelnr1/5X/9k8y
35
35
  J9QOd3C5jpSShf/HMvpJnFuSYFm19cH9GrHjvw==
36
36
  -----END CERTIFICATE-----
37
- date: 2021-04-26 00:00:00.000000000 Z
37
+ date: 2021-10-03 00:00:00.000000000 Z
38
38
  dependencies:
39
39
  - !ruby/object:Gem::Dependency
40
40
  name: request_store
@@ -198,6 +198,7 @@ files:
198
198
  - lib/mobility/plugins/active_record.rb
199
199
  - lib/mobility/plugins/active_record/backend.rb
200
200
  - lib/mobility/plugins/active_record/cache.rb
201
+ - lib/mobility/plugins/active_record/column_fallback.rb
201
202
  - lib/mobility/plugins/active_record/dirty.rb
202
203
  - lib/mobility/plugins/active_record/query.rb
203
204
  - lib/mobility/plugins/active_record/uniqueness_validation.rb
@@ -209,6 +210,7 @@ files:
209
210
  - lib/mobility/plugins/backend.rb
210
211
  - lib/mobility/plugins/backend_reader.rb
211
212
  - lib/mobility/plugins/cache.rb
213
+ - lib/mobility/plugins/column_fallback.rb
212
214
  - lib/mobility/plugins/default.rb
213
215
  - lib/mobility/plugins/dirty.rb
214
216
  - lib/mobility/plugins/fallbacks.rb
@@ -220,6 +222,7 @@ files:
220
222
  - lib/mobility/plugins/sequel.rb
221
223
  - lib/mobility/plugins/sequel/backend.rb
222
224
  - lib/mobility/plugins/sequel/cache.rb
225
+ - lib/mobility/plugins/sequel/column_fallback.rb
223
226
  - lib/mobility/plugins/sequel/dirty.rb
224
227
  - lib/mobility/plugins/sequel/query.rb
225
228
  - lib/mobility/plugins/writer.rb
metadata.gz.sig CHANGED
Binary file