configurable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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