mobility 1.1.3 → 1.2.3

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: 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