-
Notifications
You must be signed in to change notification settings - Fork 82
/
attributes.rb
219 lines (174 loc) · 7.54 KB
/
attributes.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
require "mobility/util"
module Mobility
=begin
Defines accessor methods to include on model class. Inspired by Traco's
+Traco::Attributes+ class.
Normally this class will be created through class methods defined using
{Mobility::Translates} accessor methods, and need not be created directly.
However, the class is central to how Mobility hooks into models to add
accessors and other methods, and should be useful as a reference when
understanding and designing backends.
==Including Attributes in a Class
Since {Attributes} is a subclass of +Module+, including an instance of it is
like including a module. Creating an instance like this:
Attributes.new(:accessor, ["title"], backend: :my_backend, locale_accessors: [:en, :ja], cache: true, fallbacks: true)
will generate an anonymous module that behaves like this:
Module.new do
def title_backend
# Create a subclass of Mobility::Backends::MyBackend and include in it:
# - Mobility::Plugins::Cache (from the +cache: true+ option)
# - Mobility::Plugins::Fallbacks (from the +fallbacks: true+ option)
# - Mobility::Plugins::Presence (by default, disabled by +presence: false+)
# Then instantiate the backend, memoize it, and return it.
end
def title(**options)
title_backend.read(Mobility.locale, **options).presence
end
def title?(**options)
title_backend.read(Mobility.locale, **options).present?
end
def title=(value)
title_backend.write(Mobility.locale, value.presence)
end
# Start Locale Accessors
#
def title_en(**options)
title_backend.read(:en, **options).presence
end
def title_en?(**options)
title_backend.read(:en, **options).present?
end
def title_en=(value)
title_backend.write(:en, value.presence)
end
def title_ja(**options)
title_backend.read(:ja, **options).presence
end
def title_ja?(**options)
title_backend.read(:ja, **options).present?
end
def title_ja=(value)
title_backend.write(:ja, value.presence)
end
# End Locale Accessors
end
Including this module into a model class will thus add the backend method, the
reader, writer and presence methods, and the locale accessor so the model
class. (These methods are in fact added to the model in an +included+ hook.)
==Setting up the Model Class
Accessor methods alone are of limited use without a hook to actually modify the
model class. This hook is provided by the {Backend::Setup#setup_model} method,
which is added to every backend class when it includes the {Backend} module.
Assuming the backend has defined a setup block by calling +setup+, this block
will be called when {Attributes} is {#included} in the model class, passed
attributes and options defined when the backend was defined on the model class.
This allows a backend to do things like (for example) define associations on a
model class required by the backend, as happens in the {Backends::KeyValue} and
{Backends::Table} backends.
The +setup+ block is also used to extend the query scope/dataset (+i18n+ by
default) with backend-specific query method support.
Since setup blocks are evaluated on the model class, it is possible that
backends can conflict (for example, overwriting previously defined methods).
Care should be taken to avoid defining methods on the model class, or where
necessary, ensure that names are defined in such a way as to avoid conflicts
with other backends.
=end
class Attributes < Module
# Method (accessor, reader or writer)
# @return [Symbol] method
attr_reader :method
# Attribute names for which accessors will be defined
# @return [Array<String>] Array of names
attr_reader :names
# Backend options
# @return [Hash] Backend options
attr_reader :options
# Backend class
# @return [Class] Backend class
attr_reader :backend_class
# Name of backend
# @return [Symbol,Class] Name of backend, or backend class
attr_reader :backend_name
# Model class
# @return [Class] Class of model
attr_reader :model_class
# @param [Symbol] method One of: [reader, writer, accessor]
# @param [Array<String>] attribute_names Names of attributes to define backend for
# @param [Hash] backend_options Backend options hash
# @option backend_options [Class] model_class Class of model
# @raise [ArgumentError] if method is not reader, writer or accessor
def initialize(method, *attribute_names, backend: Mobility.default_backend, **backend_options)
raise ArgumentError, "method must be one of: reader, writer, accessor" unless %i[reader writer accessor].include?(method)
@method = method
@options = Mobility.default_options.merge(backend_options)
@names = attribute_names.map(&:to_s)
raise Mobility::BackendRequired, "Backend option required if Mobility.config.default_backend is not set." if backend.nil?
@backend_name = backend
end
# Setup backend class, include modules into model class, add this
# attributes module to shared {Mobility::Wrapper} and setup model with
# backend setup block (see {Mobility::Backend::Setup#setup_model}).
# @param klass [Class] Class of model
def included(klass)
@model_class = @options[:model_class] = klass
@backend_class = Class.new(get_backend_class(backend_name).for(model_class))
@backend_class.configure(options) if @backend_class.respond_to?(:configure)
Mobility.plugins.each do |name|
plugin = get_plugin_class(name)
plugin.apply(self, options[name])
end
names.each do |name|
define_backend(name)
define_reader(name) if %i[accessor reader].include?(method)
define_writer(name) if %i[accessor writer].include?(method)
end
model_class.mobility << self
backend_class.setup_model(model_class, names, options)
end
# Yield each attribute name to block
# @yieldparam [String] Attribute
def each &block
names.each(&block)
end
private
def define_backend(attribute)
backend_class_, options_ = backend_class, options
define_method Backend.method_name(attribute) do
@mobility_backends ||= {}
@mobility_backends[attribute] ||= backend_class_.new(self, attribute, options_)
end
end
def define_reader(attribute)
define_method attribute do |locale: Mobility.locale, **options|
return super() if options.delete(:super)
Mobility.enforce_available_locales!(locale)
mobility_backend_for(attribute).read(locale.to_sym, options)
end
define_method "#{attribute}?" do |locale: Mobility.locale, **options|
return super() if options.delete(:super)
Mobility.enforce_available_locales!(locale)
mobility_backend_for(attribute).present?(locale.to_sym, options)
end
end
def define_writer(attribute)
define_method "#{attribute}=" do |value, locale: Mobility.locale, **options|
return super(value) if options.delete(:super)
Mobility.enforce_available_locales!(locale)
mobility_backend_for(attribute).write(locale.to_sym, value, options)
end
end
def get_backend_class(backend)
return backend if Module === backend
require "mobility/backends/#{backend}"
get_class_from_key(Mobility::Backends, backend)
end
def get_plugin_class(plugin)
require "mobility/plugins/#{plugin}"
get_class_from_key(Mobility::Plugins, plugin)
end
def get_class_from_key(parent_class, key)
klass_name = key.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
parent_class.const_get(klass_name)
end
end
end