mobility 1.1.3 → 1.2.3

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: 225a8aa8a3eb8691e9a89d60fdd56509b47f7f36d22d1bb8d0c6f485f0e7cc2d
4
- data.tar.gz: be10ccdea2727e568da790950b4df0633a543094543f7fd5f9c89154d5ea803e
3
+ metadata.gz: 7b94201a6b91ad2fff4818aa37fa4fd99df97944d98f3888af6b76f732f71658
4
+ data.tar.gz: 8be29d872e1488575253302daa13b46d5f065c5dd9aa35c526c0fd561ae478c5
5
5
  SHA512:
6
- metadata.gz: 6f350d0a8e5fcf4f1804716f2cdfc0d500abd033ca0243e0c6be663adedf3ee192d785546a1be1274acbd5738533c7b5e3b9f8c48446e22bde39c26de8217819
7
- data.tar.gz: 78fb7729118181d00e21f78c1144166916270e555bb4191c05a10cc660fb57d36b8af034cc7d42bfbd3d66d9509d1686f80554f85538b9a6d0fd488becd12966
6
+ metadata.gz: a7afca6199b32df4105ad2bd55d33c6213677d424d6a4bb22e5d39e33b649de2e256a04b1f03e9c33d6374e5c4ec7858a7de9b2a0e190cc1a417da895981b7a0
7
+ data.tar.gz: 23ef1c7ca95c54872c5504a50af6c94116c94157586ba755a9ed57e6ca878647a1693460232840ed58c47f5807b6fd6c6db2283c397faf7a7e3402e93f675be3
checksums.yaml.gz.sig CHANGED
Binary file
data/CHANGELOG.md CHANGED
@@ -1,8 +1,32 @@
1
1
  # Mobility Changelog
2
2
 
3
- ## Unreleased
4
- - Assign blank values in pg hash backends
5
- ([#516](https://github.com/shioyama/mobility/pull/516))
3
+ ## 1.2
4
+
5
+ ### 1.2.3
6
+ - Fix passing wrong options to super in fallbacks plugin
7
+ ([#539](https://github.com/shioyama/mobility/pull/539))
8
+
9
+ ### 1.2.2
10
+ - Make models work with `Marshal.dump`
11
+ ([#532](https://github.com/shioyama/mobility/pull/532))
12
+ - Fix Sequel container op in Sequel
13
+ ([#533](https://github.com/shioyama/mobility/pull/533))
14
+ - Simplify Fallbacks plugin
15
+ ([#531](https://github.com/shioyama/mobility/pull/531))
16
+
17
+ ### 1.2.1
18
+ - Refactor ColumnFallback plugin
19
+ ([#530](https://github.com/shioyama/mobility/pull/530))
20
+
21
+ ### 1.2.0
22
+ - Add ColumnFallback plugin
23
+ ([#512](https://github.com/shioyama/mobility/pull/512))
24
+ - Fix Sequel querying on untranslated attributes in `i18n` block
25
+ ([#529](https://github.com/shioyama/mobility/pull/529))
26
+ - Allow passing configured backend class as third argument to setup
27
+ ([#528](https://github.com/shioyama/mobility/pull/528))
28
+ - Clearly distinguish backend classes from their configured subclasses
29
+ ([#527](https://github.com/shioyama/mobility/pull/527))
6
30
 
7
31
  ## 1.1
8
32
 
@@ -10,6 +34,8 @@
10
34
  - Do not swallow keyword args on ruby 3 in fallthrough accessors
11
35
  ([#520](https://github.com/shioyama/mobility/pull/520)) thanks
12
36
  [doits](https://github.com/doits)!
37
+ - Assign blank values in pg hash backends
38
+ ([#516](https://github.com/shioyama/mobility/pull/516))
13
39
 
14
40
  ### 1.1.2
15
41
  - Check whether class responds to mobility_attribute?
data/Gemfile.lock CHANGED
@@ -1,28 +1,44 @@
1
+ GIT
2
+ remote: https://github.com/rails/rails.git
3
+ revision: 0a751a021bfc0c71bb2a10ff8af9654c785c94f1
4
+ branch: main
5
+ specs:
6
+ activemodel (7.0.0.alpha2)
7
+ activesupport (= 7.0.0.alpha2)
8
+ activerecord (7.0.0.alpha2)
9
+ activemodel (= 7.0.0.alpha2)
10
+ activesupport (= 7.0.0.alpha2)
11
+ activesupport (7.0.0.alpha2)
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ i18n (>= 1.6, < 2)
14
+ minitest (>= 5.1)
15
+ tzinfo (~> 2.0)
16
+
1
17
  PATH
2
18
  remote: .
3
19
  specs:
4
- mobility (1.1.2)
20
+ mobility (1.3.0.alpha)
5
21
  i18n (>= 0.6.10, < 2)
6
22
  request_store (~> 1.0)
7
23
 
8
24
  GEM
9
25
  remote: https://rubygems.org/
10
26
  specs:
11
- benchmark-ips (2.8.4)
27
+ benchmark-ips (2.9.1)
12
28
  byebug (11.1.3)
13
29
  coderay (1.1.3)
14
- concurrent-ruby (1.1.8)
30
+ concurrent-ruby (1.1.9)
15
31
  database_cleaner (1.99.0)
16
32
  diff-lcs (1.4.4)
17
- ffi (1.15.0)
18
- formatador (0.2.5)
19
- guard (2.16.2)
33
+ ffi (1.15.4)
34
+ formatador (0.3.0)
35
+ guard (2.18.0)
20
36
  formatador (>= 0.2.4)
21
37
  listen (>= 2.7, < 4.0)
22
38
  lumberjack (>= 1.0.12, < 2.0)
23
39
  nenv (~> 0.1)
24
40
  notiffany (~> 0.0)
25
- pry (>= 0.9.12)
41
+ pry (>= 0.13.0)
26
42
  shellany (~> 0.0)
27
43
  thor (>= 0.18.1)
28
44
  guard-compat (1.2.1)
@@ -32,11 +48,12 @@ GEM
32
48
  rspec (>= 2.99.0, < 4.0)
33
49
  i18n (1.8.10)
34
50
  concurrent-ruby (~> 1.0)
35
- listen (3.5.1)
51
+ listen (3.7.0)
36
52
  rb-fsevent (~> 0.10, >= 0.10.3)
37
53
  rb-inotify (~> 0.9, >= 0.9.10)
38
54
  lumberjack (1.2.8)
39
55
  method_source (1.0.0)
56
+ minitest (5.14.4)
40
57
  nenv (0.3.0)
41
58
  notiffany (0.1.3)
42
59
  nenv (~> 0.1)
@@ -50,7 +67,7 @@ GEM
50
67
  pry (~> 0.13.0)
51
68
  rack (2.2.3)
52
69
  rake (12.3.3)
53
- rb-fsevent (0.10.4)
70
+ rb-fsevent (0.11.0)
54
71
  rb-inotify (0.10.1)
55
72
  ffi (~> 1.0)
56
73
  request_store (1.5.0)
@@ -68,15 +85,18 @@ GEM
68
85
  diff-lcs (>= 1.2.0, < 2.0)
69
86
  rspec-support (~> 3.10.0)
70
87
  rspec-support (3.10.2)
71
- sequel (5.44.0)
72
88
  shellany (0.0.1)
73
89
  thor (1.1.0)
90
+ tzinfo (2.0.4)
91
+ concurrent-ruby (~> 1.0)
74
92
  yard (0.9.26)
75
93
 
76
94
  PLATFORMS
77
95
  ruby
78
96
 
79
97
  DEPENDENCIES
98
+ activerecord!
99
+ activesupport!
80
100
  benchmark-ips
81
101
  database_cleaner (~> 1.5, >= 1.5.3)
82
102
  guard-rspec
@@ -85,7 +105,6 @@ DEPENDENCIES
85
105
  pry-byebug
86
106
  rake (~> 12, >= 12.2.1)
87
107
  rspec (~> 3.0)
88
- sequel (~> 5.0)
89
108
  yard (~> 0.9.0)
90
109
 
91
110
  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.3'
58
+ gem 'mobility', '~> 1.2.3'
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.
@@ -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,9 +33,7 @@ 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
@@ -47,8 +45,8 @@ jsonb).
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, **)
@@ -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, **kwargs)
141
+ return super(locale, **kwargs) if !fallback || kwargs[: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, **kwargs)
146
+ return value if Util.present?(value)
156
147
  end
148
+
149
+ super(locale, **kwargs)
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
@@ -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 = 3
12
12
  PRE = nil
13
13
 
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.3
4
+ version: 1.2.3
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-08-06 00:00:00.000000000 Z
37
+ date: 2021-10-25 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