tap 0.8.0 → 0.9.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 (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