qoobaa-user-choices 1.1.7
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/.document +5 -0
- data/.gitignore +5 -0
- data/LICENSE +39 -0
- data/README.rdoc +7 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/examples/older/README.txt +133 -0
- data/examples/older/command-line.rb +46 -0
- data/examples/older/default-values.rb +41 -0
- data/examples/older/multiple-sources.rb +58 -0
- data/examples/older/postprocess.rb +39 -0
- data/examples/older/switches.rb +44 -0
- data/examples/older/two-args.rb +31 -0
- data/examples/older/types.rb +61 -0
- data/examples/tutorial/css/LICENSE.txt +1 -0
- data/examples/tutorial/css/bg2.gif +0 -0
- data/examples/tutorial/css/left.gif +0 -0
- data/examples/tutorial/css/left_on.gif +0 -0
- data/examples/tutorial/css/main.css +242 -0
- data/examples/tutorial/css/right.gif +0 -0
- data/examples/tutorial/css/right_on.gif +0 -0
- data/examples/tutorial/css/tvline.gif +0 -0
- data/examples/tutorial/css/von-foerster.jpg +0 -0
- data/examples/tutorial/css/von-foerster2.jpg +0 -0
- data/examples/tutorial/index.html +703 -0
- data/examples/tutorial/tutorial1.rb +41 -0
- data/examples/tutorial/tutorial2.rb +44 -0
- data/examples/tutorial/tutorial3.rb +47 -0
- data/examples/tutorial/tutorial4.rb +47 -0
- data/examples/tutorial/tutorial5.rb +35 -0
- data/examples/tutorial/tutorial6.rb +35 -0
- data/examples/tutorial/tutorial7.rb +41 -0
- data/lib/user-choices.rb +131 -0
- data/lib/user-choices/arglist-strategies.rb +179 -0
- data/lib/user-choices/builder.rb +118 -0
- data/lib/user-choices/command-line-source.rb +224 -0
- data/lib/user-choices/command.rb +42 -0
- data/lib/user-choices/conversions.rb +169 -0
- data/lib/user-choices/ruby-extensions.rb +20 -0
- data/lib/user-choices/sources.rb +278 -0
- data/lib/user-choices/version.rb +3 -0
- data/test/arglist_strategy_test.rb +42 -0
- data/test/builder_test.rb +631 -0
- data/test/command_line_source_test.rb +443 -0
- data/test/conversion_test.rb +172 -0
- data/test/source_test.rb +451 -0
- data/test/test_helper.rb +9 -0
- data/test/user_choices_slow_test.rb +276 -0
- data/user-choices.gemspec +104 -0
- metadata +122 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 's4t-utils'
|
2
|
+
include S4tUtils
|
3
|
+
require 'enumerator'
|
4
|
+
|
5
|
+
require 'user-choices/conversions'
|
6
|
+
require 'user-choices/sources'
|
7
|
+
|
8
|
+
module UserChoices
|
9
|
+
|
10
|
+
# This class accepts a series of source and choice descriptions
|
11
|
+
# and then builds a hash-like object that describes all the choices
|
12
|
+
# a user has made before (or while) invoking a script.
|
13
|
+
class ChoicesBuilder
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@defaults = {}
|
17
|
+
@conversions = {}
|
18
|
+
@sources = []
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add the choice named _choice_, a symbol. _args_ is a keyword
|
22
|
+
# argument:
|
23
|
+
# * <tt>:default</tt> takes a value that is the default value of the _choice_.
|
24
|
+
# * <tt>:type</tt> can be given an array of valid string values. These are
|
25
|
+
# checked.
|
26
|
+
# * <tt>:type</tt> can also be given <tt>:integer</tt>. The value is cast into
|
27
|
+
# an integer. If that's impossible, an exception is raised.
|
28
|
+
# * <tt>:type</tt> can also be given <tt>:boolean</tt>. The value is converted into
|
29
|
+
# +true+ or +false+ (or an exception is raised).
|
30
|
+
# * <tt>:type</tt> can also be given <tt>[:string]</tt>. The value
|
31
|
+
# will be an array of strings. For example, "--value a,b,c" will
|
32
|
+
# produce ['a', 'b', 'c'].
|
33
|
+
#
|
34
|
+
# You might also give <tt>:length => 5</tt> or <tt>:length => 3..4</tt>. (In
|
35
|
+
# this case, a <tt>:type</tt> of <tt>[:string]</tt> is assumed.)
|
36
|
+
#
|
37
|
+
# The _block_ is passed a CommandLineSource object. It's used
|
38
|
+
# to describe the command line.
|
39
|
+
def add_choice(choice, args={}, &block)
|
40
|
+
# TODO: does the has_key? actually make a difference?
|
41
|
+
@defaults[choice] = args[:default] if args.has_key?(:default)
|
42
|
+
@conversions[choice] = []
|
43
|
+
Conversion.record_for(args[:type], @conversions[choice])
|
44
|
+
if args.has_key?(:length)
|
45
|
+
Conversion.record_for({:length => args[:length]}, @conversions[choice])
|
46
|
+
end
|
47
|
+
block.call(ArgForwarder.new(@command_line_source, choice)) if block
|
48
|
+
end
|
49
|
+
|
50
|
+
# This adds a source of choices. The _source_ is a class like
|
51
|
+
# CommandLineSource. The <tt>messages_and_args</tt> are sent
|
52
|
+
# to a new object of that class.
|
53
|
+
def add_source(source_class, *messages_and_args)
|
54
|
+
source = source_class.new
|
55
|
+
message_sends(messages_and_args).each { | send_me | source.send(*send_me) }
|
56
|
+
@sources << source
|
57
|
+
@command_line_source = source if source_class <= CommandLineSource
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add a single line composed of _string_ to the current position in the
|
61
|
+
# help output.
|
62
|
+
def add_help_line(string)
|
63
|
+
user_claims(@command_line_source) {
|
64
|
+
"Can't use 'add_help_string' when there's no command line source."
|
65
|
+
}
|
66
|
+
@command_line_source.add_help_line(string)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Demarcate a section of help text. It begins with the _description_,
|
70
|
+
# ends with a dashed line.
|
71
|
+
def section(description)
|
72
|
+
add_help_line("... " + description + ":")
|
73
|
+
yield
|
74
|
+
add_help_line("---------------------------------")
|
75
|
+
add_help_line('')
|
76
|
+
end
|
77
|
+
|
78
|
+
# In groups of related commands, there are often choices that apply to
|
79
|
+
# all commands and choices that apply only to this particular command.
|
80
|
+
# Use this to define the latter.
|
81
|
+
def section_specific_to_script
|
82
|
+
section("specific to this script") do
|
83
|
+
yield
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
|
89
|
+
# Once sources and choices have been described, this builds and
|
90
|
+
# returns a hash-like object indexed by the choices.
|
91
|
+
def build
|
92
|
+
retval = {}
|
93
|
+
@sources << DefaultSource.new.use_hash(@defaults)
|
94
|
+
@sources.each { |s| s.fill }
|
95
|
+
@sources.each { |s| s.apply(@conversions) }
|
96
|
+
@sources.reverse.each { |s| retval.merge!(s) }
|
97
|
+
@sources.each { |s| s.adjust(retval) }
|
98
|
+
retval
|
99
|
+
end
|
100
|
+
|
101
|
+
# Public for testing.
|
102
|
+
|
103
|
+
def message_sends(messages_and_args) # :nodoc:
|
104
|
+
where_at = symbol_indices(messages_and_args)
|
105
|
+
where_end = where_at[1..-1] + [messages_and_args.length]
|
106
|
+
where_at.to_enum(:each_with_index).collect do |start, where_end_index |
|
107
|
+
messages_and_args[start...where_end[where_end_index]]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def symbol_indices(array) # :nodoc:
|
112
|
+
array.to_enum(:each_with_index).collect do |obj, index|
|
113
|
+
index if obj.is_a?(Symbol)
|
114
|
+
end.compact
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 's4t-utils'
|
3
|
+
require 'user-choices/sources.rb'
|
4
|
+
require 'user-choices/arglist-strategies'
|
5
|
+
include S4tUtils
|
6
|
+
|
7
|
+
module UserChoices # :nodoc
|
8
|
+
|
9
|
+
# Treat the command line (including the arguments) as a source
|
10
|
+
# of choices.
|
11
|
+
class CommandLineSource < AbstractSource
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
super
|
15
|
+
@parser = OptionParser.new
|
16
|
+
@arglist_handler = NoArguments.new(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def source # :nodoc:
|
20
|
+
"the command line"
|
21
|
+
end
|
22
|
+
|
23
|
+
# The _usage_lines_ will be used to produce the output from
|
24
|
+
# --help (or on error).
|
25
|
+
def usage(*usage_lines)
|
26
|
+
help_banner(*usage_lines)
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Called in the case of command-line error or explicit request (--help)
|
31
|
+
# to print usage information.
|
32
|
+
def help
|
33
|
+
$stderr.puts @parser
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
|
37
|
+
# What we can parse out of the command line
|
38
|
+
|
39
|
+
# Describes how a particular _choice_ is represented on the
|
40
|
+
# command line. The _args_ are passed to OptionParser. Each arg
|
41
|
+
# will either describe one variant of option (such as <tt>"-s"</tt>
|
42
|
+
# or <tt>"--show VALUE"</tt>) or is a line of help text about
|
43
|
+
# the option (multiple lines are allowed).
|
44
|
+
#
|
45
|
+
# If the option takes an array of values, separate the values by commas:
|
46
|
+
# --files a,b,c
|
47
|
+
# There's currently no way to escape a comma and no cleverness about
|
48
|
+
# quotes.
|
49
|
+
def uses_option(choice, *args)
|
50
|
+
external_names[choice] = '--' + extract_switch_raw_name(args)
|
51
|
+
@parser.on(*args) do | value |
|
52
|
+
self[choice] = value
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# A switch is an option that doesn't take a value. A switch
|
57
|
+
# described as <tt>"--switch"</tt> has these effects:
|
58
|
+
# * If it is not given, the _choice_ is the default value
|
59
|
+
# or is not present in the hash that holds all the choices.
|
60
|
+
# * If it is given as <tt>--switch</tt>, the _choice_ has the
|
61
|
+
# value <tt>"true"</tt>. (If the _choice_ was described in
|
62
|
+
# ChoicesBuilder#add_choice as having a <tt>:type => :boolean</tt>,
|
63
|
+
# that value is converted from a string to +true+.)
|
64
|
+
# * If it is given as <tt>--no-switch</tt>, the _choice_ has the
|
65
|
+
# value <tt>"false"</tt>.
|
66
|
+
def uses_switch(choice, *args)
|
67
|
+
external_name = extract_switch_raw_name(args)
|
68
|
+
external_names[choice] = '--' + external_name
|
69
|
+
args = change_name_to_switch(external_name, args)
|
70
|
+
@parser.on(*args) do | value |
|
71
|
+
self[choice] = value.to_s
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Bundle up all non-option and non-switch arguments into an
|
76
|
+
# array of strings indexed by _choice_.
|
77
|
+
def uses_arglist(choice)
|
78
|
+
use_strategy(choice, ArbitraryArglist)
|
79
|
+
end
|
80
|
+
|
81
|
+
# The single argument required argument is turned into
|
82
|
+
# a string indexed by _choice_. Any other case is an error.
|
83
|
+
def uses_arg(choice)
|
84
|
+
use_strategy(choice, OneRequiredArg)
|
85
|
+
end
|
86
|
+
|
87
|
+
# If a single argument is present, it (as a string) is the value of
|
88
|
+
# _choice_. If no argument is present, _choice_ has no value.
|
89
|
+
# Any other case is an error.
|
90
|
+
def uses_optional_arg(choice)
|
91
|
+
use_strategy(choice, OneOptionalArg)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Add a single line composed of _string_ to the current position in the
|
95
|
+
# help output.
|
96
|
+
def add_help_line(string)
|
97
|
+
@parser.separator(string)
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# Public for testing.
|
102
|
+
|
103
|
+
def fill # :nodoc:
|
104
|
+
exit_upon_error do
|
105
|
+
remainder = @parser.parse(ARGV)
|
106
|
+
@arglist_handler.fill(remainder)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def apply(all_choice_conversions) # :nodoc:
|
111
|
+
safely_modifiable_conversions = deep_copy(all_choice_conversions)
|
112
|
+
@arglist_handler.claim_conversions(safely_modifiable_conversions)
|
113
|
+
|
114
|
+
exit_upon_error do
|
115
|
+
@arglist_handler.apply_claimed_conversions
|
116
|
+
super(safely_modifiable_conversions)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def adjust(all_choices) # :nodoc:
|
121
|
+
exit_upon_error do
|
122
|
+
@arglist_handler.adjust(all_choices)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def help_banner(banner, *more) # :nodoc:
|
127
|
+
@parser.banner = banner
|
128
|
+
more.each do | line |
|
129
|
+
add_help_line(line)
|
130
|
+
end
|
131
|
+
|
132
|
+
add_help_line ''
|
133
|
+
add_help_line 'Options:'
|
134
|
+
|
135
|
+
@parser.on_tail("-?", "-h", "--help", "Show this message.") do
|
136
|
+
help
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def deep_copy(conversions) # :nodoc:
|
141
|
+
copy = conversions.dup
|
142
|
+
copy.each do |k, v|
|
143
|
+
copy[k] = v.collect { |conversion| conversion.dup }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def use_strategy(choice, strategy) # :nodoc:
|
148
|
+
# The argument list choice probably does not need a name.
|
149
|
+
# (Currently, the name is unused.) But I'll give it one, just
|
150
|
+
# in case, and for debugging.
|
151
|
+
external_names[choice] = "the argument list"
|
152
|
+
@arglist_handler = strategy.new(self, choice)
|
153
|
+
end
|
154
|
+
|
155
|
+
|
156
|
+
def exit_upon_error # :nodoc:
|
157
|
+
begin
|
158
|
+
yield
|
159
|
+
rescue SystemExit
|
160
|
+
raise
|
161
|
+
rescue Exception => ex
|
162
|
+
message = if ex.message.has_exact_prefix?(error_prefix)
|
163
|
+
ex.message
|
164
|
+
else
|
165
|
+
error_prefix + ex.message
|
166
|
+
end
|
167
|
+
$stderr.puts(message)
|
168
|
+
help
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def extract_switch_raw_name(option_descriptions)
|
177
|
+
option_descriptions.each do | desc |
|
178
|
+
break $1 if /^--([\w-]+)/ =~ desc
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def change_name_to_switch(name, option_descriptions)
|
183
|
+
option_descriptions.collect do | desc |
|
184
|
+
/^--/ =~ desc ? "--[no-]#{name}" : desc
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
# Process command-line choices according to POSIX rules. Consider
|
191
|
+
#
|
192
|
+
# ruby copy.rb file1 --odd-file-name
|
193
|
+
#
|
194
|
+
# Ordinarily, that's permuted so that --odd-file-name is expected to
|
195
|
+
# be an option or switch, not an argument. One way to make
|
196
|
+
# CommandLineSource parsing treat it as an argument is to use a -- to
|
197
|
+
# signal the end of option parsing:
|
198
|
+
#
|
199
|
+
# ruby copy.rb -- file1 --odd-file-name
|
200
|
+
#
|
201
|
+
# Another is to rely on the user to set environment variable
|
202
|
+
# POSIXLY_CORRECT.
|
203
|
+
#
|
204
|
+
# Since both of those require the user to do something, they're error-prone.
|
205
|
+
#
|
206
|
+
# Another way is to use this class, which obeys POSIX-standard rules. Under
|
207
|
+
# those rules, the first word on the command line that does not begin with
|
208
|
+
# a dash marks the end of all options. In that case, the first command line
|
209
|
+
# above would parse into two arguments and no options.
|
210
|
+
class PosixCommandLineSource < CommandLineSource
|
211
|
+
def fill
|
212
|
+
begin
|
213
|
+
already_set = ENV.include?('POSIXLY_CORRECT')
|
214
|
+
ENV['POSIXLY_CORRECT'] = 'true' unless already_set
|
215
|
+
super
|
216
|
+
ensure
|
217
|
+
ENV.delete('POSIXLY_CORRECT') unless already_set
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
|
@@ -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,169 @@
|
|
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 NoOpConversion < Conversion # :nodoc:
|
41
|
+
def self.described_by?(conversion_tag)
|
42
|
+
conversion_tag == :string
|
43
|
+
end
|
44
|
+
|
45
|
+
def description; "a string"; end
|
46
|
+
def suitable?(actual); true; end
|
47
|
+
def convert(value); value; end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
class ConversionToInteger < Conversion # :nodoc:
|
52
|
+
def self.described_by?(conversion_tag)
|
53
|
+
conversion_tag == :integer
|
54
|
+
end
|
55
|
+
|
56
|
+
def description; "an integer"; end
|
57
|
+
|
58
|
+
def suitable?(actual)
|
59
|
+
return true if actual.is_a?(Integer)
|
60
|
+
actual.is_a?(String) and /^\d+$/ =~ actual # String check for better error message.
|
61
|
+
end
|
62
|
+
|
63
|
+
def convert(value); value.to_i; end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ConversionToBoolean < Conversion # :nodoc:
|
67
|
+
def self.described_by?(conversion_tag)
|
68
|
+
conversion_tag == :boolean
|
69
|
+
end
|
70
|
+
|
71
|
+
def description; "a boolean"; end
|
72
|
+
|
73
|
+
def suitable?(actual)
|
74
|
+
return true if [true, false].include?(actual)
|
75
|
+
return false unless actual.is_a?(String)
|
76
|
+
['true', 'false'].include?(actual.downcase)
|
77
|
+
end
|
78
|
+
|
79
|
+
def convert(value)
|
80
|
+
case value
|
81
|
+
when String
|
82
|
+
eval(value.downcase)
|
83
|
+
else
|
84
|
+
value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class SplittingConversion < Conversion # :nodoc:
|
90
|
+
def self.described_by?(conversion_tag)
|
91
|
+
conversion_tag == [:string]
|
92
|
+
end
|
93
|
+
|
94
|
+
def description; "a comma-separated list"; end
|
95
|
+
|
96
|
+
def suitable?(actual)
|
97
|
+
actual.is_a?(String) || actual.is_a?(Array)
|
98
|
+
end
|
99
|
+
|
100
|
+
def convert(value)
|
101
|
+
case value
|
102
|
+
when String
|
103
|
+
value.split(',')
|
104
|
+
when Array
|
105
|
+
value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
class LengthConversion < Conversion # :nodoc:
|
112
|
+
is_abstract
|
113
|
+
|
114
|
+
attr_reader :required_length
|
115
|
+
def initialize(conversion_tag)
|
116
|
+
super
|
117
|
+
@required_length = conversion_tag[:length]
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.described_by?(conversion_tag, value_class)
|
121
|
+
conversion_tag.is_a?(Hash) && conversion_tag[:length].is_a?(value_class)
|
122
|
+
end
|
123
|
+
|
124
|
+
def suitable?(actual)
|
125
|
+
actual.respond_to?(:length) && yield
|
126
|
+
end
|
127
|
+
|
128
|
+
def does_length_check?; true; end
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
class ExactLengthConversion < LengthConversion # :nodoc:
|
133
|
+
def self.described_by?(conversion_tag)
|
134
|
+
super(conversion_tag, Integer)
|
135
|
+
end
|
136
|
+
|
137
|
+
def description; "of length #{@required_length}"; end
|
138
|
+
|
139
|
+
def suitable?(actual)
|
140
|
+
super(actual) { actual.length == @required_length }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class RangeLengthConversion < LengthConversion # :nodoc:
|
145
|
+
def self.described_by?(conversion_tag)
|
146
|
+
super(conversion_tag, Range)
|
147
|
+
end
|
148
|
+
|
149
|
+
def description; "a list whose length is in this range: #{@required_length}"; end
|
150
|
+
|
151
|
+
def suitable?(actual)
|
152
|
+
super(actual) { @required_length.include?(actual.length) }
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# Note: since some of the above classes are described_by? methods that
|
157
|
+
# respond_to :include, this class should be last, so that it's checked
|
158
|
+
# last.
|
159
|
+
class ChoiceCheckingConversion < Conversion # :nodoc:
|
160
|
+
def self.described_by?(conversion_tag)
|
161
|
+
conversion_tag.respond_to?(:include?)
|
162
|
+
end
|
163
|
+
|
164
|
+
def suitable?(actual); @conversion_tag.include?(actual); end
|
165
|
+
def description; "one of #{friendly_list('or', @conversion_tag)}"; end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
|