configurable 0.1.0

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.
@@ -0,0 +1,52 @@
1
+ require 'config_parser/utils'
2
+
3
+ class ConfigParser
4
+ class Option
5
+
6
+ attr_reader :short
7
+ attr_reader :long
8
+ attr_reader :arg_name
9
+ attr_reader :desc
10
+ attr_reader :block
11
+
12
+ def initialize(options={}, &block)
13
+ @short = Utils.shortify(options[:short])
14
+ @long = Utils.longify(options[:long])
15
+ @arg_name = options[:arg_name]
16
+ @desc = options[:desc]
17
+ @block = block
18
+ end
19
+
20
+ # Returns an array of non-nil switches mapping to this option
21
+ # (ie [long, short]). May be overridden in subclasses.
22
+ def switches
23
+ [long, short].compact
24
+ end
25
+
26
+ # Selects the value or the shifts a value off of argv and sets
27
+ # that value in config.
28
+ #
29
+ # Parse is a hook for fancier ways of determining an option
30
+ # value and/or setting the value in config. Parse recieves
31
+ # the switch (ie long or short) mapping to self for subclasses
32
+ # that need it (ex the Switch class).
33
+ def parse(switch, value, argv)
34
+ if arg_name
35
+ unless value
36
+ raise "no value provided for: #{switch}" if argv.empty?
37
+ value = argv.shift
38
+ end
39
+ block ? block.call(value) : value
40
+ else
41
+ raise "value specified for flag" if value
42
+ block ? block.call : nil
43
+ end
44
+ end
45
+
46
+ def to_s
47
+ short_str = short ? short + ',' : ' '
48
+ desc_str = desc.kind_of?(Lazydoc::Comment) ? desc.trailer : desc
49
+ "%-37s%-43s" % [" #{short_str} #{long} #{arg_name}", desc_str]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ class ConfigParser
2
+ class Switch < Option
3
+ attr_reader :negative_long
4
+
5
+ def initialize(options={})
6
+ super
7
+ raise ArgumentError, "arg_name specified for switch: #{arg_name}" if arg_name
8
+ raise ArgumentError, "no long specified" unless long
9
+ @negative_long = Utils.longify("no-#{long[2,long.length-2]}")
10
+ end
11
+
12
+ def switches
13
+ [long, negative_long, short].compact
14
+ end
15
+
16
+ def parse(switch, value, argv)
17
+ raise "value specified for switch" if value
18
+ value = (switch == negative_long ? false : true)
19
+ block ? block.call(value) : value
20
+ end
21
+
22
+ def to_s
23
+ short_str = short ? short + ',' : ' '
24
+ long_str = long ? "--[no-]#{long[2,long.length-2]}" : ''
25
+ desc_str = desc.kind_of?(Lazydoc::Comment) ? desc.trailer : desc
26
+ "%-37s%-43s" % [" #{short_str} #{long_str}", desc_str]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,133 @@
1
+ class ConfigParser
2
+ module Utils
3
+ module_function
4
+
5
+ # The option break argument
6
+ OPTION_BREAK = "--"
7
+
8
+ # Matches a nested long option, with or without a value
9
+ # (ex: '--opt', '--nested:opt', '--opt=value'). After
10
+ # the match:
11
+ #
12
+ # $1:: the switch
13
+ # $3:: the value
14
+ #
15
+ LONG_OPTION = /^(--[A-z].*?)(=(.*))?$/
16
+
17
+ # Matches a nested short option, with or without a value
18
+ # (ex: '-o', '-n:o', '-o=value'). After the match:
19
+ #
20
+ # $1:: the switch
21
+ # $4:: the value
22
+ #
23
+ SHORT_OPTION = /^(-[A-z](:[A-z])*)(=(.*))?$/
24
+
25
+ # Matches the alternate syntax for short options
26
+ # (ex: '-n:ovalue', '-ovalue'). After the match:
27
+ #
28
+ # $1:: the switch
29
+ # $3:: the value
30
+ #
31
+ ALT_SHORT_OPTION = /^(-[A-z](:[A-z])*)(.+)$/
32
+
33
+ # Turns the input string into a short-format option. Raises
34
+ # an error if the option does not match SHORT_OPTION. Nils
35
+ # are returned directly.
36
+ #
37
+ # ConfigParser.shortify("-o") # => '-o'
38
+ # ConfigParser.shortify(:o) # => '-o'
39
+ #
40
+ def shortify(str)
41
+ return nil if str == nil
42
+
43
+ str = str.to_s
44
+ str = "-#{str}" unless str[0] == ?-
45
+ unless str =~ SHORT_OPTION && $3 == nil
46
+ raise ArgumentError, "invalid short option: #{str}"
47
+ end
48
+ str
49
+ end
50
+
51
+ # Turns the input string into a long-format option. Underscores
52
+ # are converted to hyphens. Raises an error if the option does
53
+ # not match LONG_OPTION. Nils are returned directly.
54
+ #
55
+ # ConfigParser.longify("--opt") # => '--opt'
56
+ # ConfigParser.longify(:opt) # => '--opt'
57
+ # ConfigParser.longify(:opt_ion) # => '--opt-ion'
58
+ #
59
+ def longify(str)
60
+ return nil if str == nil
61
+
62
+ str = str.to_s
63
+ str = "--#{str}" unless str =~ /^--/
64
+ str.gsub!(/_/, '-')
65
+ unless str =~ LONG_OPTION && $3 == nil
66
+ raise ArgumentError, "invalid long option: #{str}"
67
+ end
68
+ str
69
+ end
70
+
71
+ # Options:
72
+ #
73
+ # :long the long key ("--key")
74
+ # :arg_name the argument name ("KEY")
75
+ #
76
+ def setup_option(key, options={})
77
+ options[:long] ||= "--#{key}"
78
+ options[:long].to_s =~ /^(--)?(.*)$/
79
+ options[:arg_name] ||= $2.upcase
80
+
81
+ lambda {|value| config[key] = value }
82
+ end
83
+
84
+ # Options:
85
+ #
86
+ # :long the long key ("--key")
87
+ #
88
+ def setup_flag(key, default=true, options={})
89
+ options[:long] ||= "--#{key}"
90
+
91
+ lambda {config[key] = !default }
92
+ end
93
+
94
+ # Options:
95
+ #
96
+ # :long the long key ("--[no-]key")
97
+ #
98
+ def setup_switch(key, default=true, options={})
99
+ options[:long] ||= "--#{key}"
100
+ options[:long].to_s =~ /^(--)?(\[no-\])?(.*)$/
101
+ options[:long] = "--[no-]#{$3}" unless $2
102
+
103
+ lambda {|value| config[key] = (value ? !default : default) }
104
+ end
105
+
106
+ # Options:
107
+ #
108
+ # :long the long key ("--key")
109
+ # :arg_name the argument name ("KEY" or "A,B,C" for a comma split)
110
+ # :split the split character
111
+ #
112
+ def setup_list(key, options={})
113
+ options[:long] ||= "--#{key}"
114
+
115
+ if split = options[:split]
116
+ options[:arg_name] ||= %w{A B C}.join(split)
117
+ else
118
+ options[:long].to_s =~ /^(--)?(.*)$/
119
+ options[:arg_name] ||= $2.upcase
120
+ end
121
+
122
+ n = options[:n]
123
+
124
+ lambda do |value|
125
+ array = (config[key] ||= [])
126
+ array.concat(split ? value.split(split) : [value])
127
+ if n && array.length > n
128
+ raise "too many assignments: #{key.inspect}"
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,155 @@
1
+ require 'configurable/class_methods'
2
+
3
+ # Configurable enables the specification of configurations within a class definition.
4
+ #
5
+ # class ConfigClass
6
+ # include Configurable
7
+ #
8
+ # config :one, 'one'
9
+ # config :two, 'two'
10
+ # config :three, 'three'
11
+ #
12
+ # def initialize(overrides={})
13
+ # initialize_config(overrides)
14
+ # end
15
+ # end
16
+ #
17
+ # c = ConfigClass.new
18
+ # c.config.class # => Configurable::DelegateHash
19
+ # c.config # => {:one => 'one', :two => 'two', :three => 'three'}
20
+ #
21
+ # The <tt>config</tt> object acts as a forwarding hash; declared configurations
22
+ # map to accessors while undeclared configurations are stored internally:
23
+ #
24
+ # c.config[:one] = 'ONE'
25
+ # c.one # => 'ONE'
26
+ #
27
+ # c.one = 1
28
+ # c.config # => {:one => 1, :two => 'two', :three => 'three'}
29
+ #
30
+ # c.config[:undeclared] = 'value'
31
+ # c.config.store # => {:undeclared => 'value'}
32
+ #
33
+ # The writer for a configuration can be defined by providing a block to config.
34
+ # The Validation module provides a number of common validation/transform
35
+ # blocks which can be accessed through the class method 'c':
36
+ #
37
+ # class SubClass < ConfigClass
38
+ # config(:one, 'one') {|v| v.upcase }
39
+ # config :two, 2, &c.integer
40
+ # end
41
+ #
42
+ # s = SubClass.new
43
+ # s.config # => {:one => 'ONE', :two => 2, :three => 'three'}
44
+ #
45
+ # s.one = 'aNothER'
46
+ # s.one # => 'ANOTHER'
47
+ #
48
+ # s.two = -2
49
+ # s.two # => -2
50
+ # s.two = "3"
51
+ # s.two # => 3
52
+ # s.two = nil # !> ValidationError
53
+ # s.two = 'str' # !> ValidationError
54
+ #
55
+ # Configurations are inherited from the parent and may be overridden in
56
+ # subclasses.
57
+ #
58
+ # === Options
59
+ #
60
+ # Alternative reader and writer methods may be specified as options to config.
61
+ # When alternate methods are specified, Configurable assumes the methods are
62
+ # declared elsewhere and will not define accessors.
63
+ #
64
+ # class AlternativeClass
65
+ # include Configurable
66
+ #
67
+ # config_attr :sym, 'value', :reader => :get_sym, :writer => :set_sym
68
+ #
69
+ # def initialize
70
+ # initialize_config
71
+ # end
72
+ #
73
+ # def get_sym
74
+ # @sym
75
+ # end
76
+ #
77
+ # def set_sym(input)
78
+ # @sym = input.to_sym
79
+ # end
80
+ # end
81
+ #
82
+ # alt = AlternativeClass.new
83
+ # alt.respond_to?(:sym) # => false
84
+ # alt.respond_to?(:sym=) # => false
85
+ #
86
+ # alt.config[:sym] = 'one'
87
+ # alt.get_sym # => :one
88
+ #
89
+ # alt.set_sym('two')
90
+ # alt.config[:sym] # => :two
91
+ #
92
+ # Idiosyncratically, true, false, and nil may also be provided as
93
+ # reader/writer options.
94
+ #
95
+ # true Same as using the defaults, accessors are defined.
96
+ #
97
+ # false Sets the default reader/writer but does not define
98
+ # the accessors (think 'define reader/writer' => false).
99
+ #
100
+ # nil Does not define a reader/writer, and does not define
101
+ # the accessors. In effect this will define a config
102
+ # that does not map to the instance, but will be
103
+ # present in instance.config
104
+ #
105
+ module Configurable
106
+
107
+ # Extends including classes with Configurable::ClassMethods
108
+ def self.included(mod) # :nodoc:
109
+ mod.extend ClassMethods if mod.kind_of?(Class)
110
+ end
111
+
112
+ # A ConfigHash bound to self
113
+ attr_reader :config
114
+
115
+ # Reconfigures self with the given overrides. Only the specified configs
116
+ # are modified. Keys are symbolized.
117
+ #
118
+ # Returns self.
119
+ def reconfigure(overrides={})
120
+ overrides.each_pair do |key, value|
121
+ config[key] = value
122
+ end
123
+
124
+ self
125
+ end
126
+
127
+ # Reinitializes configurations in the copy such that
128
+ # the new object has it's own set of configurations,
129
+ # separate from the original object.
130
+ def initialize_copy(orig)
131
+ super
132
+ initialize_config(orig.config)
133
+ end
134
+
135
+ protected
136
+
137
+ # Initializes config. Default config values
138
+ # are overridden as specified by overrides.
139
+ def initialize_config(overrides={})
140
+ delegates = self.class.configurations
141
+
142
+ # note the defaults could be stored first and overridden
143
+ # by the overrides, but this is likely more efficient
144
+ # on average since delegates duplicate default values.
145
+ store = {}
146
+ overrides.each_pair do |key, value|
147
+ store[key] = value
148
+ end
149
+ delegates.each_pair do |key, delegate|
150
+ store[key] = delegate.default unless store.has_key?(key)
151
+ end
152
+
153
+ @config = DelegateHash.new(delegates, store).bind(self)
154
+ end
155
+ end
@@ -0,0 +1,308 @@
1
+ require 'lazydoc/attributes'
2
+ require 'configurable/delegate_hash'
3
+ require 'configurable/validation'
4
+ require 'configurable/indifferent_access'
5
+
6
+ autoload(:ConfigParser, 'config_parser')
7
+
8
+ module Configurable
9
+
10
+ # ClassMethods extends classes that include Configurable and
11
+ # provides methods for declaring configurations.
12
+ module ClassMethods
13
+ include Lazydoc::Attributes
14
+
15
+ # A hash holding the class configurations.
16
+ attr_reader :configurations
17
+
18
+ def self.extended(base) # :nodoc:
19
+ caller.each_with_index do |line, index|
20
+ case line
21
+ when /\/configurable.rb/ then next
22
+ when Lazydoc::CALLER_REGEXP
23
+ base.instance_variable_set(:@source_file, File.expand_path($1))
24
+ break
25
+ end
26
+ end
27
+
28
+ configurations = {}.extend IndifferentAccess
29
+ base.instance_variable_set(:@configurations, configurations)
30
+ end
31
+
32
+ def inherited(child) # :nodoc:
33
+ unless child.instance_variable_defined?(:@source_file)
34
+ caller.first =~ Lazydoc::CALLER_REGEXP
35
+ child.instance_variable_set(:@source_file, File.expand_path($1))
36
+ end
37
+
38
+ configurations = {}
39
+ configurations.extend IndifferentAccess if @configurations.kind_of?(IndifferentAccess)
40
+ @configurations.each_pair {|key, config| configurations[key] = config.dup }
41
+ child.instance_variable_set(:@configurations, configurations)
42
+ super
43
+ end
44
+
45
+ def parser
46
+ ConfigParser.new do |parser|
47
+ configurations.to_a.sort_by do |(key, config)|
48
+ config.attributes[:order] || 0
49
+ end.each do |(key, config)|
50
+ parser.define(key, config.default, config.attributes)
51
+ end
52
+ end
53
+ end
54
+
55
+ # Loads the contents of path as YAML. Returns an empty hash if the path
56
+ # is empty, does not exist, or is not a file.
57
+ def load_config(path)
58
+ # the last check prevents YAML from auto-loading itself for empty files
59
+ return {} if path == nil || !File.file?(path) || File.size(path) == 0
60
+ YAML.load_file(path) || {}
61
+ end
62
+
63
+ protected
64
+
65
+ def use_indifferent_access(value=true)
66
+ current = @configurations
67
+ @configurations = value ? HashWithIndifferentAccess.new : {}
68
+ current.each_pair do |key, value|
69
+ @configurations[key] = value
70
+ end
71
+ end
72
+
73
+ # Declares a class configuration and generates the associated accessors.
74
+ # If a block is given, the <tt>key=</tt> method will set <tt>@key</tt>
75
+ # to the return of the block, which executes in class-context.
76
+ #
77
+ # class SampleClass
78
+ # include Configurable
79
+ #
80
+ # config :str, 'value'
81
+ # config(:upcase, 'value') {|input| input.upcase }
82
+ # end
83
+ #
84
+ # # An equivalent class to illustrate class-context
85
+ # class EquivalentClass
86
+ # attr_accessor :str
87
+ # attr_reader :upcase
88
+ #
89
+ # UPCASE_BLOCK = lambda {|input| input.upcase }
90
+ #
91
+ # def upcase=(input)
92
+ # @upcase = UPCASE_BLOCK.call(input)
93
+ # end
94
+ # end
95
+ #
96
+ def config(key, value=nil, options={}, &block)
97
+ # register with Lazydoc
98
+ options[:desc] ||= Lazydoc.register_caller
99
+
100
+ if block_given?
101
+ options = default_options(block).merge!(options)
102
+
103
+ instance_variable = "@#{key}".to_sym
104
+ config_attr(key, value, options) do |input|
105
+ instance_variable_set(instance_variable, yield(input))
106
+ end
107
+ else
108
+ config_attr(key, value, options)
109
+ end
110
+ end
111
+
112
+ # Declares a class configuration and generates the associated accessors.
113
+ # If a block is given, the <tt>key=</tt> method will perform the block
114
+ # with instance-context.
115
+ #
116
+ # class SampleClass
117
+ # include Configurable
118
+ #
119
+ # def initialize
120
+ # initialize_config
121
+ # end
122
+ #
123
+ # config_attr :str, 'value'
124
+ # config_attr(:upcase, 'value') {|input| @upcase = input.upcase }
125
+ # end
126
+ #
127
+ # # An equivalent class to illustrate instance-context
128
+ # class EquivalentClass
129
+ # attr_accessor :str
130
+ # attr_reader :upcase
131
+ #
132
+ # def upcase=(input)
133
+ # @upcase = input.upcase
134
+ # end
135
+ # end
136
+ #
137
+ def config_attr(key, value=nil, options={}, &block)
138
+ options = default_options(block).merge!(options)
139
+
140
+ # define the default public reader method
141
+ reader = options.delete(:reader)
142
+
143
+ case reader
144
+ when true
145
+ reader = key
146
+ attr_reader(key)
147
+ public(key)
148
+ when false
149
+ reader = key
150
+ end
151
+
152
+ # define the default public writer method
153
+ writer = options.delete(:writer)
154
+
155
+ if block_given? && writer != true
156
+ raise ArgumentError, "a block may not be specified without writer == true"
157
+ end
158
+
159
+ case writer
160
+ when true
161
+ writer = "#{key}="
162
+ block_given? ? define_method(writer, &block) : attr_writer(key)
163
+ public writer
164
+ when false
165
+ writer = "#{key}="
166
+ end
167
+
168
+ # register with Lazydoc
169
+ options[:desc] ||= Lazydoc.register_caller
170
+
171
+ configurations[key] = Delegate.new(reader, writer, value, options)
172
+ end
173
+
174
+ # Adds a configuration to self accessing the configurations for the
175
+ # configurable class. Unlike config_attr and config, nest does not
176
+ # create accessors; the configurations must be accessed through
177
+ # the instance config method.
178
+ #
179
+ # class A
180
+ # include Configurable
181
+ # config :key, 'value'
182
+ #
183
+ # def initialize(overrides={})
184
+ # initialize_config(overrides)
185
+ # end
186
+ # end
187
+ #
188
+ # class B
189
+ # include Configurable
190
+ # nest :a, A
191
+ #
192
+ # def initialize(overrides={})
193
+ # initialize_config(overrides)
194
+ # end
195
+ # end
196
+ #
197
+ # b = B.new
198
+ # b.config[:a] # => {:key => 'value'}
199
+ #
200
+ # Nest may be provided a block which receives the first value for
201
+ # the nested config and is expected to initialize an instance of
202
+ # configurable_class. In this case a reader for the instance is
203
+ # created and access becomes quite natural.
204
+ #
205
+ # class C
206
+ # include Configurable
207
+ # nest(:a, A) {|overrides| A.new(overrides) }
208
+ #
209
+ # def initialize(overrides={})
210
+ # initialize_config(overrides)
211
+ # end
212
+ # end
213
+ #
214
+ # c = C.new
215
+ # c.a.key # => "value"
216
+ #
217
+ # c.a.key = "one"
218
+ # c.config[:a].to_hash # => {:key => 'one'}
219
+ #
220
+ # c.config[:a][:key] = 'two'
221
+ # c.a.key # => "two"
222
+ #
223
+ # c.config[:a] = {:key => 'three'}
224
+ # c.a.key # => "three"
225
+ #
226
+ # Nesting with an initialization block creates private methods
227
+ # that config[:a] uses to read and write the instance configurations;
228
+ # these methods are "#{key}_config" and "#{key}_config=" by default,
229
+ # but they may be renamed using the :reader and :writer options.
230
+ #
231
+ # Nest checks for recursive nesting and raises an error if
232
+ # a recursive nest is detected.
233
+ #
234
+ def nest(key, configurable_class, options={})
235
+ unless configurable_class.kind_of?(Configurable::ClassMethods)
236
+ raise ArgumentError, "not a Configurable class: #{configurable_class}"
237
+ end
238
+
239
+ reader = options.delete(:reader)
240
+ writer = options.delete(:writer)
241
+
242
+ if block_given?
243
+ # define instance accessor methods
244
+ instance_var = "@#{key}".to_sym
245
+ reader = "#{key}_config" unless reader
246
+ writer = "#{key}_config=" unless writer
247
+
248
+ # the public accessor
249
+ attr_reader key
250
+ public(key)
251
+
252
+ # the reader returns the config for the instance
253
+ define_method(reader) do
254
+ instance_variable_get(instance_var).config
255
+ end
256
+
257
+ # the writer initializes the instance if necessary,
258
+ # or reconfigures the instance if it already exists
259
+ define_method(writer) do |value|
260
+ if instance_variable_defined?(instance_var)
261
+ instance_variable_get(instance_var).reconfigure(value)
262
+ else
263
+ instance_variable_set(instance_var, yield(value))
264
+ end
265
+ end
266
+ private(reader, writer)
267
+ else
268
+ reader = writer = nil
269
+ end
270
+
271
+ # register with Lazydoc
272
+ options[:desc] ||= Lazydoc.register_caller
273
+
274
+ value = DelegateHash.new(configurable_class.configurations).update
275
+ configurations[key] = Delegate.new(reader, writer, value, options)
276
+
277
+ check_infinite_nest(configurable_class.configurations)
278
+ end
279
+
280
+ # Alias for Validation
281
+ def c
282
+ Validation
283
+ end
284
+
285
+ private
286
+
287
+ def default_options(block)
288
+ Validation::ATTRIBUTES[block].merge(
289
+ :reader => true,
290
+ :writer => true,
291
+ :order => configurations.length)
292
+ end
293
+
294
+ # helper to recursively check a set of
295
+ # configurations for an infinite nest
296
+ def check_infinite_nest(configurations) # :nodoc:
297
+ raise "infinite nest detected" if configurations == self.configurations
298
+
299
+ configurations.each_pair do |key, config|
300
+ config_hash = config.default(false)
301
+
302
+ if config_hash.kind_of?(DelegateHash)
303
+ check_infinite_nest(config_hash.delegates)
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end