bahuvrihi-tap 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/History +69 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +119 -0
  4. data/bin/tap +114 -0
  5. data/cmd/console.rb +42 -0
  6. data/cmd/destroy.rb +16 -0
  7. data/cmd/generate.rb +16 -0
  8. data/cmd/run.rb +126 -0
  9. data/doc/Class Reference +362 -0
  10. data/doc/Command Reference +153 -0
  11. data/doc/Tutorial +237 -0
  12. data/lib/tap.rb +32 -0
  13. data/lib/tap/app.rb +720 -0
  14. data/lib/tap/constants.rb +8 -0
  15. data/lib/tap/env.rb +640 -0
  16. data/lib/tap/file_task.rb +547 -0
  17. data/lib/tap/generator/base.rb +109 -0
  18. data/lib/tap/generator/destroy.rb +37 -0
  19. data/lib/tap/generator/generate.rb +61 -0
  20. data/lib/tap/generator/generators/command/command_generator.rb +21 -0
  21. data/lib/tap/generator/generators/command/templates/command.erb +32 -0
  22. data/lib/tap/generator/generators/config/config_generator.rb +26 -0
  23. data/lib/tap/generator/generators/config/templates/doc.erb +12 -0
  24. data/lib/tap/generator/generators/config/templates/nodoc.erb +8 -0
  25. data/lib/tap/generator/generators/file_task/file_task_generator.rb +27 -0
  26. data/lib/tap/generator/generators/file_task/templates/file.txt +11 -0
  27. data/lib/tap/generator/generators/file_task/templates/result.yml +6 -0
  28. data/lib/tap/generator/generators/file_task/templates/task.erb +33 -0
  29. data/lib/tap/generator/generators/file_task/templates/test.erb +29 -0
  30. data/lib/tap/generator/generators/root/root_generator.rb +55 -0
  31. data/lib/tap/generator/generators/root/templates/Rakefile +86 -0
  32. data/lib/tap/generator/generators/root/templates/gemspec +27 -0
  33. data/lib/tap/generator/generators/root/templates/tapfile +8 -0
  34. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +5 -0
  36. data/lib/tap/generator/generators/root/templates/test/tapfile_test.rb +15 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +27 -0
  38. data/lib/tap/generator/generators/task/templates/task.erb +14 -0
  39. data/lib/tap/generator/generators/task/templates/test.erb +21 -0
  40. data/lib/tap/generator/manifest.rb +14 -0
  41. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  42. data/lib/tap/patches/rake/testtask.rb +55 -0
  43. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  44. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  45. data/lib/tap/root.rb +581 -0
  46. data/lib/tap/support/aggregator.rb +55 -0
  47. data/lib/tap/support/assignments.rb +172 -0
  48. data/lib/tap/support/audit.rb +418 -0
  49. data/lib/tap/support/batchable.rb +47 -0
  50. data/lib/tap/support/batchable_class.rb +107 -0
  51. data/lib/tap/support/class_configuration.rb +194 -0
  52. data/lib/tap/support/command_line.rb +98 -0
  53. data/lib/tap/support/comment.rb +270 -0
  54. data/lib/tap/support/configurable.rb +114 -0
  55. data/lib/tap/support/configurable_class.rb +296 -0
  56. data/lib/tap/support/configuration.rb +122 -0
  57. data/lib/tap/support/constant.rb +70 -0
  58. data/lib/tap/support/constant_utils.rb +127 -0
  59. data/lib/tap/support/declarations.rb +111 -0
  60. data/lib/tap/support/executable.rb +111 -0
  61. data/lib/tap/support/executable_queue.rb +82 -0
  62. data/lib/tap/support/framework.rb +71 -0
  63. data/lib/tap/support/framework_class.rb +199 -0
  64. data/lib/tap/support/instance_configuration.rb +147 -0
  65. data/lib/tap/support/lazydoc.rb +428 -0
  66. data/lib/tap/support/manifest.rb +89 -0
  67. data/lib/tap/support/run_error.rb +39 -0
  68. data/lib/tap/support/shell_utils.rb +71 -0
  69. data/lib/tap/support/summary.rb +30 -0
  70. data/lib/tap/support/tdoc.rb +404 -0
  71. data/lib/tap/support/tdoc/tdoc_html_generator.rb +38 -0
  72. data/lib/tap/support/tdoc/tdoc_html_template.rb +42 -0
  73. data/lib/tap/support/templater.rb +180 -0
  74. data/lib/tap/support/validation.rb +410 -0
  75. data/lib/tap/support/versions.rb +97 -0
  76. data/lib/tap/task.rb +259 -0
  77. data/lib/tap/tasks/dump.rb +56 -0
  78. data/lib/tap/tasks/rake.rb +93 -0
  79. data/lib/tap/test.rb +37 -0
  80. data/lib/tap/test/env_vars.rb +29 -0
  81. data/lib/tap/test/file_methods.rb +377 -0
  82. data/lib/tap/test/script_methods.rb +144 -0
  83. data/lib/tap/test/subset_methods.rb +420 -0
  84. data/lib/tap/test/tap_methods.rb +237 -0
  85. data/lib/tap/workflow.rb +187 -0
  86. metadata +145 -0
@@ -0,0 +1,270 @@
1
+ require 'strscan'
2
+
3
+ module Tap
4
+ module Support
5
+ # Comment represents a comment parsed by Lazydoc.
6
+ class Comment
7
+
8
+ class << self
9
+
10
+ # Parses the input string into a comment, stopping at end_regexp
11
+ # or the first non-comment line. Also parses the next non-comment
12
+ # lines as the comment subject. Takes a string or a StringScanner
13
+ # and returns the new comment.
14
+ #
15
+ # comment_string = %Q{
16
+ # # comments spanning multiple
17
+ # # lines are collected
18
+ # #
19
+ # # while indented lines
20
+ # # are preserved individually
21
+ # #
22
+ # this is the subject line
23
+ #
24
+ # # this line is not parsed as it
25
+ # # is after a non-comment line
26
+ # }
27
+ #
28
+ # c = Comment.parse(comment_string)
29
+ # c.lines
30
+ # # => [
31
+ # # ['comments spanning multiple', 'lines are collected'],
32
+ # # [''],
33
+ # # [' while indented lines'],
34
+ # # [' are preserved individually'],
35
+ # # [''],
36
+ # # []]
37
+ # c.subject # => "this is the subject line"
38
+ #
39
+ def parse(str, parse_subject=true) # :yields: fragment
40
+ scanner = case str
41
+ when StringScanner then str
42
+ when String then StringScanner.new(str)
43
+ else raise TypeError, "can't convert #{str.class} into StringScanner or String"
44
+ end
45
+
46
+ comment = Comment.new
47
+ while scanner.scan(/\r?\n?[ \t]*#[ \t]?(([ \t]*).*?)\r?$/)
48
+ fragment = scanner[1]
49
+ indent = scanner[2]
50
+
51
+ # collect continuous description line
52
+ # fragments and join into a single line
53
+ if block_given? && yield(fragment)
54
+ # break on comment if the description end is reached
55
+ parse_subject = false
56
+ break
57
+ else
58
+ categorize(fragment, indent) {|f| comment.push(f) }
59
+ end
60
+ end
61
+
62
+ if parse_subject
63
+ scanner.skip(/\s+/)
64
+ unless scanner.peek(1) == '#'
65
+ comment.subject = scanner.scan(/.+?$/)
66
+ comment.subject.strip! unless comment.subject == nil
67
+ end
68
+ end
69
+
70
+ comment
71
+ end
72
+
73
+ # Scans the line checking if it is a comment. If so, scan
74
+ # yields the parse fragments to the block which correspond
75
+ # to the type of comment input (continuation, indent, etc).
76
+ # Returns true if the line is a comment, false otherwise.
77
+ #
78
+ # Scan may be used to build a comment from an array of lines:
79
+ #
80
+ # lines = [
81
+ # "# comments spanning multiple",
82
+ # "# lines are collected",
83
+ # "#",
84
+ # "# while indented lines",
85
+ # "# are preserved individually",
86
+ # "# ",
87
+ # "not a comment line",
88
+ # "# skipped since the loop breaks",
89
+ # "# at the first non-comment line"]
90
+ #
91
+ # c = Comment.new
92
+ # lines.each do |line|
93
+ # break unless Comment.scan(line) do |fragment|
94
+ # # c.unshift will also work if building in reverse
95
+ # c.push(fragment)
96
+ # end
97
+ # end
98
+ #
99
+ # c.lines
100
+ # # => [
101
+ # # ['comments spanning multiple', 'lines are collected'],
102
+ # # [''],
103
+ # # [' while indented lines'],
104
+ # # [' are preserved individually'],
105
+ # # [''],
106
+ # # []]
107
+ #
108
+ def scan(line) # :yields: fragment
109
+ return false unless line =~ /^[ \t]*#[ \t]?(([ \t]*).*?)\r?$/
110
+ categorize($1, $2) do |fragment|
111
+ yield(fragment)
112
+ end
113
+ true
114
+ end
115
+
116
+ def wrap(lines, cols=80, tabsize=2)
117
+ lines = lines.split(/\r?\n/) unless lines.kind_of?(Array)
118
+
119
+ lines.collect do |line|
120
+ line = line.gsub(/\t/, " " * tabsize) unless tabsize == nil
121
+
122
+ if line.strip.empty?
123
+ line
124
+ else
125
+ # wrapping algorithm is slightly modified from
126
+ # http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
127
+ line.gsub(/(.{1,#{cols}})( +|$\r?\n?)|(.{1,#{cols}})/, "\\1\\3\n").split(/\s*\n/)
128
+ end
129
+ end.flatten
130
+ end
131
+
132
+ private
133
+
134
+ def categorize(fragment, indent)
135
+ case
136
+ when fragment == indent
137
+ # empty comment line
138
+ yield [""]
139
+ yield []
140
+ when indent.empty?
141
+ # continuation line
142
+ yield fragment.rstrip
143
+ else
144
+ # indented line
145
+ yield [fragment.rstrip]
146
+ yield []
147
+ end
148
+ end
149
+ end
150
+
151
+ # An array of line fragment arrays.
152
+ attr_reader :lines
153
+
154
+ # The next non-comment line after the comment ends.
155
+ # This is the line that would receive the comment
156
+ # in RDoc documentation.
157
+ attr_accessor :subject
158
+
159
+ # Returns the line number for the subject line, if known.
160
+ attr_accessor :line_number
161
+
162
+ def initialize(line_number=nil)
163
+ @lines = []
164
+ @subject = nil
165
+ @line_number = line_number
166
+ end
167
+
168
+ # Pushes the fragment onto the last line array. If fragment is an
169
+ # array itself, then fragment will be pushed onto lines.
170
+ #
171
+ # c = Comment.new
172
+ # c.push "some line"
173
+ # c.push "fragments"
174
+ # c.push ["a", "whole", "new line"]
175
+ # c.lines # => [["some line", "fragments"], ["a", "whole", "new line"]]
176
+ #
177
+ def push(fragment)
178
+ lines << [] if lines.empty?
179
+
180
+ case fragment
181
+ when Array
182
+ if lines[-1].empty?
183
+ lines[-1] = fragment
184
+ else
185
+ lines.push fragment
186
+ end
187
+ else
188
+ lines[-1].push fragment
189
+ end
190
+ end
191
+
192
+ # Alias for push.
193
+ def <<(fragment)
194
+ push(fragment)
195
+ end
196
+
197
+ # Unshifts the fragment to the first line array. If fragment is an
198
+ # array itself, then fragment will be unshifted onto lines.
199
+ #
200
+ # c = Comment.new
201
+ # c.unshift "some line"
202
+ # c.unshift "fragments"
203
+ # c.unshift ["a", "whole", "new line"]
204
+ # c.lines # => [["a", "whole", "new line"], ["fragments", "some line"]]
205
+ #
206
+ def unshift(fragment)
207
+ lines << [] if lines.empty?
208
+
209
+ case fragment
210
+ when Array
211
+ if lines[0].empty?
212
+ lines[0] = fragment
213
+ else
214
+ lines.unshift fragment
215
+ end
216
+ else
217
+ lines[0].unshift fragment
218
+ end
219
+ end
220
+
221
+ def prepend(comment_line)
222
+ Comment.scan(comment_line) {|f| unshift(f) }
223
+ end
224
+
225
+ def append(comment_line)
226
+ Comment.scan(comment_line) {|f| push(f) }
227
+ end
228
+
229
+ # Removes leading and trailing lines that are empty ([])
230
+ # or whitespace (['']). Returns self.
231
+ def trim
232
+ lines.shift while !lines.empty? && (lines[0].empty? || lines[0].join.strip.empty?)
233
+ lines.pop while !lines.empty? && (lines[-1].empty? || lines[-1].join.strip.empty?)
234
+ self
235
+ end
236
+
237
+ # True if there are no fragments in self.
238
+ def empty?
239
+ !lines.find {|array| !array.empty?}
240
+ end
241
+
242
+ def wrap(cols=80, tabsize=2, line_sep="\n", fragment_sep=" ", strip=true)
243
+ resolved_lines = Comment.wrap(to_s(fragment_sep, nil, strip), cols, tabsize)
244
+ line_sep ? resolved_lines.join(line_sep) : resolved_lines
245
+ end
246
+
247
+ # Returns lines as a string where line fragments are joined by
248
+ # fragment_sep and lines are joined by line_sep.
249
+ def to_s(fragment_sep=" ", line_sep="\n", strip=true)
250
+ resolved_lines = lines.collect {|line| line.join(fragment_sep)}
251
+
252
+ # strip leading an trailing whitespace lines
253
+ if strip
254
+ resolved_lines.shift while !resolved_lines.empty? && resolved_lines[0].empty?
255
+ resolved_lines.pop while !resolved_lines.empty? && resolved_lines[-1].empty?
256
+ end
257
+
258
+ line_sep ? resolved_lines.join(line_sep) : resolved_lines
259
+ end
260
+
261
+ def ==(another)
262
+ another.kind_of?(Comment) &&
263
+ self.line_number == another.line_number &&
264
+ self.subject == another.subject &&
265
+ self.lines == another.lines
266
+ end
267
+
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,114 @@
1
+ require 'tap/support/configurable_class'
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Configurable enables the specification of configurations within a class definition.
7
+ #
8
+ # class ConfigClass
9
+ # include Configurable
10
+ #
11
+ # config :one, 'one'
12
+ # config :two, 'two'
13
+ # config :three, 'three'
14
+ #
15
+ # def initialize(overrides={})
16
+ # initialize_config(overrides)
17
+ # end
18
+ # end
19
+ #
20
+ # c = ConfigClass.new
21
+ # c.config.class # => InstanceConfiguration
22
+ # c.config # => {:one => 'one', :two => 'two', :three => 'three'}
23
+ #
24
+ # The <tt>config</tt> object acts as a kind of forwarding hash; declared configurations
25
+ # map to accessors while undeclared configurations are stored internally:
26
+ #
27
+ # c.config[:one] = 'ONE'
28
+ # c.one # => 'ONE'
29
+ #
30
+ # c.one = 1
31
+ # c.config # => {:one => 1, :two => 'two', :three => 'three'}
32
+ #
33
+ # c.config[:undeclared] = 'value'
34
+ # c.config.store # => {:undeclared => 'value'}
35
+ #
36
+ # The writer method for a configuration can be modified by providing a block to config.
37
+ # The Validation module provides a number of common validation and string-transform
38
+ # blocks which can be accessed through the class method 'c':
39
+ #
40
+ # class SubClass < ConfigClass
41
+ # config(:one, 'one') {|v| v.upcase }
42
+ # config :two, 2, &c.integer
43
+ # end
44
+ #
45
+ # s = SubClass.new
46
+ # s.config # => {:one => 'ONE', :two => 2, :three => 'three'}
47
+ #
48
+ # s.one = 'aNothER'
49
+ # s.one # => 'ANOTHER'
50
+ #
51
+ # s.two = -2
52
+ # s.two # => -2
53
+ # s.two = "3"
54
+ # s.two # => 3
55
+ # s.two = nil # !> ValidationError
56
+ # s.two = 'str' # !> ValidationError
57
+ #
58
+ # As shown above, configurations are inherited from the parent and can be
59
+ # overridden in subclasses. See ConfigurableClass for more details.
60
+ #
61
+ module Configurable
62
+
63
+ # Extends including classes with ConfigurableClass
64
+ def self.included(mod)
65
+ mod.extend Support::ConfigurableClass if mod.kind_of?(Class)
66
+ end
67
+
68
+ # The instance configurations for self
69
+ attr_reader :config
70
+
71
+ # Reconfigures self with the given configuration overrides. Only
72
+ # the specified configs are modified. Override keys are symbolized.
73
+ #
74
+ # Returns self.
75
+ def reconfigure(overrides={})
76
+ keys = (config.class_config.ordered_keys + overrides.keys) & overrides.keys
77
+ keys.each do |key|
78
+ config[key.to_sym] = overrides[key]
79
+ end
80
+
81
+ self
82
+ end
83
+
84
+ # Reinitializes config with a copy of orig.config (this assures
85
+ # that duplicates have their own copy of configurations,
86
+ # separate from the original object).
87
+ def initialize_copy(orig)
88
+ super
89
+ initialize_config(orig.config)
90
+ end
91
+
92
+ protected
93
+
94
+ # Initializes config to an InstanceConfiguration specific for self.
95
+ # Default config values are assigned or overridden if specified in
96
+ # overrides. Override keys are symbolized.
97
+ def initialize_config(overrides={})
98
+ class_config = self.class.configurations
99
+ @config = class_config.instance_config
100
+
101
+ overrides.each_pair do |key, value|
102
+ config[key.to_sym] = value
103
+ end
104
+
105
+ class_config.each_pair do |key, value|
106
+ next if config.has_key?(key)
107
+ config[key] = value.default
108
+ end
109
+
110
+ config.bind(self)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,296 @@
1
+ require 'tap/support/class_configuration'
2
+ require 'tap/support/validation'
3
+ require 'tap/support/lazydoc'
4
+
5
+ module Tap
6
+ module Support
7
+ autoload(:Templater, 'tap/support/templater')
8
+
9
+ # ConfigurableClass encapsulates class methods used to declare class configurations.
10
+ # When configurations are declared using the config method, ConfigurableClass
11
+ # generates accessors in the class, much like attr_accessor.
12
+ #
13
+ # class ConfigurableClass
14
+ # extend ConfigurableClass
15
+ # config :one, 'one'
16
+ # end
17
+ #
18
+ # ConfigurableClass.configurations.to_hash # => {:one => 'one'}
19
+ #
20
+ # c = ConfigurableClass.new
21
+ # c.respond_to?('one') # => true
22
+ # c.respond_to?('one=') # => true
23
+ #
24
+ # If a block is given, the block will be used to create the writer method
25
+ # for the config. Used in this manner, config defines a <tt>config_key=</tt> method
26
+ # wherein <tt>@config_key</tt> will be set to the return value of the block.
27
+ #
28
+ # class AnotherConfigurableClass
29
+ # extend ConfigurableClass
30
+ # config(:one, 'one') {|value| value.upcase }
31
+ # end
32
+ #
33
+ # ac = AnotherConfigurableClass.new
34
+ # ac.one = 'value'
35
+ # ac.one # => 'VALUE'
36
+ #
37
+ # The block has class-context in this case. To have instance-context, use the
38
+ # config_attr method which defines the writer method using the block directly.
39
+ #
40
+ # class YetAnotherConfigurableClass
41
+ # extend ConfigurableClass
42
+ # config_attr(:one, 'one') {|value| @one = value.reverse }
43
+ # end
44
+ #
45
+ # ac = YetAnotherConfigurableClass.new
46
+ # ac.one = 'value'
47
+ # ac.one # => 'eulav'
48
+ #
49
+ module ConfigurableClass
50
+
51
+ # A ClassConfiguration holding the class configurations.
52
+ attr_reader :configurations
53
+
54
+ # The source_file for self. By default the first file
55
+ # to define the class inheriting ConfigurableClass.
56
+ attr_accessor :source_file
57
+
58
+ # Sets the source_file for base and initializes base.configurations.
59
+ def self.extended(base)
60
+ caller.each_with_index do |line, index|
61
+ case line
62
+ when /\/configurable.rb/ then next
63
+ when /^(([A-z]:)?[^:]+):(\d+)/
64
+ base.instance_variable_set(:@source_file, File.expand_path($1))
65
+ break
66
+ end
67
+ end
68
+
69
+ base.instance_variable_set(:@configurations, ClassConfiguration.new(base))
70
+ end
71
+
72
+ # When subclassed, the parent.configurations are duplicated and passed to
73
+ # the child class where they can be extended/modified without affecting
74
+ # the configurations of the parent class.
75
+ def inherited(child)
76
+ unless child.instance_variable_defined?(:@source_file)
77
+ caller.first =~ /^(([A-z]:)?[^:]+):(\d+)/
78
+ child.instance_variable_set(:@source_file, File.expand_path($1))
79
+ end
80
+
81
+ child.instance_variable_set(:@configurations, ClassConfiguration.new(child, @configurations))
82
+ super
83
+ end
84
+
85
+ # Returns the lazydoc for source_file
86
+ def lazydoc(resolve=false)
87
+ Lazydoc.resolve(configurations.code_comments) if resolve
88
+ Lazydoc[source_file]
89
+ end
90
+
91
+ # Loads the contents of path as YAML. Returns an empty hash if the path
92
+ # is empty, does not exist, or is not a file.
93
+ def load_config(path)
94
+ return {} if path == nil || !File.exists?(path) || File.directory?(path)
95
+
96
+ YAML.load_file(path) || {}
97
+ end
98
+
99
+ protected
100
+
101
+ # Declares a class configuration and generates the associated accessors.
102
+ # If a block is given, the <tt>key=</tt> method will set <tt>@key</tt>
103
+ # to the return of the block, which executes in class-context.
104
+ # Configurations are inherited, and can be overridden in subclasses.
105
+ #
106
+ # class SampleClass
107
+ # include Tap::Support::Configurable
108
+ #
109
+ # config :str, 'value'
110
+ # config(:upcase, 'value') {|input| input.upcase }
111
+ # end
112
+ #
113
+ # # An equivalent class to illustrate class-context
114
+ # class EquivalentClass
115
+ # attr_accessor :str
116
+ # attr_reader :upcase
117
+ #
118
+ # UPCASE_BLOCK = lambda {|input| input.upcase }
119
+ #
120
+ # def upcase=(input)
121
+ # @upcase = UPCASE_BLOCK.call(input)
122
+ # end
123
+ # end
124
+ #
125
+ def config(key, value=nil, options={}, &block)
126
+ if block_given?
127
+ # add arg_type implied by block, if necessary
128
+ options[:arg_type] = arg_type(block) if options[:arg_type] == nil
129
+ options[:arg_name] = arg_name(block) if options[:arg_name] == nil
130
+
131
+ instance_variable = "@#{key}".to_sym
132
+ config_attr(key, value, options) do |input|
133
+ instance_variable_set(instance_variable, block.call(input))
134
+ end
135
+ else
136
+ config_attr(key, value, options)
137
+ end
138
+ end
139
+
140
+ # Declares a class configuration and generates the associated accessors.
141
+ # If a block is given, the <tt>key=</tt> method will perform the block with
142
+ # instance-context. Configurations are inherited, and can be overridden
143
+ # in subclasses.
144
+ #
145
+ # class SampleClass
146
+ # include Tap::Support::Configurable
147
+ #
148
+ # def initialize
149
+ # initialize_config
150
+ # end
151
+ #
152
+ # config_attr :str, 'value'
153
+ # config_attr(:upcase, 'value') {|input| @upcase = input.upcase }
154
+ # end
155
+ #
156
+ # # An equivalent class to illustrate instance-context
157
+ # class EquivalentClass
158
+ # attr_accessor :str
159
+ # attr_reader :upcase
160
+ #
161
+ # def upcase=(input)
162
+ # @upcase = input.upcase
163
+ # end
164
+ # end
165
+ #
166
+ # Instances of a Configurable class may set configurations through config.
167
+ # The config object is an InstanceConfiguration which forwards read/write
168
+ # operations to the configuration accessors. For example:
169
+ #
170
+ # s = SampleClass.new
171
+ # s.config.class # => Tap::Support::InstanceConfiguration
172
+ # s.str # => 'value'
173
+ # s.config[:str] # => 'value'
174
+ #
175
+ # s.str = 'one'
176
+ # s.config[:str] # => 'one'
177
+ #
178
+ # s.config[:str] = 'two'
179
+ # s.str # => 'two'
180
+ #
181
+ # Alternative reader and writer methods may be specified as an option;
182
+ # in this case config_attr assumes the methods are declared elsewhere
183
+ # and will not define the associated accessors.
184
+ #
185
+ # class AlternativeClass
186
+ # include Tap::Support::Configurable
187
+ #
188
+ # config_attr :sym, 'value', :reader => :get_sym, :writer => :set_sym
189
+ #
190
+ # def initialize
191
+ # initialize_config
192
+ # end
193
+ #
194
+ # def get_sym
195
+ # @sym
196
+ # end
197
+ #
198
+ # def set_sym(input)
199
+ # @sym = input.to_sym
200
+ # end
201
+ # end
202
+ #
203
+ # alt = AlternativeClass.new
204
+ # alt.respond_to?(:sym) # => false
205
+ # alt.respond_to?(:sym=) # => false
206
+ #
207
+ # alt.config[:sym] = 'one'
208
+ # alt.get_sym # => :one
209
+ #
210
+ # alt.set_sym('two')
211
+ # alt.config[:sym] # => :two
212
+ #
213
+ # Idiosyncratically, true, false, and nil may also be provided as
214
+ # reader/writer options. Specifying true is the same as using the
215
+ # default. Specifying false or nil prevents config_attr from
216
+ # defining accessors, but the configuration still expects to use
217
+ # the default reader/writer methods (ie <tt>key</tt> and <tt>key=</tt>)
218
+ # which must be defined elsewhere.
219
+ def config_attr(key, value=nil, options={}, &block)
220
+
221
+ # add arg_type implied by block, if necessary
222
+ options[:arg_type] = arg_type(block) if block_given? && options[:arg_type] == nil
223
+ options[:arg_name] = arg_name(block) if block_given? && options[:arg_name] == nil
224
+
225
+ # define the default public reader method
226
+ if !options.has_key?(:reader) || options[:reader] == true
227
+ attr_reader(key)
228
+ public key
229
+ end
230
+
231
+ # define the public writer method
232
+ case
233
+ when options.has_key?(:writer) && options[:writer] != true
234
+ raise ArgumentError.new("block may not be specified with writer") if block_given?
235
+ when block_given?
236
+ define_method("#{key}=", &block)
237
+ public "#{key}="
238
+ else
239
+ attr_writer(key)
240
+ public "#{key}="
241
+ end
242
+
243
+ # remove any true, false, nil reader/writer declarations...
244
+ # implicitly reverting the option to the default reader
245
+ # and writer methods
246
+ [:reader, :writer].each do |option|
247
+ case options[option]
248
+ when true, false, nil then options.delete(option)
249
+ end
250
+ end
251
+
252
+ # register with TDoc so that all extra documentation can be extracted
253
+ caller.each_with_index do |line, index|
254
+ case line
255
+ when /in .config.$/ then next
256
+ when /^(([A-z]:)?[^:]+):(\d+)/
257
+ options[:desc] = Lazydoc.register($1, $3.to_i - 1)
258
+ break
259
+ end
260
+ end if options[:desc] == nil
261
+
262
+ configurations.add(key, value, options)
263
+ end
264
+
265
+ # Alias for Tap::Support::Validation
266
+ def c
267
+ Validation
268
+ end
269
+
270
+ private
271
+
272
+ # Returns special argument types for standard validation
273
+ # blocks, such as switch (Validation::SWITCH) and list
274
+ # (Validation::LIST).
275
+ def arg_type(block) # :nodoc:
276
+ case block
277
+ when Validation::SWITCH then :switch
278
+ when Validation::FLAG then :flag
279
+ when Validation::LIST then :list
280
+ else nil
281
+ end
282
+ end
283
+
284
+ # Returns special argument names for standard validation
285
+ # blocks, such as switch (Validation::ARRAY) and list
286
+ # (Validation::HASH).
287
+ def arg_name(block) # :nodoc:
288
+ case block
289
+ when Validation::ARRAY then "'[a, b, c]'"
290
+ when Validation::HASH then "'{one: 1, two: 2}'"
291
+ else nil
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end