mobility 0.8.10 → 1.0.0.beta2

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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +66 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +36 -101
  7. data/README.md +183 -91
  8. data/Rakefile +6 -4
  9. data/lib/mobility.rb +44 -166
  10. data/lib/mobility/arel.rb +1 -1
  11. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  12. data/lib/mobility/backend.rb +27 -51
  13. data/lib/mobility/backends.rb +20 -0
  14. data/lib/mobility/backends/active_record.rb +4 -0
  15. data/lib/mobility/backends/active_record/column.rb +2 -0
  16. data/lib/mobility/backends/active_record/container.rb +6 -7
  17. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  18. data/lib/mobility/backends/active_record/json.rb +2 -0
  19. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  20. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  21. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  22. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  23. data/lib/mobility/backends/active_record/table.rb +6 -4
  24. data/lib/mobility/backends/column.rb +0 -6
  25. data/lib/mobility/backends/container.rb +10 -1
  26. data/lib/mobility/backends/hash.rb +39 -0
  27. data/lib/mobility/backends/hash_valued.rb +4 -0
  28. data/lib/mobility/backends/hstore.rb +0 -1
  29. data/lib/mobility/backends/json.rb +0 -1
  30. data/lib/mobility/backends/jsonb.rb +1 -2
  31. data/lib/mobility/backends/key_value.rb +31 -26
  32. data/lib/mobility/backends/null.rb +2 -0
  33. data/lib/mobility/backends/sequel.rb +5 -2
  34. data/lib/mobility/backends/sequel/column.rb +2 -0
  35. data/lib/mobility/backends/sequel/container.rb +6 -6
  36. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  37. data/lib/mobility/backends/sequel/json.rb +3 -0
  38. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  39. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  40. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  41. data/lib/mobility/backends/sequel/table.rb +5 -2
  42. data/lib/mobility/backends/serialized.rb +1 -3
  43. data/lib/mobility/backends/table.rb +29 -26
  44. data/lib/mobility/pluggable.rb +56 -0
  45. data/lib/mobility/plugin.rb +260 -0
  46. data/lib/mobility/plugins.rb +27 -24
  47. data/lib/mobility/plugins/active_model.rb +17 -0
  48. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  49. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  50. data/lib/mobility/plugins/active_record.rb +34 -0
  51. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  52. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  53. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  54. data/lib/mobility/plugins/active_record/query.rb +48 -34
  55. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  56. data/lib/mobility/plugins/attribute_methods.rb +29 -20
  57. data/lib/mobility/plugins/attributes.rb +72 -0
  58. data/lib/mobility/plugins/backend.rb +161 -0
  59. data/lib/mobility/plugins/backend_reader.rb +34 -0
  60. data/lib/mobility/plugins/cache.rb +68 -26
  61. data/lib/mobility/plugins/default.rb +22 -17
  62. data/lib/mobility/plugins/dirty.rb +12 -33
  63. data/lib/mobility/plugins/fallbacks.rb +52 -44
  64. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  65. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  66. data/lib/mobility/plugins/presence.rb +28 -21
  67. data/lib/mobility/plugins/query.rb +8 -17
  68. data/lib/mobility/plugins/reader.rb +50 -0
  69. data/lib/mobility/plugins/sequel.rb +34 -0
  70. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  71. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  72. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  73. data/lib/mobility/plugins/sequel/query.rb +21 -6
  74. data/lib/mobility/plugins/writer.rb +44 -0
  75. data/lib/mobility/translations.rb +95 -0
  76. data/lib/mobility/version.rb +12 -1
  77. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  78. metadata +28 -27
  79. metadata.gz.sig +0 -0
  80. data/lib/mobility/active_model.rb +0 -4
  81. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  82. data/lib/mobility/active_record.rb +0 -23
  83. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  85. data/lib/mobility/attributes.rb +0 -324
  86. data/lib/mobility/backend/orm_delegator.rb +0 -44
  87. data/lib/mobility/backend_resetter.rb +0 -50
  88. data/lib/mobility/configuration.rb +0 -138
  89. data/lib/mobility/fallbacks.rb +0 -28
  90. data/lib/mobility/interface.rb +0 -0
  91. data/lib/mobility/loaded.rb +0 -4
  92. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  93. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  94. data/lib/mobility/sequel.rb +0 -9
  95. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  96. data/lib/mobility/translates.rb +0 -73
@@ -40,29 +40,44 @@ locale suffix, so +title_en+, +title_pt_br+, etc.)
40
40
 
41
41
  =end
42
42
  module Dirty
43
- class MethodsBuilder < ActiveModel::Dirty::MethodsBuilder
44
- # @param [Attributes] attributes
45
- def included(model_class)
46
- super
43
+ extend Plugin
47
44
 
48
- model_class.include InstanceMethods
45
+ requires :dirty, include: false
46
+ requires :active_model_dirty, include: :before
47
+
48
+ initialize_hook do
49
+ if options[:dirty]
50
+ include InstanceMethods
49
51
  end
52
+ end
50
53
 
51
- class << self
52
- def dirty_class
53
- @dirty_class ||= (Class.new do
54
- # In earlier versions of Rails, these are needed to avoid an
55
- # exception when including the AR Dirty module outside of an
56
- # AR::Base class. Eventually we should be able to drop them.
57
- def self.after_create; end
58
- def self.after_update; end
59
-
60
- include ::ActiveRecord::AttributeMethods::Dirty
61
- end)
62
- end
54
+ included_hook do |_, backend_class|
55
+ if options[:dirty]
56
+ backend_class.include BackendMethods
63
57
  end
64
58
  end
65
59
 
60
+ private
61
+
62
+ def dirty_handler_methods
63
+ HandlerMethods
64
+ end
65
+
66
+ # Module which defines generic ActiveRecord::Dirty handler methods like
67
+ # +attribute_before_last_save+ that are patched to work with translated
68
+ # attributes.
69
+ HandlerMethods = ActiveModel::Dirty::HandlerMethodsBuilder.new(
70
+ Class.new do
71
+ # In earlier versions of Rails, these are needed to avoid an
72
+ # exception when including the AR Dirty module outside of an
73
+ # AR::Base class. Eventually we should be able to drop them.
74
+ def self.after_create; end
75
+ def self.after_update; end
76
+
77
+ include ::ActiveRecord::AttributeMethods::Dirty
78
+ end
79
+ )
80
+
66
81
  module InstanceMethods
67
82
  if ::ActiveRecord::VERSION::STRING >= '5.1' # define patterns added in 5.1
68
83
  def saved_changes
@@ -98,5 +113,7 @@ locale suffix, so +title_en+, +title_pt_br+, etc.)
98
113
  BackendMethods = ActiveModel::Dirty::BackendMethods
99
114
  end
100
115
  end
116
+
117
+ register_plugin(:active_record_dirty, ActiveRecord::Dirty)
101
118
  end
102
119
  end
@@ -1,4 +1,6 @@
1
1
  # frozen-string-literal: true
2
+ require "active_record/relation"
3
+
2
4
  module Mobility
3
5
  module Plugins
4
6
  =begin
@@ -15,45 +17,47 @@ enabled for any one attribute on the model.
15
17
  =end
16
18
  module ActiveRecord
17
19
  module Query
18
- class << self
19
- def apply(attributes)
20
- attributes.model_class.class_eval do
20
+ extend Plugin
21
+
22
+ requires :query, include: false
23
+
24
+ included_hook do |klass, backend_class|
25
+ plugin = self
26
+ if options[:query]
27
+ raise MissingBackend, "backend required for Query plugin" unless backend_class
28
+
29
+ klass.class_eval do
21
30
  extend QueryMethod
22
- extend FindByMethods.new(*attributes.names)
23
- singleton_class.send :alias_method, Mobility.query_method, :__mobility_query_scope__
31
+ extend FindByMethods.new(*plugin.names)
32
+ singleton_class.send :alias_method, plugin.query_method, :__mobility_query_scope__
24
33
  end
25
- attributes.backend_class.include self
34
+ backend_class.include BackendMethods
26
35
  end
36
+ end
27
37
 
38
+ class << self
28
39
  def attribute_alias(attribute, locale = Mobility.locale)
29
40
  "__mobility_%s_%s__" % [attribute, ::Mobility.normalize_locale(locale)]
30
41
  end
31
42
  end
32
43
 
33
- # @note We use +instance_variable_get+ here to get the +AttributeSet+
34
- # rather than the hash of attributes. Getting the full hash of
35
- # attributes is a performance hit and better to avoid if unnecessary.
36
- # TODO: Improve this.
37
- def read(locale, **)
38
- if (model_attributes_defined? &&
39
- model_attributes.key?(alias_ = Query.attribute_alias(attribute, locale)))
40
- model_attributes[alias_].value
41
- else
42
- super
44
+ module BackendMethods
45
+ # @note We use +instance_variable_get+ here to get the +AttributeSet+
46
+ # rather than the hash of attributes. Getting the full hash of
47
+ # attributes is a performance hit and better to avoid if unnecessary.
48
+ # TODO: Improve this.
49
+ def read(locale, **)
50
+ if model.instance_variable_defined?(:@attributes) &&
51
+ (model_attributes = model.instance_variable_get(:@attributes)).key?(alias_ = Query.attribute_alias(attribute, locale))
52
+ model_attributes[alias_].value
53
+ else
54
+ super
55
+ end
43
56
  end
44
57
  end
45
58
 
46
- private
47
-
48
- def model_attributes_defined?
49
- model.instance_variable_defined?(:@attributes)
50
- end
51
-
52
- def model_attributes
53
- model.instance_variable_get(:@attributes)
54
- end
55
-
56
59
  module QueryMethod
60
+ # This is required for UniquenessValidator.
57
61
  def __mobility_query_scope__(locale: Mobility.locale, &block)
58
62
  if block_given?
59
63
  VirtualRow.build_query(self, locale, &block)
@@ -121,7 +125,7 @@ enabled for any one attribute on the model.
121
125
  case opts
122
126
  when Symbol, String
123
127
  @klass.mobility_attribute?(opts) ? order({ opts => :asc }, *rest) : super
124
- when Hash
128
+ when ::Hash
125
129
  i18n_keys, keys = opts.keys.partition(&@klass.method(:mobility_attribute?))
126
130
  return super if i18n_keys.empty?
127
131
 
@@ -139,8 +143,10 @@ enabled for any one attribute on the model.
139
143
 
140
144
  if ::ActiveRecord::VERSION::STRING >= '5.0'
141
145
  %w[pluck group select].each do |method_name|
142
- define_method method_name do |*attrs|
143
- return super(*attrs) unless attrs.any?(&@klass.method(:mobility_attribute?))
146
+ define_method method_name do |*attrs, &block|
147
+ return super(*attrs, &block) if (method_name == 'select' && block.present?)
148
+
149
+ return super(*attrs, &block) unless attrs.any?(&@klass.method(:mobility_attribute?))
144
150
 
145
151
  keys = attrs.dup
146
152
 
@@ -154,7 +160,7 @@ enabled for any one attribute on the model.
154
160
  @klass.mobility_backend_class(key).apply_scope(query, backend_node(key))
155
161
  end
156
162
 
157
- base.public_send(method_name, *keys)
163
+ base.public_send(method_name, *keys, &block)
158
164
  end
159
165
  end
160
166
  end
@@ -180,7 +186,7 @@ enabled for any one attribute on the model.
180
186
 
181
187
  class << self
182
188
  def build(scope, where_opts, invert: false, &block)
183
- return yield unless Hash === where_opts
189
+ return yield unless ::Hash === where_opts
184
190
 
185
191
  opts = where_opts.with_indifferent_access
186
192
  locale = opts.delete(:locale) || Mobility.locale
@@ -193,11 +199,11 @@ enabled for any one attribute on the model.
193
199
  # Builds a translated relation for a given opts hash and optional
194
200
  # invert boolean.
195
201
  def _build(scope, opts, locale, invert)
196
- return yield unless scope.respond_to?(:mobility_modules)
202
+ return yield if (mods = attribute_modules(scope)).empty?
197
203
 
198
204
  keys, predicates = opts.keys.map(&:to_s), []
199
205
 
200
- query_map = scope.mobility_modules.inject(IDENTITY) do |qm, mod|
206
+ query_map = mods.inject(IDENTITY) do |qm, mod|
201
207
  i18n_keys = mod.names & keys
202
208
  next qm if i18n_keys.empty?
203
209
 
@@ -213,7 +219,11 @@ enabled for any one attribute on the model.
213
219
  return yield if query_map == IDENTITY
214
220
 
215
221
  relation = opts.empty? ? scope : yield(opts)
216
- query_map[relation.where(predicates.inject(&:and))]
222
+ query_map[relation.where(predicates.inject(:and))]
223
+ end
224
+
225
+ def attribute_modules(scope)
226
+ scope.model.ancestors.grep(::Mobility::Translations)
217
227
  end
218
228
 
219
229
  def build_predicate(node, values)
@@ -265,6 +275,10 @@ enabled for any one attribute on the model.
265
275
 
266
276
  private_constant :QueryExtension, :FindByMethods
267
277
  end
278
+
279
+ class MissingBackend < Mobility::Error; end
268
280
  end
281
+
282
+ register_plugin(:active_record_query, ActiveRecord::Query)
269
283
  end
270
284
  end
@@ -0,0 +1,60 @@
1
+ module Mobility
2
+ module Plugins
3
+ module ActiveRecord
4
+ module UniquenessValidation
5
+ extend Plugin
6
+
7
+ requires :query, include: false
8
+
9
+ included_hook do |klass|
10
+ klass.class_eval do
11
+ unless const_defined?(:UniquenessValidator, false)
12
+ self.const_set(:UniquenessValidator, Class.new(UniquenessValidator))
13
+ end
14
+ end
15
+ end
16
+
17
+ class UniquenessValidator < ::ActiveRecord::Validations::UniquenessValidator
18
+ # @param [ActiveRecord::Base] record Translated model
19
+ # @param [String] attribute Name of attribute
20
+ # @param [Object] value Attribute value
21
+ def validate_each(record, attribute, value)
22
+ klass = record.class
23
+
24
+ if ([*options[:scope]] + [attribute]).any? { |name| klass.mobility_attribute?(name) }
25
+ return unless value.present?
26
+ relation = klass.unscoped.__mobility_query_scope__ do |m|
27
+ node = m.__send__(attribute)
28
+ options[:case_sensitive] == false ? node.lower.eq(value.downcase) : node.eq(value)
29
+ end
30
+ relation = relation.where.not(klass.primary_key => record.id) if record.persisted?
31
+ relation = mobility_scope_relation(record, relation)
32
+ relation = relation.merge(options[:conditions]) if options[:conditions]
33
+
34
+ if relation.exists?
35
+ error_options = options.except(:case_sensitive, :scope, :conditions)
36
+ error_options[:value] = value
37
+
38
+ record.errors.add(attribute, :taken, **error_options)
39
+ end
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def mobility_scope_relation(record, relation)
48
+ [*options[:scope]].inject(relation) do |scoped_relation, scope_item|
49
+ scoped_relation.__mobility_query_scope__ do |m|
50
+ m.__send__(scope_item).eq(record.send(scope_item))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ register_plugin(:active_record_uniqueness_validation, ActiveRecord::UniquenessValidation)
59
+ end
60
+ end
@@ -12,30 +12,39 @@ attributes only.
12
12
 
13
13
  =end
14
14
  module AttributeMethods
15
- class << self
16
- # Applies attribute_methods plugin for a given option value.
17
- # @param [Attributes] attributes
18
- # @param [Boolean] option Value of option
19
- # @raise [ArgumentError] if model class does not support dirty tracking
20
- def apply(attributes, option)
21
- if option
22
- include_attribute_methods_module(attributes.model_class, *attributes.names)
23
- end
15
+ extend Plugin
16
+
17
+ default true
18
+ requires :attributes
19
+
20
+ initialize_hook do |*names|
21
+ include InstanceMethods
22
+
23
+ define_method :translated_attributes do
24
+ super().merge(names.inject({}) do |attributes, name|
25
+ attributes.merge(name.to_s => send(name))
26
+ end)
27
+ end
28
+ end
29
+
30
+ # Applies attribute_methods plugin for a given option value.
31
+ included_hook do
32
+ if options[:attribute_methods]
33
+ define_method :untranslated_attributes, ::ActiveRecord::Base.instance_method(:attributes)
24
34
  end
35
+ end
25
36
 
26
- private
27
-
28
- def include_attribute_methods_module(model_class, *attribute_names)
29
- module_builder =
30
- if Loaded::ActiveRecord && model_class.ancestors.include?(::ActiveRecord::AttributeMethods)
31
- require "mobility/plugins/active_record/attribute_methods"
32
- Plugins::ActiveRecord::AttributeMethods
33
- else
34
- raise ArgumentError, "#{model_class} does not support AttributeMethods plugin."
35
- end
36
- model_class.include module_builder.new(*attribute_names)
37
+ module InstanceMethods
38
+ def translated_attributes
39
+ {}
40
+ end
41
+
42
+ def attributes
43
+ super.merge(translated_attributes)
37
44
  end
38
45
  end
39
46
  end
47
+
48
+ register_plugin(:attribute_methods, AttributeMethods)
40
49
  end
41
50
  end
@@ -0,0 +1,72 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ =begin
5
+
6
+ Takes arguments, converts them to strings, and stores in an array +@names+,
7
+ made available with an +attr_reader+. Also provides some convenience methods
8
+ for aggregating attributes.
9
+
10
+ =end
11
+ module Attributes
12
+ extend Plugin
13
+
14
+ # Attribute names for which accessors will be defined
15
+ # @return [Array<String>] Array of names
16
+ attr_reader :names
17
+
18
+ initialize_hook do |*names|
19
+ @names = names.map(&:to_s).freeze
20
+ end
21
+
22
+ # Yield each attribute name to block
23
+ # @yieldparam [String] Attribute
24
+ def each &block
25
+ names.each(&block)
26
+ end
27
+
28
+ # Show useful information about this module.
29
+ # @return [String]
30
+ def inspect
31
+ "#<Translations @names=#{names.join(", ")}>"
32
+ end
33
+
34
+ included_hook do |klass|
35
+ names = @names
36
+
37
+ klass.class_eval do
38
+ extend ClassMethods
39
+ names.each { |name| mobility_attributes << name.to_s }
40
+ mobility_attributes.uniq!
41
+ rescue FrozenError
42
+ raise FrozenAttributesError, "Attempting to translate these attributes on #{klass}, which has already been subclassed: #{names.join(', ')}."
43
+ end
44
+ end
45
+
46
+ module ClassMethods
47
+ # Return true if attribute name is translated on this model.
48
+ # @param [String, Symbol] Attribute name
49
+ # @return [Boolean]
50
+ def mobility_attribute?(name)
51
+ mobility_attributes.include?(name.to_s)
52
+ end
53
+
54
+ # Return translated attribute names on this model.
55
+ # @return [Array<String>] Attribute names
56
+ def mobility_attributes
57
+ @mobility_attributes ||= []
58
+ end
59
+
60
+ def inherited(klass)
61
+ super
62
+ attrs = mobility_attributes.freeze # ensure attributes are not modified after being inherited
63
+ klass.class_eval { @mobility_attributes = attrs.dup }
64
+ end
65
+ end
66
+
67
+ class FrozenAttributesError < Error; end
68
+ end
69
+
70
+ register_plugin(:attributes, Attributes)
71
+ end
72
+ end
@@ -0,0 +1,161 @@
1
+ # frozen-string-literal: true
2
+ module Mobility
3
+ module Plugins
4
+ =begin
5
+
6
+ Plugin for setting up a backend for a set of model attributes. All backend
7
+ plugins must depend on this.
8
+
9
+ Defines:
10
+ - instance method +mobility_backends+ which returns a hash whose keys are
11
+ attribute names and values a backend for each attribute.
12
+ - class method +mobility_backend_class+ which takes an attribute name and
13
+ returns the backend class for that name.
14
+
15
+ =end
16
+ module Backend
17
+ extend Plugin
18
+
19
+ requires :attributes, include: :before
20
+
21
+ # Backend class
22
+ # @return [Class] Backend class
23
+ attr_reader :backend_class
24
+
25
+ # Backend
26
+ # @return [Symbol,Class,Class] Name of backend, or backend class
27
+ attr_reader :backend
28
+
29
+ # Backend options
30
+ # @return [Hash] Options for backend
31
+ attr_reader :backend_options
32
+
33
+ def initialize(*args, **original_options)
34
+ super
35
+
36
+ # Validate that the default backend from config has valid keys
37
+ if (default = self.class.defaults[:backend])
38
+ name, backend_options = default
39
+ extra_keys = backend_options.keys - backend.valid_keys
40
+ raise InvalidOptionKey, "These are not valid #{name} backend keys: #{extra_keys.join(', ')}." unless extra_keys.empty?
41
+ end
42
+
43
+ include InstanceMethods
44
+ end
45
+
46
+ # Setup backend class, include modules into model class, include/extend
47
+ # shared modules and setup model with backend setup block (see
48
+ # {Mobility::Backend::Setup#setup_model}).
49
+ def included(klass)
50
+ super
51
+
52
+ klass.extend ClassMethods
53
+
54
+ if backend
55
+ @backend_class = backend.build_subclass(klass, backend_options)
56
+
57
+ backend_class.setup_model(klass, names)
58
+
59
+ names = @names
60
+ backend_class = @backend_class
61
+
62
+ klass.class_eval do
63
+ names.each { |name| mobility_backend_classes[name.to_sym] = backend_class }
64
+ end
65
+
66
+ backend_class
67
+ end
68
+ end
69
+
70
+ # Include backend name in inspect string.
71
+ # @return [String]
72
+ def inspect
73
+ "#<Translations (#{backend}) @names=#{names.join(", ")}>"
74
+ end
75
+
76
+ def load_backend(backend)
77
+ Backends.load_backend(backend)
78
+ rescue Backends::LoadError => e
79
+ raise e, "could not find a #{backend} backend. Did you forget to include an ORM plugin like active_record or sequel?"
80
+ end
81
+
82
+ private
83
+
84
+ # Override to extract backend options from options hash.
85
+ def initialize_options(original_options)
86
+ super
87
+
88
+ case options[:backend]
89
+ when String, Symbol, Class
90
+ @backend, @backend_options = options[:backend], options
91
+ when Array
92
+ @backend, @backend_options = options[:backend]
93
+ @backend_options = @backend_options.merge(original_options)
94
+ when NilClass
95
+ @backend = @backend_options = nil
96
+ else
97
+ raise ArgumentError, "backend must be either a backend name, a backend class, or a two-element array"
98
+ end
99
+
100
+ @backend = load_backend(backend)
101
+ end
102
+
103
+ # Override default validation to exclude backend options, which may be
104
+ # mixed in with plugin options.
105
+ def validate_options(options)
106
+ return super unless backend
107
+ super(options.slice(*(options.keys - backend.valid_keys)))
108
+ end
109
+
110
+ # Override default argument-handling in DSL to store kwargs passed along
111
+ # with plugin name.
112
+ def self.configure_default(defaults, key, backend = nil, backend_options = {})
113
+ defaults[key] = [backend, backend_options] if backend
114
+ end
115
+
116
+ module InstanceMethods
117
+ # Return a new backend for an attribute name.
118
+ # @return [Hash] Hash of attribute names and backend instances
119
+ # @api private
120
+ def mobility_backends
121
+ @mobility_backends ||= ::Hash.new do |hash, name|
122
+ next hash[name.to_sym] if String === name
123
+ hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
124
+ end
125
+ end
126
+
127
+ def initialize_dup(other)
128
+ @mobility_backends = nil
129
+ super
130
+ end
131
+ end
132
+
133
+ module ClassMethods
134
+ # Return backend class for a given attribute name.
135
+ # @param [Symbol,String] Name of attribute
136
+ # @return [Class] Backend class
137
+ def mobility_backend_class(name)
138
+ mobility_backend_classes.fetch(name.to_sym)
139
+ rescue KeyError
140
+ raise KeyError, "No backend for: #{name}"
141
+ end
142
+
143
+ def inherited(klass)
144
+ parent_classes = mobility_backend_classes.freeze # ensure backend classes are not modified after being inherited
145
+ klass.class_eval { @mobility_backend_classes = parent_classes.dup }
146
+ super
147
+ end
148
+
149
+ protected
150
+
151
+ def mobility_backend_classes
152
+ @mobility_backend_classes ||= {}
153
+ end
154
+ end
155
+
156
+ class InvalidOptionKey < Error; end
157
+ end
158
+
159
+ register_plugin(:backend, Backend)
160
+ end
161
+ end