mobility 0.8.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) 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 +63 -0
  5. data/Gemfile +50 -18
  6. data/Gemfile.lock +44 -52
  7. data/Guardfile +23 -1
  8. data/README.md +183 -91
  9. data/Rakefile +6 -4
  10. data/lib/mobility.rb +44 -166
  11. data/lib/mobility/active_record/translation.rb +1 -1
  12. data/lib/mobility/arel.rb +1 -1
  13. data/lib/mobility/arel/nodes/pg_ops.rb +1 -1
  14. data/lib/mobility/backend.rb +27 -51
  15. data/lib/mobility/backends.rb +20 -0
  16. data/lib/mobility/backends/active_record.rb +4 -0
  17. data/lib/mobility/backends/active_record/column.rb +2 -0
  18. data/lib/mobility/backends/active_record/container.rb +6 -7
  19. data/lib/mobility/backends/active_record/hstore.rb +3 -1
  20. data/lib/mobility/backends/active_record/json.rb +2 -0
  21. data/lib/mobility/backends/active_record/jsonb.rb +2 -0
  22. data/lib/mobility/backends/active_record/key_value.rb +6 -4
  23. data/lib/mobility/backends/active_record/pg_hash.rb +1 -1
  24. data/lib/mobility/backends/active_record/serialized.rb +6 -0
  25. data/lib/mobility/backends/active_record/table.rb +6 -4
  26. data/lib/mobility/backends/column.rb +0 -6
  27. data/lib/mobility/backends/container.rb +10 -1
  28. data/lib/mobility/backends/hash.rb +39 -0
  29. data/lib/mobility/backends/hash_valued.rb +4 -0
  30. data/lib/mobility/backends/hstore.rb +0 -1
  31. data/lib/mobility/backends/json.rb +0 -1
  32. data/lib/mobility/backends/jsonb.rb +1 -2
  33. data/lib/mobility/backends/key_value.rb +31 -26
  34. data/lib/mobility/backends/null.rb +2 -0
  35. data/lib/mobility/backends/sequel.rb +5 -2
  36. data/lib/mobility/backends/sequel/column.rb +2 -0
  37. data/lib/mobility/backends/sequel/container.rb +6 -6
  38. data/lib/mobility/backends/sequel/hstore.rb +3 -1
  39. data/lib/mobility/backends/sequel/json.rb +3 -0
  40. data/lib/mobility/backends/sequel/jsonb.rb +3 -1
  41. data/lib/mobility/backends/sequel/key_value.rb +8 -6
  42. data/lib/mobility/backends/sequel/serialized.rb +6 -0
  43. data/lib/mobility/backends/sequel/table.rb +5 -2
  44. data/lib/mobility/backends/serialized.rb +1 -3
  45. data/lib/mobility/backends/table.rb +29 -26
  46. data/lib/mobility/pluggable.rb +56 -0
  47. data/lib/mobility/plugin.rb +260 -0
  48. data/lib/mobility/plugins.rb +27 -24
  49. data/lib/mobility/plugins/active_model.rb +17 -0
  50. data/lib/mobility/plugins/active_model/cache.rb +26 -0
  51. data/lib/mobility/plugins/active_model/dirty.rb +119 -78
  52. data/lib/mobility/plugins/active_record.rb +34 -0
  53. data/lib/mobility/plugins/active_record/backend.rb +25 -0
  54. data/lib/mobility/plugins/active_record/cache.rb +28 -0
  55. data/lib/mobility/plugins/active_record/dirty.rb +34 -17
  56. data/lib/mobility/plugins/active_record/query.rb +48 -34
  57. data/lib/mobility/plugins/active_record/uniqueness_validation.rb +60 -0
  58. data/lib/mobility/plugins/attribute_methods.rb +28 -20
  59. data/lib/mobility/plugins/attributes.rb +70 -0
  60. data/lib/mobility/plugins/backend.rb +161 -0
  61. data/lib/mobility/plugins/backend_reader.rb +34 -0
  62. data/lib/mobility/plugins/cache.rb +68 -26
  63. data/lib/mobility/plugins/default.rb +22 -17
  64. data/lib/mobility/plugins/dirty.rb +12 -33
  65. data/lib/mobility/plugins/fallbacks.rb +52 -44
  66. data/lib/mobility/plugins/fallthrough_accessors.rb +25 -25
  67. data/lib/mobility/plugins/locale_accessors.rb +22 -35
  68. data/lib/mobility/plugins/presence.rb +28 -21
  69. data/lib/mobility/plugins/query.rb +8 -17
  70. data/lib/mobility/plugins/reader.rb +50 -0
  71. data/lib/mobility/plugins/sequel.rb +34 -0
  72. data/lib/mobility/plugins/sequel/backend.rb +25 -0
  73. data/lib/mobility/plugins/sequel/cache.rb +24 -0
  74. data/lib/mobility/plugins/sequel/dirty.rb +33 -22
  75. data/lib/mobility/plugins/sequel/query.rb +21 -6
  76. data/lib/mobility/plugins/writer.rb +44 -0
  77. data/lib/mobility/translations.rb +95 -0
  78. data/lib/mobility/version.rb +12 -1
  79. data/lib/rails/generators/mobility/templates/initializer.rb +96 -78
  80. metadata +51 -51
  81. metadata.gz.sig +0 -0
  82. data/lib/mobility/active_model.rb +0 -4
  83. data/lib/mobility/active_model/backend_resetter.rb +0 -26
  84. data/lib/mobility/active_record.rb +0 -23
  85. data/lib/mobility/active_record/backend_resetter.rb +0 -26
  86. data/lib/mobility/active_record/uniqueness_validator.rb +0 -60
  87. data/lib/mobility/attributes.rb +0 -324
  88. data/lib/mobility/backend/orm_delegator.rb +0 -44
  89. data/lib/mobility/backend_resetter.rb +0 -50
  90. data/lib/mobility/configuration.rb +0 -138
  91. data/lib/mobility/fallbacks.rb +0 -28
  92. data/lib/mobility/interface.rb +0 -0
  93. data/lib/mobility/loaded.rb +0 -4
  94. data/lib/mobility/plugins/active_record/attribute_methods.rb +0 -38
  95. data/lib/mobility/plugins/cache/translation_cacher.rb +0 -40
  96. data/lib/mobility/sequel.rb +0 -9
  97. data/lib/mobility/sequel/backend_resetter.rb +0 -23
  98. 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,38 @@ 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
+
19
+ initialize_hook do |*names|
20
+ include InstanceMethods
21
+
22
+ define_method :translated_attributes do
23
+ super().merge(names.inject({}) do |attributes, name|
24
+ attributes.merge(name.to_s => send(name))
25
+ end)
26
+ end
27
+ end
28
+
29
+ # Applies attribute_methods plugin for a given option value.
30
+ included_hook do
31
+ if options[:attribute_methods]
32
+ define_method :untranslated_attributes, ::ActiveRecord::Base.instance_method(:attributes)
24
33
  end
34
+ end
25
35
 
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)
36
+ module InstanceMethods
37
+ def translated_attributes
38
+ {}
39
+ end
40
+
41
+ def attributes
42
+ super.merge(translated_attributes)
37
43
  end
38
44
  end
39
45
  end
46
+
47
+ register_plugin(:attribute_methods, AttributeMethods)
40
48
  end
41
49
  end
@@ -0,0 +1,70 @@
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
+ klass.extend ClassMethods
36
+ @names.each { |name| klass.register_mobility_attribute(name) }
37
+ end
38
+
39
+ module ClassMethods
40
+ # Return true if attribute name is translated on this model.
41
+ # @param [String, Symbol] Attribute name
42
+ # @return [Boolean]
43
+ def mobility_attribute?(name)
44
+ mobility_attributes.include?(name.to_s)
45
+ end
46
+
47
+ # Register a new attribute name. Public, but treat as internal.
48
+ # @param [String, Symbol] Attribute name
49
+ def register_mobility_attribute(name)
50
+ (self.mobility_attributes << name.to_s).uniq!
51
+ end
52
+
53
+ def inherited(klass)
54
+ super
55
+ mobility_attributes.each { |name| klass.register_mobility_attribute(name) }
56
+ end
57
+
58
+ protected
59
+
60
+ # Return translated attribute names on this model.
61
+ # @return [Array<String>] Attribute names
62
+ def mobility_attributes
63
+ @mobility_attributes ||= []
64
+ end
65
+ end
66
+ end
67
+
68
+ register_plugin(:attributes, Attributes)
69
+ end
70
+ 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.each do |name|
60
+ klass.register_mobility_backend_class(name, @backend_class)
61
+ end
62
+
63
+ backend_class
64
+ end
65
+ end
66
+
67
+ # Include backend name in inspect string.
68
+ # @return [String]
69
+ def inspect
70
+ "#<Translations (#{backend}) @names=#{names.join(", ")}>"
71
+ end
72
+
73
+ def load_backend(backend)
74
+ Backends.load_backend(backend)
75
+ rescue Backends::LoadError => e
76
+ raise e, "could not find a #{backend} backend. Did you forget to include an ORM plugin like active_record or sequel?"
77
+ end
78
+
79
+ private
80
+
81
+ # Override to extract backend options from options hash.
82
+ def initialize_options(original_options)
83
+ super
84
+
85
+ case options[:backend]
86
+ when String, Symbol, Class
87
+ @backend, @backend_options = options[:backend], options
88
+ when Array
89
+ @backend, @backend_options = options[:backend]
90
+ @backend_options = @backend_options.merge(original_options)
91
+ when NilClass
92
+ @backend = @backend_options = nil
93
+ else
94
+ raise ArgumentError, "backend must be either a backend name, a backend class, or a two-element array"
95
+ end
96
+
97
+ @backend = load_backend(backend)
98
+ end
99
+
100
+ # Override default validation to exclude backend options, which may be
101
+ # mixed in with plugin options.
102
+ def validate_options(options)
103
+ return super unless backend
104
+ super(options.slice(*(options.keys - backend.valid_keys)))
105
+ end
106
+
107
+ # Override default argument-handling in DSL to store kwargs passed along
108
+ # with plugin name.
109
+ def self.configure_default(defaults, key, backend = nil, backend_options = {})
110
+ defaults[key] = [backend, backend_options] if backend
111
+ end
112
+
113
+ module InstanceMethods
114
+ # Return a new backend for an attribute name.
115
+ # @return [Hash] Hash of attribute names and backend instances
116
+ # @api private
117
+ def mobility_backends
118
+ @mobility_backends ||= ::Hash.new do |hash, name|
119
+ next hash[name.to_sym] if String === name
120
+ hash[name] = self.class.mobility_backend_class(name).new(self, name.to_s)
121
+ end
122
+ end
123
+
124
+ def initialize_dup(other)
125
+ @mobility_backends = nil
126
+ super
127
+ end
128
+ end
129
+
130
+ module ClassMethods
131
+ # Return backend class for a given attribute name.
132
+ # @param [Symbol,String] Name of attribute
133
+ # @return [Class] Backend class
134
+ def mobility_backend_class(name)
135
+ mobility_backend_classes.fetch(name.to_sym)
136
+ rescue KeyError
137
+ raise KeyError, "No backend for: #{name}"
138
+ end
139
+
140
+ def register_mobility_backend_class(name, backend_class)
141
+ mobility_backend_classes[name.to_sym] = backend_class
142
+ end
143
+
144
+ def inherited(klass)
145
+ klass.mobility_backend_classes.merge!(@mobility_backend_classes)
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