user-choices 1.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.
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