filigree 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,411 @@
1
+ # Author: Chris Wailes <chris.wailes@gmail.com>
2
+ # Project: Filigree
3
+ # Date: 2013/05/14
4
+ # Description: Easy application configuration.
5
+
6
+ ############
7
+ # Requires #
8
+ ############
9
+
10
+ # Standard Library
11
+
12
+ # Filigree
13
+ require 'filigree/class_methods_module'
14
+ require 'filigree/string'
15
+
16
+ #######################
17
+ # Classes and Modules #
18
+ #######################
19
+
20
+ module Filigree
21
+ module Configuration
22
+ include ClassMethodsModule
23
+
24
+ #############
25
+ # Constants #
26
+ #############
27
+
28
+ ####################
29
+ # Instance Methods #
30
+ ####################
31
+
32
+ # @return [Array<String>] Remaining strings that weren't used in configuration
33
+ attr_accessor :rest
34
+
35
+ # Dump the state of the Configuration object. This will dump the
36
+ # state, encoded in YAML, to different destinations depending on the
37
+ # io parameter.
38
+ #
39
+ # @overload dump(io, *fields)
40
+ # Dump the state to stdout.
41
+ # @param [nil] io Tells the method to serialize to stdout
42
+ # @param [Symbol] fields Fields to serialize
43
+ #
44
+ # @overload dump(str, *fields)
45
+ # Dump the state to a file.
46
+ # @param [String] io Name of file to serialize to
47
+ # @param [Symbol] fields Fields to serialize
48
+ #
49
+ # @overload dump(io, *fields)
50
+ # Dump the state to the provided IO instance.
51
+ # @param [IO] io IO object to serialize to
52
+ # @param [Symbol] fields Fields to serialize
53
+ #
54
+ # @return [void]
55
+ def dump(io = nil, *fields)
56
+ require 'yaml'
57
+
58
+ vals =
59
+ if fields.empty? then self.class.options_long.keys else fields end.inject(Hash.new) do |hash, field|
60
+ hash.tap { hash[field.to_s] = self.send(field) }
61
+ end
62
+
63
+ case io
64
+ when nil
65
+ YAML.dump vals
66
+
67
+ when String
68
+ File.open(io, 'w') { |file| YAML.dump vals, file }
69
+
70
+ when IO
71
+ YAML.dump vals, io
72
+ end
73
+ end
74
+ alias :serialize :dump
75
+
76
+ # Configures the object based on the overloaded parameter.
77
+ #
78
+ # @overload initialize(args)
79
+ # Configure the object from an array of strings.
80
+ # @param [Array<String>] args String arguments
81
+ #
82
+ # @overload initialize(source)
83
+ # Configure the object from a serialized source. If source is a
84
+ # string then it will be treated as a file name and the
85
+ # configuration will be loaded from the specified string. If it
86
+ # an IO object then that will be used as the source.
87
+ # @param [String, IO] source Serialized configuration source
88
+ #
89
+ # @return [void]
90
+ def initialize(overloaded = ARGV.clone)
91
+ set_opts = Array.new
92
+
93
+ case overloaded
94
+ when Array
95
+ handle_array_options(overloaded, set_opts)
96
+
97
+ when String, IO
98
+ handle_serialized_options(overloaded, set_opts)
99
+ end
100
+
101
+ (self.class.options_long.keys - set_opts).each do |opt_name|
102
+ default = self.class.options_long[opt_name].default
103
+ default = self.instance_exec(&default) if default.is_a? Proc
104
+
105
+ self.send("#{opt_name}=", default)
106
+ end
107
+
108
+ # Check to make sure all the required options are set.
109
+ self.class.required_options.each do |option|
110
+ raise ArgumentError, "Option #{option} not set." if self.send(option).nil?
111
+ end
112
+ end
113
+
114
+ # Find the appropriate option object given a string.
115
+ #
116
+ # @param [String] str Search string
117
+ #
118
+ # @return [Option, nil] Desired option or nil if it wasn't found
119
+ def find_option(str)
120
+ if str[0,2] == '--'
121
+ self.class.options_long[str[2..-1]]
122
+
123
+ elsif str[0,1] == '-'
124
+ self.class.options_short[str[1..-1]]
125
+ end
126
+ end
127
+
128
+ # Configure the object from an array of strings.
129
+ #
130
+ # @param [Array<String>] argv String options
131
+ # @param [Array<String>] set_opts List of names of options already added
132
+ #
133
+ # @return [void]
134
+ def handle_array_options(argv, set_opts)
135
+ while str = argv.shift
136
+
137
+ break if str == '--'
138
+
139
+ if option = find_option(str)
140
+ args = argv.shift(option.arity == -1 ? argv.index { |str| str[0,1] == '-' } : option.arity)
141
+
142
+ case option.handler
143
+ when Array
144
+ tmp = args.zip(option.handler).map { |arg, sym| arg.send sym }
145
+ self.send("#{option.long}=", (option.arity == 1 and tmp.length == 1) ? tmp.first : tmp)
146
+
147
+ when Proc
148
+ self.send("#{option.long}=", self.instance_exec(*args, &option.handler))
149
+ end
150
+
151
+ set_opts << option.long
152
+ end
153
+ end
154
+
155
+ # Save the rest of the command line for later.
156
+ self.rest = argv
157
+ end
158
+
159
+ # Configure the object from a serialization source.
160
+ #
161
+ # @param [String, IO] overloaded Serialization source
162
+ # @param [Array<String>] set_opts List of names of options already added
163
+ #
164
+ # @return [void]
165
+ def handle_serialized_options(overloaded, set_opts)
166
+ options =
167
+ if overloaded.is_a? String
168
+ if File.exists? overloaded
169
+ YAML.load_file overloaded
170
+ else
171
+ YAML.load overloaded
172
+ end
173
+ else
174
+ YAML.load overloaded
175
+ end
176
+
177
+ options.each do |option, val|
178
+ set_opts << option
179
+ self.send "#{option}=", val
180
+ end
181
+ end
182
+
183
+ #################
184
+ # Class Methods #
185
+ #################
186
+
187
+ module ClassMethods
188
+ # @return [Hash<String, Option>] Hash of options with long names used as keys
189
+ attr_reader :options_long
190
+ # @return [Hash<String, Option>] hash of options with short name used as keys
191
+ attr_reader :options_short
192
+
193
+ # Add an option to the necessary data structures.
194
+ #
195
+ # @param [Option] opt Option to add
196
+ #
197
+ # @return [void]
198
+ def add_option(opt)
199
+ @options_long[opt.long] = opt
200
+ @options_short[opt.short] = opt unless opt.short.nil?
201
+ end
202
+
203
+ # Define an automatic configuration variable.
204
+ #
205
+ # @param [Symbol] name Name of the configuration variable
206
+ # @param [Proc] block Block to be executed to generate the value
207
+ #
208
+ # @return [void]
209
+ def auto(name, &block)
210
+ define_method(name, &block)
211
+ end
212
+
213
+ # Define a boolean option. The variable will be set to true if
214
+ # the flag is seen and be false otherwise.
215
+ #
216
+ # @param [String] long Long name of the option
217
+ # @param [String] short Short name of the option
218
+ #
219
+ # @return [void]
220
+ def bool_option(long, short = nil)
221
+ @next_default = false
222
+ option(long, short) { true }
223
+ end
224
+
225
+ # Sets the default value for the next command. If a block is
226
+ # provided it will be used. If not, the val parameter will be.
227
+ #
228
+ # @param [Object] val Default value
229
+ # @param [Proc] block Default value generator block
230
+ #
231
+ # @return [void]
232
+ def default(val = nil, &block)
233
+ @next_default = block ? block : val
234
+ end
235
+
236
+ # Sets the help string for the next command.
237
+ #
238
+ # @param [String] str Command help string
239
+ #
240
+ # @return [void]
241
+ def help(str)
242
+ @help_string = str
243
+ end
244
+
245
+ # Install the instance class variables in the including class.
246
+ #
247
+ # @return [void]
248
+ def install_icvars
249
+ @help_string = ''
250
+ @next_default = nil
251
+ @next_required = false
252
+ @options_long = Hash.new
253
+ @options_short = Hash.new
254
+ @required = Array.new
255
+ @usage = ''
256
+ end
257
+
258
+ # Define a new option.
259
+ #
260
+ # @param [String] long Long option name
261
+ # @param [String] short Short option name
262
+ # @param [Array<Symbol>] conversions List of methods used to convert string arguments
263
+ # @param [Proc] block Block used when the option is encountered
264
+ #
265
+ # @return [void]
266
+ def option(long, short = nil, conversions: nil, &block)
267
+
268
+ attr_accessor long.to_sym
269
+
270
+ long = long.to_s
271
+ short = short.to_s if short
272
+
273
+ add_option Option.new(long, short, @help_string, @next_default,
274
+ conversions.nil? ? block : conversions)
275
+
276
+ @required << long.to_sym if @next_required
277
+
278
+ # Reset state between option declarations.
279
+ @help_string = ''
280
+ @next_default = nil
281
+ @next_required = false
282
+ end
283
+
284
+ # Mark some options as required. If no names are provided then
285
+ # the next option to be defined is required; if names are
286
+ # provided they are all marked as required.
287
+ #
288
+ # @param [Symbol] names Options to be marked as required.
289
+ #
290
+ # @return [void]
291
+ def required(*names)
292
+ if names.empty?
293
+ @next_required = true
294
+ else
295
+ @required += names
296
+ end
297
+ end
298
+
299
+ # @return [Array<Symbol>] Options that need to be marked as required
300
+ def required_options
301
+ @required
302
+ end
303
+
304
+ # Define an option that takes a single string argument.
305
+ #
306
+ # @param [String] long Long option name
307
+ # @param [String] short Short option name
308
+ #
309
+ # @return [void]
310
+ def string_option(long, short = nil)
311
+ option(long, short) { |str| str }
312
+ end
313
+
314
+ # Add's a usage string to the entire configuration object. If
315
+ # no string is provided the current usage string is returned.
316
+ #
317
+ # @param [String, nil] str Usage string
318
+ #
319
+ # @return [String] Current or new usage string
320
+ def usage(str = nil)
321
+ if str then @usage = str else @usage end
322
+ end
323
+
324
+ #############
325
+ # Callbacks #
326
+ #############
327
+
328
+ def self.extended(klass)
329
+ klass.install_icvars
330
+ end
331
+ end
332
+
333
+ #################
334
+ # Inner Classes #
335
+ #################
336
+
337
+ # This class represents an option that can appear in the
338
+ # configuration.
339
+ class Option < Struct.new(:long, :short, :help, :default, :handler)
340
+ # Returns the number of arguments that this option takes.
341
+ #
342
+ # @return [Fixnum] Number of arguments the option takes
343
+ def arity
344
+ case self.handler
345
+ when Array then self.handler.length
346
+ when Proc then self.handler.arity
347
+ end
348
+ end
349
+
350
+ # Print the option information out as a string.
351
+ #
352
+ # Layout:
353
+ # | ||--`long`,|| ||-`short`|| - |
354
+ # |_______||_________||_||________||___|
355
+ # indent max_l+3 1 max_s+1 3
356
+ #
357
+ # @param [Fixnum] max_long Maximim length of all long argumetns being printed in a block
358
+ # @param [Fixnum] max_short Maximum length of all short arguments being printed in a block
359
+ # @param [Fixnum] indent Indentation to be placed before each line
360
+ #
361
+ # @return [String]
362
+ def to_s(max_long, max_short, indent = 0)
363
+ segment_indent = indent + max_long + max_short + 8
364
+ segmented_help = self.help.segment(segment_indent)
365
+
366
+ if self.short
367
+ sprintf "#{' ' * indent}%-#{max_long + 3}s %-#{max_short + 1}s - %s", "--#{self.long},", '-' + self.short, segmented_help
368
+ else
369
+ sprintf "#{' ' * indent}%-#{max_long + max_short + 5}s - %s", '--' + self.long, segmented_help
370
+ end
371
+ end
372
+
373
+ # Helper method used to print out information on a set of options.
374
+ #
375
+ # @param [Array<Option>] options Options to be printed
376
+ # @param [Fixnum] indent Indentation to be placed before each line
377
+ #
378
+ # @return [String]
379
+ def self.to_s(options, indent = 0)
380
+ lines = []
381
+
382
+ max_long = options.inject(0) { |max, opt| max <= opt.long.length ? opt.long.length : max }
383
+ max_short = options.inject(0) { |max, opt| !opt.short.nil? && max <= opt.short.length ? opt.short.length : max }
384
+
385
+ options.each do |opt|
386
+ lines << opt.to_s(max_long, max_short, indent)
387
+ end
388
+
389
+ lines.join("\n")
390
+ end
391
+ end
392
+
393
+ #######################
394
+ # Pre-defined Options #
395
+ #######################
396
+
397
+ # The default help option. This can be added to your class via
398
+ # add_option.
399
+ HELP_OPTION = Option.new('help', 'h', 'Prints this help message.', nil, Proc.new do
400
+ puts "Usage: #{self.class.usage}"
401
+ puts
402
+ puts 'Options:'
403
+
404
+ options = self.class.options_long.values.sort { |a, b| a.long <=> b.long }
405
+ puts Option.to_s(options, 2)
406
+
407
+ # Quit the application after printing the help message.
408
+ exit
409
+ end)
410
+ end
411
+ end