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.
data/MIT-LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008, Regents of the University of Colorado.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
4
+ software and associated documentation files (the "Software"), to deal in the Software
5
+ without restriction, including without limitation the rights to use, copy, modify, merge,
6
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
7
+ to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or
10
+ substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
16
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,237 @@
1
+ = Configurable[http://tap.rubyforge.org/configurable]
2
+
3
+ Class configurations that map to the command line. Configurable is used by the
4
+ Tap[http://tap.rubyforge.org] framework.
5
+
6
+ == Description
7
+
8
+ Configurable allows the declaration of inheritable, class-based configurations
9
+ that map to methods but may be accessed like a hash; a setup that is both fast
10
+ and convenient. Configurable facilitates the use of configuration files, and
11
+ parsing of configurations from the command line.
12
+
13
+ Check out these links for development, and bug tracking.
14
+
15
+ * Website[http://tap.rubyforge.org/configurable]
16
+ * Github[http://github.com/bahuvrihi/configurable/tree/master]
17
+ * Lighthouse[]
18
+ * {Google Group}[http://groups.google.com/group/ruby-on-tap]
19
+
20
+ == Usage
21
+
22
+ === Quickstart
23
+
24
+ class ConfigClass
25
+ include Configurable
26
+
27
+ config :key, 'default', :short => 'k' # a simple config with short
28
+ config :flag, false, &c.flag # a flag config
29
+ config :switch, false, &c.switch # a --[no-]switch config
30
+ config :num, 10, &c.integer # integer only
31
+ config :range, 1..10, &c.range # range only
32
+ config :upcase, 'default' do |value| # custom transformation
33
+ value.upcase
34
+ end
35
+
36
+ def initialize(overrides={})
37
+ initialize_config(overrides)
38
+ end
39
+ end
40
+
41
+ Configurations are present and documented in the class ConfigParser, the
42
+ Configurable equivalent of OptionParser:
43
+
44
+ parser = ConfigClass.parser
45
+ parser.class # => ConfigParser
46
+ "\n" + parser.to_s
47
+ # => %Q{
48
+ # -k, --key KEY a simple config with short
49
+ # --flag a flag config
50
+ # --[no-]switch a --[no-]switch config
51
+ # --num NUM integer only
52
+ # --range RANGE range only
53
+ # --upcase UPCASE custom transformation
54
+ # }
55
+
56
+ Command line arguments parse as expected:
57
+
58
+ parser.parse "one two --key=value --flag --no-switch --num 8 --range a..z three"
59
+ # => ['one', 'two', 'three']
60
+
61
+ parser.config
62
+ # => {
63
+ # :key => 'value',
64
+ # :flag => true,
65
+ # :switch => false,
66
+ # :num => '8',
67
+ # :range => 'a..z',
68
+ # :upcase => 'default'
69
+ # }
70
+
71
+ Validations/transformations occur upon initialization:
72
+
73
+ c = ConfigClass.new(parser.config)
74
+ c.config.to_hash
75
+ # => {
76
+ # :key => 'value',
77
+ # :flag => true,
78
+ # :switch => false,
79
+ # :num => 8,
80
+ # :range => 'a'..'z',
81
+ # :upcase => 'DEFAULT'
82
+ # }
83
+
84
+ Configurations have accessors, and are accessible through config.
85
+
86
+ c.upcase # => 'DEFAULT'
87
+
88
+ c.config[:upcase] = 'neW valuE'
89
+ c.upcase # => 'NEW VALUE'
90
+
91
+ c.upcase = 'fiNal Value'
92
+ c.config[:upcase] # => 'FINAL VALUE'
93
+
94
+ Note that configurations are validated every time they are set:
95
+
96
+ c.num = 'blue' # !> ValidationError
97
+
98
+ By default config treats strings and symbols as the same, so YAML config files
99
+ are easily created and used.
100
+
101
+ yaml_str = %Q{
102
+ key: a new value
103
+ flag: false
104
+ range: 1..100
105
+ }
106
+
107
+ c.reconfigure(YAML.load(yaml_str))
108
+ c.config.to_hash
109
+ # => {
110
+ # :key => 'a new value',
111
+ # :flag => false,
112
+ # :switch => false,
113
+ # :num => 8,
114
+ # :range => 1..100,
115
+ # :upcase => 'FINAL VALUE'
116
+ # }
117
+
118
+ === Declarations
119
+
120
+ Configurations are added to classes via declarations. Declarations are a lot
121
+ like specifying an attribute reader, writer, and the initialization code.
122
+
123
+ class ConfigClass
124
+ include Configurable
125
+
126
+ config :key, 'value' do |input|
127
+ input.upcase
128
+ end
129
+
130
+ def initialize
131
+ initialize_config
132
+ end
133
+ end
134
+
135
+ Is basically the same as:
136
+
137
+ class RegularClass
138
+ attr_reader :key
139
+
140
+ def key=(input)
141
+ @key = input.upcase
142
+ end
143
+
144
+ def initialize
145
+ self.key = 'value'
146
+ end
147
+ end
148
+
149
+ As far as the reader/writer goes, the analogy is quite good. The writer
150
+ method is defined so it sets the instance variable using the return of the
151
+ block. To literally define the writer with the block, use config_attr.
152
+
153
+ class ConfigAttrClass
154
+ include Configurable
155
+
156
+ config_attr :key, 'value' do |input|
157
+ @key = input.upcase
158
+ end
159
+ end
160
+
161
+ Literally defines methods:
162
+
163
+ class RegularClass
164
+ attr_reader :key
165
+
166
+ def key=(input)
167
+ @key = input.upcase
168
+ end
169
+ end
170
+
171
+ === Validation
172
+
173
+ When configurations are parsed from the command line, the config writers will
174
+ inevitably receive a string (even though the code may want a different object).
175
+ The {Validation}[link:classes/Configurable/Validation.html] module provides
176
+ standard blocks for validating and transforming string inputs and is accessible
177
+ in classes via the <tt>c</tt> method (ex: <tt>c.integer</tt> or
178
+ <tt>c.regexp</tt>). These blocks (generally) load string inputs as YAML and
179
+ validate that the result is the correct class; non-string inputs are simply
180
+ validated.
181
+
182
+ class ValidatingClass
183
+ include Configurable
184
+
185
+ config :int, 1, &c.integer # assures the input is an integer
186
+ config :int_or_nil, 1, &c.integer_or_nil # integer or nil only
187
+ config :array, [], &c.array # you get the idea
188
+ end
189
+
190
+ vc = ValidatingClass.new
191
+
192
+ vc.array = [:a, :b, :c]
193
+ vc.array # => [:a, :b, :c]
194
+
195
+ vc.array = "[1, 2, 3]"
196
+ vc.array # => [1, 2, 3]
197
+
198
+ vc.array = "string" # !> ValidationError
199
+
200
+ Validation blocks sometimes imply metadata. For instance <tt>c.flag</tt> causes
201
+ the config to appear as a flag on the command line. Metadata can be manually
202
+ specified in the options:
203
+
204
+ class ManualMetadata
205
+ include Configurable
206
+
207
+ config :key, 'default', :type => :flag do
208
+ # this block is only called if --key
209
+ # is specified, and will not take a
210
+ # value
211
+ end
212
+ end
213
+
214
+ === Documentation
215
+
216
+ Documentation on the command line is pulled from the code directly using
217
+ Lazydoc[http://tap.rubyforge.org/lazydoc/]. Documentation is a kind of
218
+ metadata for configurations, and may be specified manually as an option:
219
+
220
+ class ManualDocumentation
221
+ include Configurable
222
+ config :key, 'default', :desc => 'this is the command line description'
223
+ end
224
+
225
+ == Installation
226
+
227
+ Configurable is available as a gem on RubyForge[http://rubyforge.org/projects/tap]. Use:
228
+
229
+ % gem install configurable
230
+
231
+ == Info
232
+
233
+ Copyright (c) 2008, Regents of the University of Colorado.
234
+ Developer:: {Simon Chiang}[http://bahuvrihi.wordpress.com], {Biomolecular Structure Program}[http://biomol.uchsc.edu/], {Hansen Lab}[http://hsc-proteomics.uchsc.edu/hansenlab/]
235
+ Support:: CU Denver School of Medicine Deans Academic Enrichment Fund
236
+ Licence:: {MIT-Style}[link:files/MIT-LICENSE.html]
237
+
@@ -0,0 +1,216 @@
1
+ require 'config_parser/option'
2
+ require 'config_parser/switch'
3
+
4
+ autoload(:Shellwords, 'shellwords')
5
+
6
+ class ConfigParser
7
+ class << self
8
+ # Splits and nests compound keys of a hash.
9
+ #
10
+ # ConfigParser.nest('key' => 1, 'compound:key' => 2)
11
+ # # => {
12
+ # # 'key' => 1,
13
+ # # 'compound' => {'key' => 2}
14
+ # # }
15
+ #
16
+ # Nest does not do any consistency checking, so be aware that results will
17
+ # be ambiguous for overlapping compound keys.
18
+ #
19
+ # ConfigParser.nest('key' => {}, 'key:overlap' => 'value')
20
+ # # =? {'key' => {}}
21
+ # # =? {'key' => {'overlap' => 'value'}}
22
+ #
23
+ def nest(hash, split_char=":")
24
+ result = {}
25
+ hash.each_pair do |compound_key, value|
26
+ if compound_key.kind_of?(String)
27
+ keys = compound_key.split(split_char)
28
+
29
+ unless keys.length == 1
30
+ nested_key = keys.pop
31
+ nested_hash = keys.inject(result) {|target, key| target[key] ||= {}}
32
+ nested_hash[nested_key] = value
33
+ next
34
+ end
35
+ end
36
+
37
+ result[compound_key] = value
38
+ end
39
+
40
+ result
41
+ end
42
+ end
43
+
44
+ include Utils
45
+
46
+ # A hash of (switch, Option) pairs mapping switches to
47
+ # options.
48
+ attr_reader :switches
49
+
50
+ attr_reader :config
51
+
52
+ attr_reader :default_config
53
+
54
+ def initialize
55
+ @options = []
56
+ @switches = {}
57
+ @config = {}
58
+ @default_config = {}
59
+
60
+ yield(self) if block_given?
61
+ end
62
+
63
+ # Returns an array of options registered with self.
64
+ def options
65
+ @options.select do |opt|
66
+ opt.kind_of?(Option)
67
+ end
68
+ end
69
+
70
+ # Adds a separator string to self.
71
+ def separator(str)
72
+ @options << str
73
+ end
74
+
75
+ # Registers the option with self by adding opt to options and mapping
76
+ # the opt switches. Raises an error for conflicting keys and switches.
77
+ def register(opt)
78
+ @options << opt unless @options.include?(opt)
79
+
80
+ opt.switches.each do |switch|
81
+ case @switches[switch]
82
+ when opt then next
83
+ when nil then @switches[switch] = opt
84
+ else raise ArgumentError, "switch is already mapped to a different option: #{switch}"
85
+ end
86
+ end
87
+
88
+ opt
89
+ end
90
+
91
+ # Defines and registers a config with self.
92
+ def define(key, default_value=nil, options={})
93
+ # check for conflicts and register
94
+ if default_config.has_key?(key)
95
+ raise ArgumentError, "already set by a different option: #{key.inspect}"
96
+ end
97
+ default_config[key] = default_value
98
+
99
+ block = case options[:type]
100
+ when :switch then setup_switch(key, default_value, options)
101
+ when :flag then setup_flag(key, default_value, options)
102
+ when :list then setup_list(key, options)
103
+ when nil then setup_option(key, options)
104
+ when respond_to?("setup_#{options[:type]}")
105
+ send("setup_#{options[:type]}", key, default_value, options)
106
+ else
107
+ raise ArgumentError, "unsupported type: #{options[:type]}"
108
+ end
109
+
110
+ on(options, &block)
111
+ end
112
+
113
+ def on(*args, &block)
114
+ options = args.last.kind_of?(Hash) ? args.pop : {}
115
+ args.each do |arg|
116
+ # split switch arguments... descriptions
117
+ # still won't match as a switch even
118
+ # after a split
119
+ switch, arg_name = arg.split(' ', 2)
120
+
121
+ # determine the kind of argument specified
122
+ key = case switch
123
+ when SHORT_OPTION then :short
124
+ when LONG_OPTION then :long
125
+ else :desc
126
+ end
127
+
128
+ # check for conflicts
129
+ if options[key]
130
+ raise ArgumentError, "conflicting #{key} options: [#{options[key]}, #{arg}]"
131
+ end
132
+
133
+ # set the option
134
+ case key
135
+ when :long, :short
136
+ options[key] = switch
137
+ options[:arg_name] = arg_name.strip if arg_name
138
+ else
139
+ options[key] = arg.strip
140
+ end
141
+ end
142
+
143
+ # check if the option is specifying a Switch
144
+ klass = case
145
+ when options[:long].to_s =~ /^--\[no-\](.*)$/
146
+ options[:long] = "--#{$1}"
147
+ Switch
148
+ else
149
+ Option
150
+ end
151
+
152
+ # instantiate and register the new option
153
+ register klass.new(options, &block)
154
+ end
155
+
156
+ # Parse is non-destructive to argv. If a string argv is provided, parse
157
+ # splits it into an array using Shellwords.
158
+ #
159
+ def parse(argv=ARGV, config={})
160
+ @config = config
161
+ argv = argv.kind_of?(String) ? Shellwords.shellwords(argv) : argv.dup
162
+ args = []
163
+
164
+ while !argv.empty?
165
+ arg = argv.shift
166
+
167
+ # determine if the arg is an option
168
+ unless arg.kind_of?(String) && arg[0] == ?-
169
+ args << arg
170
+ next
171
+ end
172
+
173
+ # add the remaining args and break
174
+ # for the option break
175
+ if arg == OPTION_BREAK
176
+ args.concat(argv)
177
+ break
178
+ end
179
+
180
+ # split the arg...
181
+ # switch= $1
182
+ # value = $4 || $3 (if arg matches SHORT_OPTION, value is $4 or $3 otherwise)
183
+ arg =~ LONG_OPTION || arg =~ SHORT_OPTION || arg =~ ALT_SHORT_OPTION
184
+
185
+ # lookup the option
186
+ unless option = @switches[$1]
187
+ raise "unknown option: #{$1}"
188
+ end
189
+
190
+ option.parse($1, $4 || $3, argv)
191
+ end
192
+
193
+ default_config.each_pair do |key, default|
194
+ config[key] = default unless config.has_key?(key)
195
+ end
196
+
197
+ args
198
+ end
199
+
200
+ # Same as parse, but removes parsed args from argv.
201
+ def parse!(argv=ARGV, config={})
202
+ argv.replace(parse(argv, config))
203
+ end
204
+
205
+ def to_s
206
+ comments = @options.collect do |option|
207
+ next unless option.respond_to?(:desc)
208
+ option.desc.kind_of?(Lazydoc::Comment) ? option.desc : nil
209
+ end.compact
210
+ Lazydoc.resolve_comments(comments)
211
+
212
+ @options.collect do |option|
213
+ option.to_s.rstrip
214
+ end.join("\n") + "\n"
215
+ end
216
+ end