-
Notifications
You must be signed in to change notification settings - Fork 0
/
class_methods.rb
409 lines (338 loc) · 12.3 KB
/
class_methods.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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
require 'lazydoc'
require 'configurable/config_hash'
require 'configurable/conversions'
module Configurable
# Hash of default config types (bool, integer, float, string).
DEFAULT_CONFIG_TYPES = {
:bool => ConfigClasses::BooleanConfig,
:integer => ConfigClasses::IntegerConfig,
:float => ConfigClasses::FloatConfig,
:string => ConfigClasses::StringConfig,
:nest => ConfigClasses::Nest,
:obj => ConfigClasses::Config
}
# ClassMethods extends classes that include Configurable and provides methods
# for declaring configurations.
module ClassMethods
include ConfigClasses
include ConfigClasses
# A hash of (key, Config) pairs tracking configs defined on self. See the
# configs method for all configs declared across all ancestors.
attr_reader :config_registry
# A hash of (key, ConfigType) pairs tracking config_types defined on self.
# See the config_types method for all config_types declared across all
# ancestors.
attr_reader :config_type_registry
def self.initialize(base) # :nodoc:
base.reset_configs
unless base.instance_variable_defined?(:@config_registry)
base.instance_variable_set(:@config_registry, {})
end
base.reset_config_types
unless base.instance_variable_defined?(:@config_type_registry)
base.instance_variable_set(:@config_type_registry, {})
end
end
# A hash of (key, Config) pairs representing all configs defined on this
# class or inherited from ancestors. The configs hash is memoized for
# performance. Call reset_configs if configs needs to be recalculated for
# any reason.
#
# Configs is extended with the Conversions module.
def configs
@configs ||= begin
configs = {}
ancestors.reverse.each do |ancestor|
next unless ancestor.kind_of?(ClassMethods)
ancestor.config_registry.each_pair do |key, value|
if value.nil?
configs.delete(key)
else
configs[key] = value
end
end
end
configs.extend Conversions
configs
end
end
# Resets configs such that they will be recalculated.
def reset_configs
@configs = nil
end
# A hash of (key, ConfigType) pairs representing all config_types defined
# on this class or inherited from ancestors. The config_types hash is
# memoized for performance. Call reset_config_types if config_types needs
# to be recalculated for any reason.
def config_types
@config_types ||= begin
config_types = Configurable::DEFAULT_CONFIG_TYPES.dup
ancestors.reverse.each do |ancestor|
next unless ancestor.kind_of?(ClassMethods)
ancestor.config_type_registry.each_pair do |key, value|
if value.nil?
config_types.delete(key)
else
config_types[key] = value
end
end
end
config_types
end
end
# Resets config_types such that they will be recalculated.
def reset_config_types
@config_types = nil
end
protected
# Defines and registers an instance of config_class with the specified key
# and attrs. Unless attrs specifies a :reader or :writer, the
# corresponding attr accessors will be defined for the config name (which
# by default is the key).
def define_config(key, attrs={}, config_class=Config)
reader = attrs[:reader]
writer = attrs[:writer]
config = config_class.new(key, attrs)
attr_reader(config.name) unless reader
attr_writer(config.name) unless writer
config_registry[config.key] = config
reset_configs
config
end
# Defines a config after guessing or setting some standard values into
# attrs. Specifically:
#
# * :default is the default
# * :caster is the caster block (if provided)
# * :desc is set using Lazydoc (unless already set)
# * :list is set to true for array defaults (unless already set)
#
# In addition config also guesses the type of a config (if not manually
# specified by :type) and merges in any attributes for the corresponding
# config_type. The class of the config is guessed from the attrs, based
# on the :list and :options attributes using this logic:
#
# :list :otions config_class
# ---------------------------
# false false Config
# true false List
# false true Select
# true true ListSelect
#
# == Usage Note
#
# Config is meant to be a convenience method. It gets most things right
# but if the attrs logic is too convoluted (and at times it is) then
# define configs manually with the define_config method.
def config(key, default=nil, attrs={}, &block)
nest_class = guess_nest_class(default, block)
attrs[:default] = nest_class ? nest_class.new : default
attrs[:desc] = guess_config_desc(attrs, Lazydoc.register_caller)
attrs[:list] = default.kind_of?(Array) unless attrs.has_key?(:list)
config_class = guess_config_type(attrs)
config = define_config(key, attrs, config_class)
if attrs[:list]
config.extend List
end
if nest_class
const_name = attrs[:const_name] || guess_nest_const_name(config)
unless const_defined?(const_name)
const_set(const_name, nest_class)
end
end
config
end
# Removes a config much like remove_method removes a method. The reader
# and writer for the config are likewise removed. Nested configs can be
# removed using this method.
#
# Setting :reader or :writer to false in the options prevents those
# methods from being removed.
def remove_config(key, options={})
unless config_registry.has_key?(key)
raise NameError.new("#{key.inspect} is not a config on #{self}")
end
options = {
:reader => true,
:writer => true
}.merge(options)
config = config_registry.delete(key)
reset_configs
remove_method(config.reader) if options[:reader]
remove_method(config.writer) if options[:writer]
config
end
# Undefines a config much like undef_method undefines a method. The
# reader and writer for the config are likewise undefined. Nested configs
# can be undefined using this method.
#
# Setting :reader or :writer to false in the options prevents those
# methods from being undefined.
#
# ==== Implementation Note
#
# Configurations are undefined by setting the key to nil in the registry.
# Deleting the config is not sufficient because the registry needs to
# convey to self and subclasses to not inherit the config from ancestors.
#
# This is unlike remove_config where the config is simply deleted from the
# config_registry.
def undef_config(key, options={})
unless configs.has_key?(key)
raise NameError.new("#{key.inspect} is not a config on #{self}")
end
options = {
:reader => true,
:writer => true
}.merge(options)
config = configs[key]
config_registry[key] = nil
reset_configs
undef_method(config.reader) if options[:reader]
undef_method(config.writer) if options[:writer]
config
end
def define_config_type(name, config_type)
config_type_registry[name] = config_type
reset_config_types
config_type
end
def config_type(name, *matchers, &caster)
config_type = StringConfig.subclass(*matchers).cast(&caster)
const_name = guess_config_type_const_name(name)
unless const_defined?(const_name)
const_set(const_name, config_type)
end
define_config_type(name, config_type)
end
# Removes a config_type much like remove_method removes a method.
def remove_config_type(name)
unless config_type_registry.has_key?(name)
raise NameError.new("#{name.inspect} is not a config_type on #{self}")
end
config_type = config_type_registry.delete(name)
reset_config_types
config_type
end
# Undefines a config_type much like undef_method undefines a method.
#
# ==== Implementation Note
#
# ConfigClasses are undefined by setting the key to nil in the registry.
# Deleting the config_type is not sufficient because the registry needs to
# convey to self and subclasses to not inherit the config_type from
# ancestors.
#
# This is unlike remove_config_type where the config_type is simply
# deleted from the config_type_registry.
def undef_config_type(name)
unless config_types.has_key?(name)
raise NameError.new("#{name.inspect} is not a config_type on #{self}")
end
config_type = config_type_registry[name]
config_type_registry[name] = nil
reset_config_types
config_type
end
private
def inherited(base) # :nodoc:
ClassMethods.initialize(base)
super
end
def guess_nest_class(base, block) # :nodoc:
unless base.kind_of?(Hash) || block
return nil
end
nest_class = base.kind_of?(Class) ?
Class.new(base) :
Class.new { include Configurable }
if base.kind_of?(Hash)
base.each_pair do |key, value|
nest_class.send(:config, key, value)
end
end
if block
nest_class.class_eval(&block)
end
check_infinite_nest(nest_class)
nest_class
end
# helper to recursively check for an infinite nest
def check_infinite_nest(klass) # :nodoc:
raise "infinite nest detected" if klass == self
klass.configs.each_value do |config|
if config.kind_of?(Nest)
check_infinite_nest(config.configurable.class)
end
end
end
def each_registry # :nodoc:
ancestors.each do |ancestor|
case
when ancestor.kind_of?(ClassMethods)
yield ancestor, ancestor.config_type_registry
when ancestor == Configurable
yield ancestor, Configurable::DEFAULT_CONFIG_TYPES
break
else next
end
end
end
def guess_config_type_by_name(name) # :nodoc:
return name if name.nil?
each_registry do |ancestor, registry|
if registry.has_key?(name)
return registry[name]
end
end
raise "no such config type: #{type.inspect}"
end
def guess_config_type_by_value(value) # :nodoc:
each_registry do |ancestor, registry|
guesses = registry.values.select {|config_type| config_type.matches?(value) }
case guesses.length
when 0 then next
when 1 then return guesses.at(0)
else raise "multiple guesses for config type: #{guesses.inspect} (in: #{ancestor} default: #{default.inspect})"
end
end
Config
end
def guess_config_type(attrs) # :nodoc:
if attrs.has_key?(:type)
guess_config_type_by_name attrs[:type]
else
value = attrs[:default]
value = value.at(0) if value.kind_of?(Array)
guess_config_type_by_value value
end
end
def guess_config_desc(base_attrs, comment) # :nodoc:
Hash.new do |hash, key|
comment.resolve
if trailer = comment.trailer
flags, desc = trailer.split(':', 2)
flags, desc = '', flags unless desc
argv = flags.split(',').collect! {|arg| arg.strip }
argv << desc
comment_attrs = ConfigParser::Utils.parse_attrs(argv)
comment_attrs.each_pair do |attr_key, attr_value|
unless hash.has_key?(attr_key)
hash[attr_key] = attr_value
end
end
end
unless hash.has_key?(:help)
hash[:help] = comment.content
end
hash.has_key?(key) ? hash[key] : nil
end.merge!(base_attrs)
end
def guess_nest_const_name(config) # :nodoc:
config.name.gsub(/(?:^|_)(.)/) { $1.upcase }
end
def guess_config_type_const_name(name) # :nodoc:
"#{name}Type".gsub(/(?:^|_)(.)/) { $1.upcase }
end
end
end