tap 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. data/Basic Overview +151 -0
  2. data/Command Reference +99 -0
  3. data/History +24 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README +29 -57
  6. data/Rakefile +30 -37
  7. data/Tutorial +243 -191
  8. data/bin/tap +66 -35
  9. data/lib/tap.rb +47 -29
  10. data/lib/tap/app.rb +700 -342
  11. data/lib/tap/{script → cmd}/console.rb +0 -0
  12. data/lib/tap/{script → cmd}/destroy.rb +0 -0
  13. data/lib/tap/{script → cmd}/generate.rb +0 -0
  14. data/lib/tap/cmd/run.rb +156 -0
  15. data/lib/tap/constants.rb +4 -0
  16. data/lib/tap/dump.rb +57 -0
  17. data/lib/tap/env.rb +316 -0
  18. data/lib/tap/file_task.rb +106 -109
  19. data/lib/tap/generator.rb +4 -1
  20. data/lib/tap/generator/generators/command/USAGE +6 -0
  21. data/lib/tap/generator/generators/command/command_generator.rb +17 -0
  22. data/lib/tap/generator/generators/{script/templates/script.erb → command/templates/command.erb} +10 -10
  23. data/lib/tap/generator/generators/config/USAGE +21 -0
  24. data/lib/tap/generator/generators/config/config_generator.rb +17 -7
  25. data/lib/tap/generator/generators/file_task/USAGE +3 -0
  26. data/lib/tap/generator/generators/file_task/file_task_generator.rb +16 -0
  27. data/lib/tap/generator/generators/file_task/templates/file.txt +2 -0
  28. data/lib/tap/generator/generators/file_task/templates/file.yml +3 -0
  29. data/lib/tap/generator/generators/file_task/templates/task.erb +26 -20
  30. data/lib/tap/generator/generators/file_task/templates/test.erb +20 -10
  31. data/lib/tap/generator/generators/generator/generator_generator.rb +1 -1
  32. data/lib/tap/generator/generators/generator/templates/generator.erb +21 -12
  33. data/lib/tap/generator/generators/root/templates/Rakefile +33 -24
  34. data/lib/tap/generator/generators/root/templates/tap.yml +28 -31
  35. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +1 -0
  36. data/lib/tap/generator/generators/task/USAGE +3 -0
  37. data/lib/tap/generator/generators/task/task_generator.rb +18 -5
  38. data/lib/tap/generator/generators/task/templates/task.erb +7 -12
  39. data/lib/tap/generator/generators/task/templates/test.erb +10 -11
  40. data/lib/tap/generator/generators/workflow/templates/task.erb +1 -1
  41. data/lib/tap/generator/generators/workflow/templates/test.erb +1 -1
  42. data/lib/tap/patches/rake/rake_test_loader.rb +8 -0
  43. data/lib/tap/patches/rake/testtask.rb +55 -0
  44. data/lib/tap/patches/ruby19/backtrace_filter.rb +51 -0
  45. data/lib/tap/patches/ruby19/parsedate.rb +16 -0
  46. data/lib/tap/root.rb +172 -67
  47. data/lib/tap/script.rb +70 -336
  48. data/lib/tap/support/aggregator.rb +55 -0
  49. data/lib/tap/support/audit.rb +281 -280
  50. data/lib/tap/support/batchable.rb +59 -0
  51. data/lib/tap/support/class_configuration.rb +279 -0
  52. data/lib/tap/support/configurable.rb +92 -0
  53. data/lib/tap/support/configurable_methods.rb +296 -0
  54. data/lib/tap/support/executable.rb +98 -0
  55. data/lib/tap/support/executable_queue.rb +82 -0
  56. data/lib/tap/support/logger.rb +9 -15
  57. data/lib/tap/support/rake.rb +43 -54
  58. data/lib/tap/support/run_error.rb +32 -13
  59. data/lib/tap/support/shell_utils.rb +47 -0
  60. data/lib/tap/support/tdoc.rb +9 -8
  61. data/lib/tap/support/tdoc/config_attr.rb +40 -16
  62. data/lib/tap/support/validation.rb +77 -0
  63. data/lib/tap/support/versions.rb +36 -36
  64. data/lib/tap/task.rb +276 -482
  65. data/lib/tap/test.rb +20 -261
  66. data/lib/tap/test/env_vars.rb +7 -5
  67. data/lib/tap/test/file_methods.rb +126 -121
  68. data/lib/tap/test/subset_methods.rb +86 -45
  69. data/lib/tap/test/tap_methods.rb +271 -0
  70. data/lib/tap/workflow.rb +174 -46
  71. data/test/app/config/another/task.yml +1 -0
  72. data/test/app/config/erb.yml +2 -1
  73. data/test/app/config/some/task.yml +1 -0
  74. data/test/app/config/template.yml +2 -6
  75. data/test/app_test.rb +1241 -1008
  76. data/test/env/test_configure/recurse_a.yml +2 -0
  77. data/test/env/test_configure/recurse_b.yml +2 -0
  78. data/test/env/test_configure/tap.yml +23 -0
  79. data/test/env/test_load_env_config/dir/tap.yml +3 -0
  80. data/test/env/test_load_env_config/recurse_a.yml +2 -0
  81. data/test/env/test_load_env_config/recurse_b.yml +2 -0
  82. data/test/env/test_load_env_config/tap.yml +3 -0
  83. data/test/env_test.rb +198 -0
  84. data/test/file_task_test.rb +70 -53
  85. data/{lib/tap/generator/generators/package/USAGE → test/root/file.txt} +0 -0
  86. data/test/root_test.rb +621 -454
  87. data/test/script_test.rb +38 -174
  88. data/test/support/aggregator_test.rb +99 -0
  89. data/test/support/audit_test.rb +409 -416
  90. data/test/support/batchable_test.rb +74 -0
  91. data/test/support/{task_configuration_test.rb → class_configuration_test.rb} +106 -47
  92. data/test/{task/config/overriding.yml → support/configurable/config/configured.yml} +0 -0
  93. data/test/support/configurable_test.rb +295 -0
  94. data/test/support/executable_queue_test.rb +103 -0
  95. data/test/support/executable_test.rb +38 -0
  96. data/test/support/logger_test.rb +17 -17
  97. data/test/support/rake_test.rb +4 -2
  98. data/test/support/shell_utils_test.rb +24 -0
  99. data/test/support/tdoc_test.rb +265 -258
  100. data/test/support/validation_test.rb +54 -0
  101. data/test/support/versions_test.rb +38 -38
  102. data/test/tap_test_helper.rb +19 -5
  103. data/test/tap_test_suite.rb +5 -2
  104. data/test/task_base_test.rb +13 -104
  105. data/test/task_syntax_test.rb +300 -0
  106. data/test/task_test.rb +258 -381
  107. data/test/test/env_vars_test.rb +40 -40
  108. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/one.txt +0 -0
  109. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/expected/two.txt +0 -0
  110. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/one.txt +0 -0
  111. data/test/test/file_methods/{test_assert_output_files_equal → test_assert_files}/input/two.txt +0 -0
  112. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/one.txt +0 -0
  113. data/test/test/{test_file_task_test → file_methods/test_assert_files_can_have_no_expected_files_if_specified}/input/two.txt +0 -0
  114. data/test/test/file_methods/test_assert_files_fails_for_different_content/expected/one.txt +1 -0
  115. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_different_content}/expected/two.txt +0 -0
  116. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/one.txt +1 -0
  117. data/test/test/file_methods/test_assert_files_fails_for_different_content/input/two.txt +1 -0
  118. data/test/test/{test_file_task_test → file_methods/test_assert_files_fails_for_missing_expected_file}/expected/one.txt +0 -0
  119. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/one.txt +1 -0
  120. data/test/test/file_methods/test_assert_files_fails_for_missing_expected_file/input/two.txt +1 -0
  121. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/one.txt +1 -0
  122. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/expected/two.txt +1 -0
  123. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/one.txt +1 -0
  124. data/test/test/file_methods/test_assert_files_fails_for_missing_output_file/input/two.txt +1 -0
  125. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/one.txt +1 -0
  126. data/test/test/file_methods/test_assert_files_fails_for_no_expected_files/input/two.txt +1 -0
  127. data/test/test/file_methods_doc/test_sub/expected/one.txt +1 -0
  128. data/test/test/file_methods_doc/test_sub/expected/two.txt +1 -0
  129. data/test/test/file_methods_doc/test_sub/input/one.txt +1 -0
  130. data/test/test/file_methods_doc/test_sub/input/two.txt +1 -0
  131. data/test/test/file_methods_doc_test.rb +29 -0
  132. data/test/test/file_methods_test.rb +214 -143
  133. data/test/test/subset_methods_test.rb +111 -115
  134. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/a.txt +0 -0
  135. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/expected/task/name/b.txt +0 -0
  136. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/a.txt +0 -0
  137. data/test/test/{test_assert_expected_result_files → tap_methods/test_assert_files}/input/b.txt +0 -0
  138. data/test/test/tap_methods_test.rb +399 -0
  139. data/test/workflow_test.rb +101 -91
  140. metadata +86 -70
  141. data/lib/tap/generator/generators/package/package_generator.rb +0 -38
  142. data/lib/tap/generator/generators/package/templates/package.erb +0 -186
  143. data/lib/tap/generator/generators/script/USAGE +0 -0
  144. data/lib/tap/generator/generators/script/script_generator.rb +0 -17
  145. data/lib/tap/script/run.rb +0 -154
  146. data/lib/tap/support/batch_queue.rb +0 -162
  147. data/lib/tap/support/combinator.rb +0 -114
  148. data/lib/tap/support/task_configuration.rb +0 -169
  149. data/lib/tap/support/template.rb +0 -81
  150. data/lib/tap/support/templater.rb +0 -155
  151. data/lib/tap/version.rb +0 -4
  152. data/test/app/config/addition_template.yml +0 -6
  153. data/test/app_class_test.rb +0 -33
  154. data/test/check/binding_eval.rb +0 -23
  155. data/test/check/define_method_check.rb +0 -22
  156. data/test/check/dependencies_check.rb +0 -175
  157. data/test/check/inheritance_check.rb +0 -22
  158. data/test/support/batch_queue_test.rb +0 -320
  159. data/test/support/combinator_test.rb +0 -249
  160. data/test/support/template_test.rb +0 -122
  161. data/test/support/templater/erb.txt +0 -2
  162. data/test/support/templater/erb.yml +0 -2
  163. data/test/support/templater/somefile.txt +0 -2
  164. data/test/support/templater_test.rb +0 -192
  165. data/test/task/config/template.yml +0 -4
  166. data/test/task_class_test.rb +0 -170
  167. data/test/task_execute_test.rb +0 -262
  168. data/test/test/file_methods/test_assert_expected/expected/file.txt +0 -1
  169. data/test/test/file_methods/test_assert_expected/expected/folder/file.txt +0 -1
  170. data/test/test/file_methods/test_assert_expected/input/file.txt +0 -1
  171. data/test/test/file_methods/test_assert_expected/input/folder/file.txt +0 -1
  172. data/test/test/file_methods/test_assert_files_exist/input/input_1.txt +0 -0
  173. data/test/test/file_methods/test_assert_files_exist/input/input_2.txt +0 -0
  174. data/test/test/file_methods/test_file_compare/expected/output_1.txt +0 -3
  175. data/test/test/file_methods/test_file_compare/expected/output_2.txt +0 -1
  176. data/test/test/file_methods/test_file_compare/input/input_1.txt +0 -3
  177. data/test/test/file_methods/test_file_compare/input/input_2.txt +0 -3
  178. data/test/test/file_methods/test_infer_glob/expected/file.yml +0 -0
  179. data/test/test/file_methods/test_infer_glob/expected/file_1.txt +0 -0
  180. data/test/test/file_methods/test_infer_glob/expected/file_2.txt +0 -0
  181. data/test/test/file_methods/test_yml_compare/expected/output_1.yml +0 -6
  182. data/test/test/file_methods/test_yml_compare/expected/output_2.yml +0 -6
  183. data/test/test/file_methods/test_yml_compare/input/input_1.yml +0 -4
  184. data/test/test/file_methods/test_yml_compare/input/input_2.yml +0 -4
  185. data/test/test_test.rb +0 -373
@@ -0,0 +1,59 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Batchable encapsulates the methods used to support batching
5
+ # of tasks. See the 'Batches' section in the Tap::Task
6
+ # documentation for more details on how Batchable works in
7
+ # practice.
8
+ module Batchable
9
+
10
+ # Merges the batches for the specified objects. All objects
11
+ # sharing the individual object batches will be affected, even
12
+ # if they are not listed explicitly as an input.
13
+ #
14
+ # t1 = Tap::Task.new
15
+ # t2 = Tap::Task.new
16
+ # t3 = t2.initialize_batch_obj
17
+ #
18
+ # Batchable.batch(t1, t2)
19
+ # t3.batch # => [t1,t2,t3]
20
+ #
21
+ # Returns the new batch.
22
+ def self.batch(*tasks)
23
+ merged = []
24
+ batches = tasks.collect {|task| task.batch }.uniq
25
+ batches.each do |batch|
26
+ merged.concat(batch)
27
+ batch.clear
28
+ end
29
+ merged.uniq!
30
+ batches.each {|batch| batch.concat(merged) }
31
+ merged
32
+ end
33
+
34
+ # The object batch. (must be initializd by classes that
35
+ # include Batchable)
36
+ attr_reader :batch
37
+
38
+ # Returns true if the batch size is greater than one
39
+ # (the one being self).
40
+ def batched?
41
+ batch.length > 1
42
+ end
43
+
44
+ # Returns the index of the self in batch.
45
+ def batch_index
46
+ batch.index(self)
47
+ end
48
+
49
+ # Initializes a new batch object and adds the object to batch.
50
+ # The object will be self if batch is empty (ie for the first
51
+ # call) or a duplicate of self if not.
52
+ def initialize_batch_obj
53
+ obj = (batch.empty? ? self : self.dup)
54
+ batch << obj
55
+ obj
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,279 @@
1
+ autoload(:GetOptLong, 'getoptlong')
2
+
3
+ module Tap
4
+ module Support
5
+ # == UNDER CONSTRUCTION
6
+ #--
7
+ #
8
+ # ClassConfiguration holds the class configurations defined in a Tap::Task.
9
+ # The configurations are stored as an array of declarations_array like:
10
+ # [name, default, msg, declaration_class]. In addition, ClassConfiguration
11
+ # collapse the array of declarations_array into a hash, which acts as the default
12
+ # task configuration.
13
+ #
14
+ # Storing metadata about the configurations, such as the declaration_class,
15
+ # allows the creation of more user-friendly configuration files and facilitates
16
+ # incorporation into command-line applications.
17
+ #
18
+ # In general, users will not have to interact with ClassConfigurations directly.
19
+ #
20
+ # === Example
21
+ #
22
+ # class BaseTask < Tap::Configurable
23
+ # class_configurations [:one, 1]
24
+ # end
25
+ #
26
+ # BaseTask.configurations.hash # => {:one => 1}
27
+ #
28
+ # class SubTask < BaseTask
29
+ # class_configurations(
30
+ # [:one, 'one', "the first configuration"],
31
+ # [:two, 'two', "the second configuration"])
32
+ # end
33
+ #
34
+ # SubTask.configurations.hash # => {:one => 'one', :two => 'two'}
35
+ #
36
+ # Now you can see how the comments and declaring classes get used in the
37
+ # configuration files. Note that configuration keys are stringified
38
+ # for clarity (this is ok -- they will be symbolized when loaded by a
39
+ # task).
40
+ #
41
+ # [BaseTask.configurations.format_yaml]
42
+ # # BaseTask configuration
43
+ # one: 1
44
+ #
45
+ # [SubTask.configurations.format_yaml]
46
+ # # BaseTask configuration
47
+ # one: one # the first configuration
48
+ #
49
+ # # SubTask configuration
50
+ # two: two # the second configuration
51
+ #
52
+ #--
53
+ # TODO -
54
+ # Revisit config formatting... right now it's a bit jacked.
55
+ #++
56
+ class ClassConfiguration
57
+ include Enumerable
58
+
59
+ # The class receiving the configurations
60
+ attr_reader :receiver
61
+
62
+ # An array of [receiver, configuration keys] arrays tracking
63
+ # the order in which configurations were declared across all
64
+ # receivers
65
+ attr_reader :declarations_array
66
+
67
+ # An array of configuration keys declared by self
68
+ attr_reader :declarations
69
+
70
+ # A hash of the unprocessed default values
71
+ attr_reader :unprocessed_default
72
+
73
+ # A hash of the processed default values
74
+ attr_reader :default
75
+
76
+ # A hash of the processing blocks
77
+ attr_reader :process_blocks
78
+
79
+ # A placeholder to indicate when no value
80
+ # was specified during a call to add.
81
+ NO_VALUE = Object.new
82
+
83
+ def initialize(receiver, parent=nil)
84
+ @receiver = receiver
85
+ @default = parent != nil ? parent.default.dup : {}
86
+ @unprocessed_default = parent != nil ? parent.unprocessed_default.dup : {}
87
+ @process_blocks = parent != nil ? parent.process_blocks.dup : {}
88
+
89
+ # use same declarations array? freeze declarations to ensure order?
90
+ # definitely falls out of order if parents are modfied after initialization
91
+ @declarations_array = parent != nil ? parent.declarations_array.dup : []
92
+ @declarations = []
93
+ declarations_array << [receiver, @declarations]
94
+ end
95
+
96
+ # Returns true if the key has been declared by some receiver.
97
+ # Note this is distinct from whether or not a particular config
98
+ # is currently in the default hash.
99
+ def declared?(key)
100
+ key = key.to_sym
101
+ declarations_array.each do |r,array|
102
+ return true if array.include?(key)
103
+ end
104
+ false
105
+ end
106
+
107
+ # Returns the class (receiver) that first added the config
108
+ # indicated by key.
109
+ def declaration_class(key)
110
+ key = key.to_sym
111
+ declarations_array.each do |r,array|
112
+ return r if array.include?(key)
113
+ end
114
+ nil
115
+ end
116
+
117
+ # Returns the configurations first declared by the specified receiver.
118
+ def declarations_for(receiver)
119
+ declarations_array.each do |r,array|
120
+ return array if r == receiver
121
+ end
122
+ nil
123
+ end
124
+
125
+ # Adds a configuration. If specified, the value is processed
126
+ # by the process_block and recorded adefault
127
+ #
128
+ # The existing value and process_block for the
129
+ # configuration will not be overwritten unless specified. However,
130
+ # if a configuration is added without specifying a value and no previous
131
+ # default value exists, the default and unprocessed_default for the
132
+ # configuration will be set to nil.
133
+ #
134
+ #--
135
+ # Note -- existing blocks are NOT overwritten unless a new block is provided.
136
+ # This allows overide of the default value in subclasses while preserving the
137
+ # validation/processing code.
138
+ def add(key, value=NO_VALUE, &process_block)
139
+ key = key.to_sym
140
+ value = unprocessed_default[key] if value == NO_VALUE
141
+
142
+ declarations << key unless declared?(key)
143
+ process_blocks[key] = process_block if block_given?
144
+ unprocessed_default[key] = value
145
+ default[key] = process(key, value)
146
+
147
+ self
148
+ end
149
+
150
+ def remove(key)
151
+ key = key.to_sym
152
+
153
+ process_blocks.delete(key)
154
+ unprocessed_default.delete(key)
155
+ default.delete(key)
156
+
157
+ self
158
+ end
159
+
160
+ def merge(another)
161
+ # check for conflicts
162
+ another.each do |receiver, key|
163
+ dc = declaration_class(key)
164
+ next if dc == nil || dc == receiver
165
+
166
+ raise "configuration conflict: #{key} (#{receiver}) already declared by #{dc}"
167
+ end
168
+
169
+ # add the new configurations
170
+ another.each do |receiver, key|
171
+ # preserve the declarations for receiver
172
+ unless declarations = declarations_for(receiver)
173
+ declarations = []
174
+ declarations_array << [receiver, declarations]
175
+ end
176
+ unless declarations.include?(key)
177
+ declarations << key
178
+ yield(key) if block_given?
179
+ end
180
+
181
+ add(key, another.unprocessed_default[key], &another.process_blocks[key])
182
+ end
183
+ end
184
+
185
+ def each # :yields: receiver, key
186
+ declarations_array.each do |receiver, keys|
187
+ keys.each {|key| yield(receiver, key) }
188
+ end
189
+ end
190
+
191
+ # Sends value to the process block identified by key and returns the result.
192
+ # Returns value if no process block has been set for key.
193
+ def process(key, value)
194
+ block = process_blocks[key.to_sym]
195
+ block ? block.call(value) : value
196
+ end
197
+
198
+ # Nicely formats the configurations into yaml with messages and
199
+ # declaration class divisions.
200
+ def format_yaml
201
+ lines = []
202
+ declarations_array.each do |receiver, keys|
203
+
204
+ # do not consider keys that have been removed
205
+ keys = keys.delete_if {|key| !self.default.has_key?(key) }
206
+ next if keys.empty?
207
+
208
+ lines << "# #{receiver} configuration#{keys.length > 1 ? 's' : ''}"
209
+
210
+ class_doc = Tap::Support::TDoc[receiver]
211
+ configurations = (class_doc == nil ? [] : class_doc.configurations)
212
+ keys.each do |key|
213
+ tdoc_config = configurations.find {|config| config.name == key.to_s }
214
+
215
+ # yaml adds a header and a final newline which should be removed:
216
+ # {'key' => 'value'}.to_yaml # => "--- \nkey: value\n"
217
+ # {'key' => 'value'}.to_yaml[5...-1] # => "key: value"
218
+ yaml = {key.to_s => unprocessed_default[key]}.to_yaml[5...-1]
219
+ message = tdoc_config ? tdoc_config.comment : ""
220
+
221
+ lines << case
222
+ when message == nil || message.empty?
223
+ # if there is no message, simply add the yaml
224
+ yaml
225
+ when yaml !~ /\r?\n/ && message !~ /\r?\n/ && yaml.length < 25 && message.length < 30
226
+ # shorthand ONLY if the config and message can be expressed in a single line
227
+ message = message.gsub(/^#\s*/, "")
228
+ "%-25s # %s" % [yaml, message]
229
+ else
230
+ lines << ""
231
+ # comment out new lines and add the message
232
+ message.split(/\n/).each do |msg|
233
+ lines << "# #{msg.strip.gsub(/^#\s*/, '')}"
234
+ end
235
+ yaml
236
+ end
237
+ end
238
+
239
+ # add a spacer line
240
+ lines << ""
241
+ end
242
+
243
+ lines.compact.join("\n")
244
+ end
245
+
246
+ def opt_map(long_option)
247
+ raise ArgumentError.new("not a long option: #{long_option}") unless long_option =~ /^--(.*)$/
248
+ long = $1
249
+
250
+ each do |receiver, key|
251
+ return key if long == key.to_s
252
+ end
253
+ nil
254
+ end
255
+
256
+ def to_opts
257
+ collect do |receiver, key|
258
+ # Note the receiver is used as a placeholder for desc,
259
+ # to be resolved using TDoc.
260
+ attributes = {
261
+ :long => key,
262
+ :short => nil,
263
+ :opt_type => GetoptLong::REQUIRED_ARGUMENT,
264
+ :desc => receiver
265
+ }
266
+
267
+ long = attributes[:long]
268
+ attributes[:long] = "--#{long}" unless long =~ /^-{2}/
269
+
270
+ short = attributes[:short].to_s
271
+ attributes[:short] = "-#{short}" unless short.empty? || short =~ /^-/
272
+
273
+ [attributes[:long], attributes[:short], attributes[:opt_type], attributes[:desc]]
274
+ end
275
+ end
276
+
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,92 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Configurable encapsulates all configuration-related methods used
5
+ # by Tasks. When Configurable is included in a class, the class itself
6
+ # is extended with Tap::Support::ConfigurableMethods, such that configs
7
+ # can be declared within the class definition.
8
+ #
9
+ # class ConfigurableClass
10
+ # include Configurable
11
+ #
12
+ # config :one, 'one'
13
+ # config :two, 'two'
14
+ # config :three, 'three'
15
+ # end
16
+ #
17
+ # ConfigurableClass.new.config # => {:one => 'one', :two => 'two', :three => 'three'}
18
+ #
19
+ # See the 'Configuration' section in the Tap::Task documentation for
20
+ # more details on how Configurable works in practice.
21
+ module Configurable
22
+ include Batchable
23
+
24
+ def self.included(mod)
25
+ mod.extend Support::ConfigurableMethods
26
+ mod.instance_variable_set(:@configurations, Support::ClassConfiguration.new(mod))
27
+ mod.instance_variable_set(:@source_files, [])
28
+ end
29
+
30
+ # The application used to load config_file templates
31
+ # (and hence, to initialize batched objects).
32
+ attr_reader :app
33
+
34
+ # The name used to determine config_file, via
35
+ # app.config_filepath(name).
36
+ attr_reader :name
37
+
38
+ # The config file used to load config templates.
39
+ attr_reader :config_file
40
+
41
+ # A configuration hash.
42
+ attr_reader :config
43
+
44
+ # Initializes a new Configurable and associated batch objects. Batch
45
+ # objects will be initialized for each configuration template specified
46
+ # in config_file, where config_file = app.config_filepath(name).
47
+ def initialize(name=nil, config={}, app=App.instance)
48
+ @app = app
49
+ @batch = []
50
+ @config_file = app.config_filepath(name)
51
+
52
+ config.symbolize_keys! unless config.empty?
53
+ app.each_config_template(config_file) do |template|
54
+ template_config = template.empty? ? config : template.symbolize_keys.merge(config)
55
+ initialize_batch_obj(name, template_config)
56
+ end
57
+ end
58
+
59
+ # Sets config with the given configuration overrides, merged with the class
60
+ # default configuration. Configurations are symbolized before they are merged,
61
+ # and validated as specified in the config declarations.
62
+ def config=(overrides)
63
+ @config = self.class.configurations.default.dup
64
+ overrides.each_pair {|key, value| set_config(key.to_sym, value) }
65
+ self.config
66
+ end
67
+
68
+ # Creates a new batched object and adds the object to batch. The batched object
69
+ # will be a duplicate of the current object but with a new name and/or
70
+ # configurations.
71
+ def initialize_batch_obj(name=nil, config={})
72
+ obj = super()
73
+
74
+ obj.name = name.nil? ? self.class.default_name : name
75
+ obj.config = config
76
+
77
+ obj
78
+ end
79
+
80
+ protected
81
+
82
+ attr_writer :name
83
+
84
+ # Sets the specified configuration, processing the input value using
85
+ # the block specified in the config declaration. The input key should
86
+ # be symbolized.
87
+ def set_config(key, value)
88
+ config[key] = self.class.configurations.process(key, value)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,296 @@
1
+ module Tap
2
+ module Support
3
+ autoload(:TDoc, 'tap/support/tdoc')
4
+
5
+ # ConfigurableMethods encapsulates all class methods used to declare
6
+ # configurations in Tasks. ConfigurableMethods extends classes that
7
+ # include Tap::Support::Configurable.
8
+ #
9
+ # class ConfigurableClass
10
+ # include Configurable
11
+ #
12
+ # config :one, 'one'
13
+ # config :two, 'two'
14
+ # config :three, 'three'
15
+ # end
16
+ #
17
+ # ConfigurableClass.new.config # => {:one => 'one', :two => 'two', :three => 'three'}
18
+ #
19
+ # See the 'Configuration' section in the Tap::Task documentation for
20
+ # more details on how Configurable works in practice.
21
+ module ConfigurableMethods
22
+
23
+ # A Tap::Support::ClassConfiguration holding the class configurations.
24
+ attr_reader :configurations
25
+
26
+ # When subclassed, the configurations are duplicated and passed to
27
+ # the child class where they can be extended/modified without affecting
28
+ # the configurations of the parent class.
29
+ def inherited(child)
30
+ super
31
+ child.instance_variable_set(:@configurations, ClassConfiguration.new(child, @configurations))
32
+ child.instance_variable_set(:@source_files, source_files.dup)
33
+ end
34
+
35
+ # EXPERIMENTAL
36
+ attr_reader :source_files # :nodoc:
37
+
38
+ # EXPERIMENTAL
39
+ # Identifies source files for TDoc documentation.
40
+ def source_file(arg) # :nodoc:
41
+ source_files << arg
42
+ end
43
+
44
+ # Declares a configuration without any accessors.
45
+ #
46
+ # With no keys specified, sets config to make no
47
+ # accessors for each new configuration.
48
+ def declare_config(*keys)
49
+ if keys.empty?
50
+ self.config_mode = :none
51
+ else
52
+ keys.each {|key| configurations.add(key)}
53
+ end
54
+ end
55
+
56
+ # Creates a configuration writer for the input keys. Works like
57
+ # attr_writer, except the value is written to config, rather than
58
+ # a local variable. In addition, the config will be validated
59
+ # using validate_config upon setting the value.
60
+ #
61
+ # With no keys specified, sets config to create config_writer
62
+ # for each new configuration.
63
+ def config_writer(*keys)
64
+ if keys.empty?
65
+ self.config_mode = :config_writer
66
+ else
67
+ keys.each do |key|
68
+ configurations.add(key)
69
+ define_config_writer(key)
70
+ end
71
+ end
72
+ end
73
+
74
+ # Creates a configuration reader for the input keys. Works like
75
+ # attr_reader, except the value is read from config, rather than
76
+ # a local variable.
77
+ #
78
+ # With no keys specified, sets config to create a config_reader
79
+ # for each new configuration.
80
+ def config_reader(*keys)
81
+ if keys.empty?
82
+ self.config_mode = :config_reader
83
+ else
84
+ keys.each do |key|
85
+ configurations.add(key)
86
+ define_config_reader(key)
87
+ end
88
+ end
89
+ end
90
+
91
+ # Creates configuration accessors for the input keys. Works like
92
+ # attr_accessor, except the value is read from and written to config,
93
+ # rather than a local variable.
94
+ #
95
+ # With no keys specified, sets config to create a config_accessor
96
+ # for each new configuration.
97
+ def config_accessor(*keys)
98
+ if keys.empty?
99
+ self.config_mode = :config_accessor
100
+ else
101
+ keys.each do |key|
102
+ configurations.add(key)
103
+ define_config_reader(key)
104
+ define_config_writer(key)
105
+ end
106
+ end
107
+ end
108
+
109
+ # Sets a class configuration. Configurations are inherited, but can
110
+ # be overridden or added in subclasses. Accessors are created by
111
+ # default, but this behavior can be modified by use of the other
112
+ # config methods.
113
+ #
114
+ # class SampleClass
115
+ # include Configurable
116
+ #
117
+ # config :key, 'value'
118
+ # config_reader
119
+ # config :reader_only
120
+ # end
121
+ #
122
+ # t = SampleClass.new
123
+ # t.respond_to?(:reader_only) # => true
124
+ # t.respond_to?(:reader_only=) # => false
125
+ #
126
+ # t.config # => {:key => 'value', :reader_only => nil}
127
+ # t.key # => 'value'
128
+ # t.key = 'another'
129
+ # t.config # => {:key => 'another', :reader_only => nil}
130
+ #
131
+ # A block can be specified for validation/pre-processing. All inputs
132
+ # set through the config accessors, as well as the instance config=
133
+ # method are processed by the block before they set the value in the
134
+ # config hash. The config value will be set to the return of the block.
135
+ #
136
+ # The Tap::Support::Validation module provides methods to perform
137
+ # common checks and transformations. These can be accessed through
138
+ # the class method 'c':
139
+ #
140
+ # class ValidatingClass
141
+ # include Configurable
142
+ #
143
+ # config :one, 'one', &c.check(String)
144
+ # config :two, 'two' do |v|
145
+ # v.upcase
146
+ # end
147
+ # end
148
+ #
149
+ # t = ValidatingClass.new
150
+ #
151
+ # # note the default values ARE processed
152
+ # t.config # => {:one => 'one', :two => 'TWO'}
153
+ # t.one = 1 # => ValidationError
154
+ # t.config = {:one => 1} # => ValidationError
155
+ #
156
+ # t.config = {:one => 'str', :two => 'str'}
157
+ # t.config # => {:one => 'str', :two => 'STR'}
158
+ #
159
+ def config(key, value=nil, &validation)
160
+ configurations.add(key, value, &validation)
161
+
162
+ case config_mode
163
+ when :config_accessor
164
+ define_config_writer(key)
165
+ define_config_reader(key)
166
+ when :config_writer
167
+ define_config_writer(key)
168
+ when :config_reader
169
+ define_config_reader(key)
170
+ end
171
+ end
172
+
173
+ def config_merge(klass)
174
+ configurations.merge(klass.configurations) do |key|
175
+ case config_mode
176
+ when :config_accessor
177
+ define_config_writer(key)
178
+ define_config_reader(key)
179
+ when :config_writer
180
+ define_config_writer(key)
181
+ when :config_reader
182
+ define_config_reader(key)
183
+ end
184
+ end
185
+ end
186
+
187
+ # Returns the default name for the class: class.to_s.underscore
188
+ def default_name
189
+ @default_name ||= to_s.underscore
190
+ end
191
+
192
+ # Alias for Batchable.batch
193
+ def batch(*batchables)
194
+ Batchable.batch(*batchables)
195
+ end
196
+
197
+ # Returns the TDoc documentation for self.
198
+ def tdoc
199
+ @tdoc ||= Tap::Support::TDoc[self]
200
+ end
201
+
202
+ # EXPERIMENTAL
203
+ def help(opts=configurations.to_opts) # :nodoc:
204
+ return "could not find help for '#{self}'" if tdoc == nil
205
+
206
+ sections = tdoc.comment_sections(/Description|Usage/i, true)
207
+ %Q{#{self}
208
+
209
+ #{sections["Description"]}
210
+ Usage:
211
+ #{sections["Usage"]}
212
+ Options:
213
+ #{Tap::Script.usage_options(opts)}
214
+
215
+ }
216
+ end
217
+
218
+ # EXPERIMENTAL
219
+ def argv_enq(app=App.instance, &block) # :nodoc:
220
+ if block_given?
221
+ @argv_enq_block = block
222
+ return
223
+ end
224
+ return @argv_enq_block.call(app) if @argv_enq_block ||= nil
225
+
226
+ config = {}
227
+ opts = configurations.to_opts
228
+ opts << ['--help', nil, GetoptLong::NO_ARGUMENT, "Print this help."]
229
+ opts << ['--debug', nil, GetoptLong::NO_ARGUMENT, "Trace execution and debug"]
230
+ opts << ['--use', nil, GetoptLong::REQUIRED_ARGUMENT, "Loads inputs from file."]
231
+ opts << ['--iterate', nil, GetoptLong::NO_ARGUMENT, "Iterates over inputs."]
232
+
233
+ iterate = false
234
+ Tap::Script.handle_options(*opts) do |opt, value|
235
+ case opt
236
+ when '--help'
237
+ puts help(opts)
238
+ exit
239
+
240
+ when '--debug'
241
+ app.options.debug = true
242
+
243
+ when '--use'
244
+ hash = YAML.load_file(value)
245
+ hash.values.each do |args|
246
+ ARGV.concat(args)
247
+ end
248
+
249
+ when '--iterate'
250
+ iterate = true
251
+
252
+ else
253
+ key = configurations.opt_map(opt)
254
+ config[key] = YAML.load(value)
255
+ end
256
+ end
257
+
258
+ # configure task
259
+ task = app.task(ARGV.shift, config)
260
+ iterate ? ARGV.each {|input| task.enq(input) } : task.enq(*ARGV)
261
+ end
262
+
263
+ protected
264
+
265
+ attr_writer :config_mode
266
+
267
+ # Tracks the current configuration mode, to determine what
268
+ # in any accessors should be generated for the configuration.
269
+ # (default :config_accessor)
270
+ def config_mode
271
+ @config_mode ||= :config_accessor
272
+ end
273
+
274
+ # Alias for Tap::Support::Validation
275
+ def c
276
+ Validation
277
+ end
278
+
279
+ private
280
+
281
+ def define_config_reader(name, key=name) # :nodoc:
282
+ key = key.to_sym
283
+ define_method(name) do
284
+ config[key]
285
+ end
286
+ end
287
+
288
+ def define_config_writer(name, key=name) # :nodoc:
289
+ key = key.to_sym
290
+ define_method("#{name}=") do |value|
291
+ set_config(key, value)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end