user-choices 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/History.txt +17 -0
  2. data/LICENSE.txt +34 -0
  3. data/Manifest.txt +40 -0
  4. data/README.txt +1 -0
  5. data/Rakefile +19 -0
  6. data/Rakefile.hoe +24 -0
  7. data/examples/older/README.txt +133 -0
  8. data/examples/older/command-line.rb +51 -0
  9. data/examples/older/default-values.rb +47 -0
  10. data/examples/older/multiple-sources.rb +63 -0
  11. data/examples/older/postprocess.rb +45 -0
  12. data/examples/older/switches.rb +50 -0
  13. data/examples/older/two-args.rb +37 -0
  14. data/examples/older/types.rb +67 -0
  15. data/examples/tutorial/index.html +648 -0
  16. data/examples/tutorial/tutorial1.rb +48 -0
  17. data/examples/tutorial/tutorial2.rb +52 -0
  18. data/examples/tutorial/tutorial3.rb +55 -0
  19. data/examples/tutorial/tutorial4.rb +55 -0
  20. data/examples/tutorial/tutorial5.rb +42 -0
  21. data/examples/tutorial/tutorial6.rb +42 -0
  22. data/examples/tutorial/tutorial7.rb +48 -0
  23. data/lib/user-choices/arglist-strategies.rb +178 -0
  24. data/lib/user-choices/builder.rb +89 -0
  25. data/lib/user-choices/command-line-source.rb +220 -0
  26. data/lib/user-choices/command.rb +42 -0
  27. data/lib/user-choices/conversions.rb +154 -0
  28. data/lib/user-choices/ruby-extensions.rb +20 -0
  29. data/lib/user-choices/sources.rb +269 -0
  30. data/lib/user-choices/version.rb +3 -0
  31. data/lib/user-choices.rb +131 -0
  32. data/setup.rb +1585 -0
  33. data/test/arglist-strategy-tests.rb +42 -0
  34. data/test/builder-tests.rb +569 -0
  35. data/test/command-line-source-tests.rb +443 -0
  36. data/test/conversion-tests.rb +157 -0
  37. data/test/set-standalone-test-paths.rb +5 -0
  38. data/test/source-tests.rb +442 -0
  39. data/test/user-choices-slowtests.rb +274 -0
  40. data/user-choices.tmproj +575 -0
  41. metadata +138 -0
@@ -0,0 +1,220 @@
1
+ require 'optparse'
2
+ require 's4t-utils'
3
+ require 'user-choices/sources.rb'
4
+ require 'user-choices/arglist-strategies'
5
+ include S4tUtils
6
+ require 'extensions/string'
7
+
8
+ module UserChoices # :nodoc
9
+
10
+ # Treat the command line (including the arguments) as a source
11
+ # of choices.
12
+ class CommandLineSource < AbstractSource
13
+
14
+ def initialize
15
+ super
16
+ @parser = OptionParser.new
17
+ @arglist_handler = NoArguments.new(self)
18
+ end
19
+
20
+ def source # :nodoc:
21
+ "the command line"
22
+ end
23
+
24
+ # The _usage_lines_ will be used to produce the output from
25
+ # --help (or on error).
26
+ def usage(*usage_lines)
27
+ help_banner(*usage_lines)
28
+ self
29
+ end
30
+
31
+ # Called in the case of command-line error or explicit request (--help)
32
+ # to print usage information.
33
+ def help
34
+ $stderr.puts @parser
35
+ exit
36
+ end
37
+
38
+ # What we can parse out of the command line
39
+
40
+ # Describes how a particular _choice_ is represented on the
41
+ # command line. The _args_ are passed to OptionParser. Each arg
42
+ # will either describe one variant of option (such as <tt>"-s"</tt>
43
+ # or <tt>"--show VALUE"</tt>) or is a line of help text about
44
+ # the option (multiple lines are allowed).
45
+ #
46
+ # If the option takes an array of values, separate the values by commas:
47
+ # --files a,b,c
48
+ # There's currently no way to escape a comma and no cleverness about
49
+ # quotes.
50
+ def uses_option(choice, *args)
51
+ external_names[choice] = '--' + extract_switch_raw_name(args)
52
+ @parser.on(*args) do | value |
53
+ self[choice] = value
54
+ end
55
+ end
56
+
57
+ # A switch is an option that doesn't take a value. A switch
58
+ # described as <tt>"--switch"</tt> has these effects:
59
+ # * If it is not given, the _choice_ is the default value
60
+ # or is not present in the hash that holds all the choices.
61
+ # * If it is given as <tt>--switch</tt>, the _choice_ has the
62
+ # value <tt>"true"</tt>. (If the _choice_ was described in
63
+ # ChoicesBuilder#add_choice as having a <tt>:type => :boolean</tt>,
64
+ # that value is converted from a string to +true+.)
65
+ # * If it is given as <tt>--no-switch</tt>, the _choice_ has the
66
+ # value <tt>"false"</tt>.
67
+ def uses_switch(choice, *args)
68
+ external_name = extract_switch_raw_name(args)
69
+ external_names[choice] = '--' + external_name
70
+ args = change_name_to_switch(external_name, args)
71
+ @parser.on(*args) do | value |
72
+ self[choice] = value.to_s
73
+ end
74
+ end
75
+
76
+ # Bundle up all non-option and non-switch arguments into an
77
+ # array of strings indexed by _choice_.
78
+ def uses_arglist(choice)
79
+ use_strategy(choice, ArbitraryArglist)
80
+ end
81
+
82
+ # The single argument required argument is turned into
83
+ # a string indexed by _choice_. Any other case is an error.
84
+ def uses_arg(choice)
85
+ use_strategy(choice, OneRequiredArg)
86
+ end
87
+
88
+ # If a single argument is present, it (as a string) is the value of
89
+ # _choice_. If no argument is present, _choice_ has no value.
90
+ # Any other case is an error.
91
+ def uses_optional_arg(choice)
92
+ use_strategy(choice, OneOptionalArg)
93
+ end
94
+
95
+
96
+
97
+
98
+ # Public for testing.
99
+
100
+ def fill # :nodoc:
101
+ exit_upon_error do
102
+ remainder = @parser.parse(ARGV)
103
+ @arglist_handler.fill(remainder)
104
+ end
105
+ end
106
+
107
+ def apply(all_choice_conversions) # :nodoc:
108
+ safely_modifiable_conversions = deep_copy(all_choice_conversions)
109
+ @arglist_handler.claim_conversions(safely_modifiable_conversions)
110
+
111
+ exit_upon_error do
112
+ @arglist_handler.apply_claimed_conversions
113
+ super(safely_modifiable_conversions)
114
+ end
115
+ end
116
+
117
+ def adjust(all_choices) # :nodoc:
118
+ exit_upon_error do
119
+ @arglist_handler.adjust(all_choices)
120
+ end
121
+ end
122
+
123
+ def help_banner(banner, *more) # :nodoc:
124
+ @parser.banner = banner
125
+ more.each do | line |
126
+ @parser.separator(line)
127
+ end
128
+ @parser.separator ''
129
+ @parser.separator 'Options:'
130
+
131
+ @parser.on_tail("-?", "-h", "--help", "Show this message.") do
132
+ help
133
+ end
134
+ end
135
+
136
+ def deep_copy(conversions) # :nodoc:
137
+ copy = conversions.dup
138
+ copy.each do |k, v|
139
+ copy[k] = v.collect { |conversion| conversion.dup }
140
+ end
141
+ end
142
+
143
+ def use_strategy(choice, strategy) # :nodoc:
144
+ # The argument list choice probably does not need a name.
145
+ # (Currently, the name is unused.) But I'll give it one, just
146
+ # in case, and for debugging.
147
+ external_names[choice] = "the argument list"
148
+ @arglist_handler = strategy.new(self, choice)
149
+ end
150
+
151
+
152
+ def exit_upon_error # :nodoc:
153
+ begin
154
+ yield
155
+ rescue SystemExit
156
+ raise
157
+ rescue Exception => ex
158
+ message = if ex.message.starts_with?(error_prefix)
159
+ ex.message
160
+ else
161
+ error_prefix + ex.message
162
+ end
163
+ $stderr.puts(message)
164
+ help
165
+ end
166
+ end
167
+
168
+
169
+
170
+ private
171
+
172
+ def extract_switch_raw_name(option_descriptions)
173
+ option_descriptions.each do | desc |
174
+ break $1 if /^--([\w-]+)/ =~ desc
175
+ end
176
+ end
177
+
178
+ def change_name_to_switch(name, option_descriptions)
179
+ option_descriptions.collect do | desc |
180
+ /^--/ =~ desc ? "--[no-]#{name}" : desc
181
+ end
182
+ end
183
+ end
184
+
185
+
186
+ # Process command-line choices according to POSIX rules. Consider
187
+ #
188
+ # ruby copy.rb file1 --odd-file-name
189
+ #
190
+ # Ordinarily, that's permuted so that --odd-file-name is expected to
191
+ # be an option or switch, not an argument. One way to make
192
+ # CommandLineSource parsing treat it as an argument is to use a -- to
193
+ # signal the end of option parsing:
194
+ #
195
+ # ruby copy.rb -- file1 --odd-file-name
196
+ #
197
+ # Another is to rely on the user to set environment variable
198
+ # POSIXLY_CORRECT.
199
+ #
200
+ # Since both of those require the user to do something, they're error-prone.
201
+ #
202
+ # Another way is to use this class, which obeys POSIX-standard rules. Under
203
+ # those rules, the first word on the command line that does not begin with
204
+ # a dash marks the end of all options. In that case, the first command line
205
+ # above would parse into two arguments and no options.
206
+ class PosixCommandLineSource < CommandLineSource
207
+ def fill
208
+ begin
209
+ already_set = ENV.include?('POSIXLY_CORRECT')
210
+ ENV['POSIXLY_CORRECT'] = 'true' unless already_set
211
+ super
212
+ ensure
213
+ ENV.delete('POSIXLY_CORRECT') unless already_set
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+
220
+
@@ -0,0 +1,42 @@
1
+ module UserChoices
2
+
3
+ # A template class. Subclasses describe all the choices a user may
4
+ # make to affect the execution of the command, and the sources for
5
+ # those choices (such as the command line).
6
+ class Command
7
+
8
+ attr_reader :user_choices
9
+
10
+ def initialize
11
+ builder = ChoicesBuilder.new
12
+ add_sources(builder)
13
+ add_choices(builder)
14
+ @user_choices = builder.build
15
+ postprocess_user_choices
16
+ end
17
+
18
+ # Add sources such as EnvironmentSource, XmlConfigFileSource,
19
+ # and CommandLineSource to the ChoicesBuilder.
20
+ #
21
+ # Must be defined in a subclass.
22
+ def add_sources(builder); subclass_responsibility; end
23
+
24
+ # Add choices to the ChoicesBuilder. A choice is a symbol that ends up in the
25
+ # @user_choices hash. It may have a :default value and a :type, as well
26
+ # as an OptionParser-style description of command-line arguments.
27
+ #
28
+ # Must be defined in a subclass.
29
+ def add_choices(builder); subclass_responsibility; end
30
+
31
+ # After choices from all sources are collected into @user_choices, this
32
+ # method is called to do any postprocessing. Does nothing unless
33
+ # overridden.
34
+ def postprocess_user_choices
35
+ end
36
+
37
+ # Execute the command, using @user_choices for parameterization.
38
+ #
39
+ # Must be defined in a subclass.
40
+ def execute; subclass_responsibility; end
41
+ end
42
+ end
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-08-06.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+ module UserChoices
7
+ class Conversion # :nodoc:
8
+ @@subclasses = []
9
+ def self.inherited(subclass)
10
+ @@subclasses << subclass
11
+ end
12
+
13
+ def self.is_abstract
14
+ @@subclasses.delete(self)
15
+ end
16
+
17
+ def self.record_for(conversion_tag, recorder)
18
+ return if conversion_tag.nil? # This simplifies caller.
19
+ recorder << self.for(conversion_tag)
20
+ end
21
+
22
+ def self.for(conversion_tag)
23
+ subclass = @@subclasses.find { |sc| sc.described_by?(conversion_tag) }
24
+ user_claims(subclass) { "#{conversion_tag} doesn't describe any Conversion object." }
25
+ subclass.new(conversion_tag)
26
+ end
27
+
28
+ def initialize(conversion_tag)
29
+ @conversion_tag = conversion_tag
30
+ end
31
+
32
+ def self.described_by?(conversion_tag); subclass_responsibility; end
33
+ def suitable?(actual); subclass_responsibility; end
34
+ def description; subclass_responsibility; end
35
+
36
+ def convert(value); value; end # Some conversions are just for error-checking
37
+ def does_length_check?; false; end
38
+ end
39
+
40
+ class ConversionToInteger < Conversion # :nodoc:
41
+ def self.described_by?(conversion_tag)
42
+ conversion_tag == :integer
43
+ end
44
+
45
+ def description; "an integer"; end
46
+
47
+ def suitable?(actual)
48
+ return true if actual.is_a?(Integer)
49
+ actual.is_a?(String) and /^\d+$/ =~ actual # String check for better error message.
50
+ end
51
+
52
+ def convert(value); value.to_i; end
53
+ end
54
+
55
+ class ConversionToBoolean < Conversion # :nodoc:
56
+ def self.described_by?(conversion_tag)
57
+ conversion_tag == :boolean
58
+ end
59
+
60
+ def description; "a boolean"; end
61
+
62
+ def suitable?(actual)
63
+ return true if [true, false].include?(actual)
64
+ return false unless actual.is_a?(String)
65
+ ['true', 'false'].include?(actual.downcase)
66
+ end
67
+
68
+ def convert(value)
69
+ case value
70
+ when String: eval(value.downcase)
71
+ else value
72
+ end
73
+ end
74
+ end
75
+
76
+ class SplittingConversion < Conversion # :nodoc:
77
+ def self.described_by?(conversion_tag)
78
+ conversion_tag == [:string]
79
+ end
80
+
81
+ def description; "a comma-separated list"; end
82
+
83
+ def suitable?(actual)
84
+ actual.is_a?(String) || actual.is_a?(Array)
85
+ end
86
+
87
+ def convert(value)
88
+ case value
89
+ when String: value.split(',')
90
+ when Array: value
91
+ end
92
+ end
93
+
94
+ end
95
+
96
+ class LengthConversion < Conversion # :nodoc:
97
+ is_abstract
98
+
99
+ attr_reader :required_length
100
+ def initialize(conversion_tag)
101
+ super
102
+ @required_length = conversion_tag[:length]
103
+ end
104
+
105
+ def self.described_by?(conversion_tag, value_class)
106
+ conversion_tag.is_a?(Hash) && conversion_tag[:length].is_a?(value_class)
107
+ end
108
+
109
+ def suitable?(actual)
110
+ actual.respond_to?(:length) && yield
111
+ end
112
+
113
+ def does_length_check?; true; end
114
+
115
+ end
116
+
117
+ class ExactLengthConversion < LengthConversion # :nodoc:
118
+ def self.described_by?(conversion_tag)
119
+ super(conversion_tag, Integer)
120
+ end
121
+
122
+ def description; "of length #{@required_length}"; end
123
+
124
+ def suitable?(actual)
125
+ super(actual) { actual.length == @required_length }
126
+ end
127
+ end
128
+
129
+ class RangeLengthConversion < LengthConversion # :nodoc:
130
+ def self.described_by?(conversion_tag)
131
+ super(conversion_tag, Range)
132
+ end
133
+
134
+ def description; "a list whose length is in this range: #{@required_length}"; end
135
+
136
+ def suitable?(actual)
137
+ super(actual) { @required_length.include?(actual.length) }
138
+ end
139
+ end
140
+
141
+ # Note: since some of the above classes are described_by? methods that
142
+ # respond_to :include, this class should be last, so that it's checked
143
+ # last.
144
+ class ChoiceCheckingConversion < Conversion # :nodoc:
145
+ def self.described_by?(conversion_tag)
146
+ conversion_tag.respond_to?(:include?)
147
+ end
148
+
149
+ def suitable?(actual); @conversion_tag.include?(actual); end
150
+ def description; "one of #{friendly_list('or', @conversion_tag)}"; end
151
+ end
152
+ end
153
+
154
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Created by Brian Marick on 2007-08-10.
4
+ # Copyright (c) 2007. All rights reserved.
5
+
6
+
7
+ class Range # :nodoc:
8
+ def in_words
9
+ last_element = self.last
10
+ last_element -= 1 if exclude_end?
11
+ "#{self.first} to #{last_element}"
12
+ end
13
+ end
14
+
15
+ class String # :nodoc:
16
+ def to_inputable_sym
17
+ gsub(/-/, '_').to_sym
18
+ end
19
+ end
20
+
@@ -0,0 +1,269 @@
1
+ require 'xmlsimple'
2
+ require 'yaml'
3
+ require 's4t-utils'
4
+ include S4tUtils
5
+
6
+ require 'user-choices/ruby-extensions'
7
+ require 'user-choices/conversions'
8
+
9
+
10
+ module UserChoices # :nodoc
11
+
12
+
13
+ # TODO: Right now, elements that are named in a source, but not in an
14
+ # add_choice() call, nevertheless appear in the final array. Is that good?
15
+ # Bad? Irrelevant?
16
+ class AbstractSource < Hash # :nodoc:
17
+
18
+ attr_reader :external_names
19
+
20
+ def initialize
21
+ super()
22
+ @external_names = {}
23
+ end
24
+
25
+ def source; subclass_responsibility; end
26
+
27
+
28
+ def fill; subclass_responsibility; end
29
+
30
+ def apply(choice_conversions)
31
+ each_conversion(choice_conversions) do | choice, conversion |
32
+ next unless self.has_key?(choice)
33
+
34
+ user_claims(conversion.suitable?(self[choice])) {
35
+ error_prefix + bad_look(choice, conversion)
36
+ }
37
+
38
+ self[choice] = conversion.convert(self[choice])
39
+ end
40
+ end
41
+
42
+ def adjust(final_results)
43
+ # Do nothing
44
+ end
45
+
46
+ def each_conversion(choice_conversion_hash)
47
+ choice_conversion_hash.each do | choice, conversion_list |
48
+ conversion_list.each do | conversion |
49
+ yield(choice, conversion)
50
+ end
51
+ end
52
+ end
53
+
54
+
55
+ protected
56
+
57
+ def error_prefix
58
+ "Error in #{source}: "
59
+ end
60
+
61
+ def pretty_value(value)
62
+ case value
63
+ when Array: value.inspect
64
+ else "'#{value}'"
65
+ end
66
+ end
67
+
68
+ def bad_look(key, conversion)
69
+ like_what = conversion.description
70
+ "#{@external_names[key]}'s value must be #{like_what}, and #{pretty_value(self[key])} doesn't look right."
71
+ end
72
+
73
+ end
74
+
75
+ class DefaultSource < AbstractSource # :nodoc:
76
+
77
+ def use_hash(defaults)
78
+ @defaults = defaults
79
+ count_symbols_as_external_names(@defaults.keys)
80
+ self
81
+ end
82
+
83
+ def fill
84
+ merge!(@defaults)
85
+ end
86
+
87
+ def source
88
+ "the default values"
89
+ end
90
+
91
+ def count_symbols_as_external_names(symbols)
92
+ symbols.each { | symbol |
93
+ # Use inspect so that symbol prints with leading colon
94
+ @external_names[symbol] = symbol.inspect
95
+ }
96
+ end
97
+ end
98
+ DefaultChoices = DefaultSource # Backward compatibility
99
+
100
+
101
+
102
+ # Describe the environment as a source of choices.
103
+ class EnvironmentSource < AbstractSource
104
+ def fill # :nodoc:
105
+ @external_names.each { | key, env_var |
106
+ self[key] = ENV[env_var] if ENV.has_key?(env_var)
107
+ }
108
+ end
109
+
110
+ # Environment variables beginning with _prefix_ (a string)
111
+ # are considered to be user choices relevant to this script.
112
+ # Everything after the prefix names a choice (that is, a symbol).
113
+ # Dashes are converted to underscores. Examples:
114
+ # * Environment variable <tt>prefix-my-choice</tt> with prefix <tt>"prefix-" is choice <tt>:my_choice</tt>.
115
+ # * Environment variable <tt>PREFIX_FOO</tt> with prefix <tt>"PREFIX_" is choice <tt>:FOO</tt>
116
+ #
117
+ # If you want an array of strings, separate the values by commas:
118
+ # ENV_VAR=a,b,c
119
+ # There's currently no way to escape a comma and no cleverness about
120
+ # quotes or whitespace.
121
+
122
+ def with_prefix(prefix)
123
+ matches = ENV.collect do | env_var, ignored_value |
124
+ if /^#{prefix}(.+)/ =~ env_var
125
+ [$1.to_inputable_sym, env_var]
126
+ end
127
+ end
128
+ @external_names.merge!(Hash[*matches.compact.flatten])
129
+ self
130
+ end
131
+
132
+ # Some environment variables have names you don't like. For example, $HOME
133
+ # might be annoying because of the uppercase. Also, if most of your program's
134
+ # environment variables have some prefix (see with_prefix) but you also want to use
135
+ # $HOME, you need a way to do that. You can satisfy both desires with
136
+ #
137
+ # EnvironmentSource.new.with_prefix("my_").mapping(:home => "HOME")
138
+
139
+ def mapping(map)
140
+ @external_names.merge!(map)
141
+ self
142
+ end
143
+
144
+
145
+ def source # :nodoc:
146
+ "the environment"
147
+ end
148
+ end
149
+ EnvironmentChoices = EnvironmentSource # Backward compatibility
150
+
151
+
152
+ class FileSource < AbstractSource # :nodoc:
153
+
154
+ def from_file(filename)
155
+ @path = File.join(S4tUtils.find_home, filename)
156
+ @contents_as_hash = self.read_into_hash
157
+ @contents_as_hash.each do | external_name, value |
158
+ sym = external_name.to_inputable_sym
159
+ @external_names[sym] = external_name
160
+ end
161
+ self
162
+ end
163
+
164
+ def fill # :nodoc:
165
+ @external_names.each do | symbol, external_name |
166
+ self[symbol] = @contents_as_hash[external_name]
167
+ end
168
+ end
169
+
170
+ def source # :nodoc:
171
+ "configuration file #{@path}"
172
+ end
173
+
174
+ def read_into_hash # :nodoc:
175
+ return {} unless File.exist?(@path)
176
+ begin
177
+ format_specific_reading
178
+ rescue Exception => ex
179
+ if format_specific_exception?(ex)
180
+ msg = "Badly formatted #{source}: " + format_specific_message(ex)
181
+ ex = ex.class.new(msg)
182
+ end
183
+ raise ex
184
+ end
185
+ end
186
+
187
+ protected
188
+
189
+ def format_specific_message(ex)
190
+ ex.message
191
+ end
192
+
193
+ def format_specific_exception_handling(ex); subclass_responsibility; end
194
+ def format_specific_reading; subclass_responsibility; end
195
+ end
196
+
197
+ # Use an XML file as a source of choices. The XML file is parsed
198
+ # with <tt>XmlSimple('ForceArray' => false)</tt>. That means that
199
+ # single elements like <home>Mars</home> are read as the value
200
+ # <tt>"Mars"</tt>, whereas <home>Mars</home><home>Venus</home> is
201
+ # read as <tt>["Mars", "Venus"]</tt>.
202
+ class XmlConfigFileSource < FileSource
203
+
204
+ # Treat _filename_ as the configuration file. _filename_ is expected
205
+ # to be in the home directory. The home directory is found in the
206
+ # same way Rubygems finds it. (First look in environment variables
207
+ # <tt>$HOME</tt>, <tt>$USERPROFILE</tt>, <tt>$HOMEDRIVE:$HOMEPATH</tt>,
208
+ # file expansion of <tt>"~"</tt> and finally the root.)
209
+ def format_specific_reading
210
+ XmlSimple.xml_in(@path, 'ForceArray' => false)
211
+ end
212
+
213
+ def format_specific_exception?(ex)
214
+ ex.is_a?(REXML::ParseException)
215
+ end
216
+
217
+ def format_specific_message(ex)
218
+ ex.continued_exception
219
+ end
220
+
221
+ end
222
+ XmlConfigFileChoices = XmlConfigFileSource # Backward compatibility
223
+
224
+
225
+
226
+
227
+ # Use an YAML file as a source of choices. Note: because the YAML parser
228
+ # can produce something out of many typo-filled YAML files, it's a
229
+ # good idea to check that your file looks like you'd expect before
230
+ # trusting in it. Do that with:
231
+ #
232
+ # irb> require 'yaml'
233
+ # irb> YAML.load_file('config.yaml')
234
+ #
235
+ class YamlConfigFileSource < FileSource
236
+ # Treat _filename_ as the configuration file. _filename_ is expected
237
+ # to be in the home directory. The home directory is found in the
238
+ # same way Rubygems finds it.
239
+ def format_specific_reading
240
+ result = YAML.load_file(@path)
241
+ ensure_hash_values_are_strings(result)
242
+ result
243
+ end
244
+
245
+ def format_specific_exception?(ex)
246
+ ex.is_a?(ArgumentError)
247
+ end
248
+
249
+
250
+
251
+ def ensure_hash_values_are_strings(h)
252
+ h.each { |k, v| ensure_element_is_string(h, k) }
253
+ end
254
+
255
+ def ensure_array_values_are_strings(a)
256
+ a.each_with_index { |elt, index| ensure_element_is_string(a, index) }
257
+ end
258
+
259
+ def ensure_element_is_string(collection, key)
260
+ case collection[key]
261
+ when Hash: ensure_hash_values_are_strings(collection[key])
262
+ when Array: ensure_array_values_are_strings(collection[key])
263
+ else collection[key] = collection[key].to_s
264
+ end
265
+ end
266
+
267
+
268
+ end
269
+ end
@@ -0,0 +1,3 @@
1
+ module UserChoices
2
+ Version = '1.1.0'
3
+ end