tap 0.7.9

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 (146) hide show
  1. data/MIT-LICENSE +21 -0
  2. data/README +71 -0
  3. data/Rakefile +117 -0
  4. data/bin/tap +63 -0
  5. data/lib/tap.rb +15 -0
  6. data/lib/tap/app.rb +739 -0
  7. data/lib/tap/file_task.rb +354 -0
  8. data/lib/tap/generator.rb +29 -0
  9. data/lib/tap/generator/generators/config/USAGE +0 -0
  10. data/lib/tap/generator/generators/config/config_generator.rb +23 -0
  11. data/lib/tap/generator/generators/config/templates/config.erb +2 -0
  12. data/lib/tap/generator/generators/file_task/USAGE +0 -0
  13. data/lib/tap/generator/generators/file_task/file_task_generator.rb +21 -0
  14. data/lib/tap/generator/generators/file_task/templates/task.erb +27 -0
  15. data/lib/tap/generator/generators/file_task/templates/test.erb +12 -0
  16. data/lib/tap/generator/generators/root/USAGE +0 -0
  17. data/lib/tap/generator/generators/root/root_generator.rb +36 -0
  18. data/lib/tap/generator/generators/root/templates/Rakefile +48 -0
  19. data/lib/tap/generator/generators/root/templates/app.yml +19 -0
  20. data/lib/tap/generator/generators/root/templates/config/process_tap_request.yml +4 -0
  21. data/lib/tap/generator/generators/root/templates/lib/process_tap_request.rb +26 -0
  22. data/lib/tap/generator/generators/root/templates/public/images/nav.jpg +0 -0
  23. data/lib/tap/generator/generators/root/templates/public/stylesheets/color.css +57 -0
  24. data/lib/tap/generator/generators/root/templates/public/stylesheets/layout.css +108 -0
  25. data/lib/tap/generator/generators/root/templates/public/stylesheets/normalize.css +40 -0
  26. data/lib/tap/generator/generators/root/templates/public/stylesheets/typography.css +21 -0
  27. data/lib/tap/generator/generators/root/templates/server/config/environment.rb +60 -0
  28. data/lib/tap/generator/generators/root/templates/server/lib/tasks/clear_database_prerequisites.rake +5 -0
  29. data/lib/tap/generator/generators/root/templates/server/test/test_helper.rb +53 -0
  30. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +3 -0
  31. data/lib/tap/generator/generators/root/templates/test/tap_test_suite.rb +4 -0
  32. data/lib/tap/generator/generators/task/USAGE +0 -0
  33. data/lib/tap/generator/generators/task/task_generator.rb +21 -0
  34. data/lib/tap/generator/generators/task/templates/task.erb +21 -0
  35. data/lib/tap/generator/generators/task/templates/test.erb +29 -0
  36. data/lib/tap/generator/generators/workflow/USAGE +0 -0
  37. data/lib/tap/generator/generators/workflow/templates/task.erb +16 -0
  38. data/lib/tap/generator/generators/workflow/templates/test.erb +7 -0
  39. data/lib/tap/generator/generators/workflow/workflow_generator.rb +21 -0
  40. data/lib/tap/generator/options.rb +26 -0
  41. data/lib/tap/generator/usage.rb +26 -0
  42. data/lib/tap/root.rb +275 -0
  43. data/lib/tap/script/console.rb +7 -0
  44. data/lib/tap/script/destroy.rb +8 -0
  45. data/lib/tap/script/generate.rb +8 -0
  46. data/lib/tap/script/run.rb +111 -0
  47. data/lib/tap/script/server.rb +12 -0
  48. data/lib/tap/support/audit.rb +415 -0
  49. data/lib/tap/support/batch_queue.rb +165 -0
  50. data/lib/tap/support/combinator.rb +114 -0
  51. data/lib/tap/support/logger.rb +91 -0
  52. data/lib/tap/support/rap.rb +38 -0
  53. data/lib/tap/support/run_error.rb +20 -0
  54. data/lib/tap/support/template.rb +81 -0
  55. data/lib/tap/support/templater.rb +155 -0
  56. data/lib/tap/support/versions.rb +63 -0
  57. data/lib/tap/task.rb +448 -0
  58. data/lib/tap/test.rb +320 -0
  59. data/lib/tap/test/env_vars.rb +16 -0
  60. data/lib/tap/test/inference_methods.rb +298 -0
  61. data/lib/tap/test/subset_methods.rb +260 -0
  62. data/lib/tap/version.rb +3 -0
  63. data/lib/tap/workflow.rb +73 -0
  64. data/test/app/config/addition_template.yml +6 -0
  65. data/test/app/config/batch.yml +2 -0
  66. data/test/app/config/empty.yml +0 -0
  67. data/test/app/config/erb.yml +1 -0
  68. data/test/app/config/template.yml +6 -0
  69. data/test/app/config/version-0.1.yml +1 -0
  70. data/test/app/config/version.yml +1 -0
  71. data/test/app/lib/app_test_task.rb +2 -0
  72. data/test/app_class_test.rb +33 -0
  73. data/test/app_test.rb +1372 -0
  74. data/test/file_task/config/batch.yml +2 -0
  75. data/test/file_task/config/configured.yml +1 -0
  76. data/test/file_task/old_file_one.txt +0 -0
  77. data/test/file_task/old_file_two.txt +0 -0
  78. data/test/file_task_test.rb +1041 -0
  79. data/test/root/alt_lib/alt_module.rb +4 -0
  80. data/test/root/lib/absolute_alt_filepath.rb +2 -0
  81. data/test/root/lib/alternative_filepath.rb +2 -0
  82. data/test/root/lib/another_module.rb +2 -0
  83. data/test/root/lib/nested/some_module.rb +4 -0
  84. data/test/root/lib/no_module_included.rb +0 -0
  85. data/test/root/lib/some/module.rb +4 -0
  86. data/test/root/lib/some_class.rb +2 -0
  87. data/test/root/lib/some_module.rb +3 -0
  88. data/test/root/load_path/load_path_module.rb +2 -0
  89. data/test/root/load_path/skip_module.rb +2 -0
  90. data/test/root/mtime/older.txt +0 -0
  91. data/test/root/unload/full_path.rb +2 -0
  92. data/test/root/unload/loaded_by_nested.rb +2 -0
  93. data/test/root/unload/nested/nested_load.rb +6 -0
  94. data/test/root/unload/nested/nested_with_ext.rb +4 -0
  95. data/test/root/unload/nested/relative_path.rb +4 -0
  96. data/test/root/unload/older.rb +2 -0
  97. data/test/root/unload/unload_base.rb +9 -0
  98. data/test/root/versions/another.yml +0 -0
  99. data/test/root/versions/file-0.1.2.yml +0 -0
  100. data/test/root/versions/file-0.1.yml +0 -0
  101. data/test/root/versions/file.yml +0 -0
  102. data/test/root_test.rb +483 -0
  103. data/test/support/audit_test.rb +449 -0
  104. data/test/support/batch_queue_test.rb +320 -0
  105. data/test/support/combinator_test.rb +249 -0
  106. data/test/support/logger_test.rb +31 -0
  107. data/test/support/template_test.rb +122 -0
  108. data/test/support/templater/erb.txt +2 -0
  109. data/test/support/templater/erb.yml +2 -0
  110. data/test/support/templater/somefile.txt +2 -0
  111. data/test/support/templater_test.rb +192 -0
  112. data/test/support/versions_test.rb +71 -0
  113. data/test/tap_test_helper.rb +4 -0
  114. data/test/tap_test_suite.rb +4 -0
  115. data/test/task/config/batch.yml +2 -0
  116. data/test/task/config/batched.yml +2 -0
  117. data/test/task/config/configured.yml +1 -0
  118. data/test/task/config/example.yml +1 -0
  119. data/test/task/config/overriding.yml +2 -0
  120. data/test/task/config/task_with_config.yml +1 -0
  121. data/test/task/config/template.yml +4 -0
  122. data/test/task_class_test.rb +118 -0
  123. data/test/task_execute_test.rb +233 -0
  124. data/test/task_test.rb +424 -0
  125. data/test/test/inference_methods/test_assert_expected/expected/file.txt +1 -0
  126. data/test/test/inference_methods/test_assert_expected/expected/folder/file.txt +1 -0
  127. data/test/test/inference_methods/test_assert_expected/input/file.txt +1 -0
  128. data/test/test/inference_methods/test_assert_expected/input/folder/file.txt +1 -0
  129. data/test/test/inference_methods/test_assert_files_exist/input/input_1.txt +0 -0
  130. data/test/test/inference_methods/test_assert_files_exist/input/input_2.txt +0 -0
  131. data/test/test/inference_methods/test_file_compare/expected/output_1.txt +3 -0
  132. data/test/test/inference_methods/test_file_compare/expected/output_2.txt +1 -0
  133. data/test/test/inference_methods/test_file_compare/input/input_1.txt +3 -0
  134. data/test/test/inference_methods/test_file_compare/input/input_2.txt +3 -0
  135. data/test/test/inference_methods/test_infer_glob/expected/file.yml +0 -0
  136. data/test/test/inference_methods/test_infer_glob/expected/file_1.txt +0 -0
  137. data/test/test/inference_methods/test_infer_glob/expected/file_2.txt +0 -0
  138. data/test/test/inference_methods/test_yml_compare/expected/output_1.yml +6 -0
  139. data/test/test/inference_methods/test_yml_compare/expected/output_2.yml +6 -0
  140. data/test/test/inference_methods/test_yml_compare/input/input_1.yml +4 -0
  141. data/test/test/inference_methods/test_yml_compare/input/input_2.yml +4 -0
  142. data/test/test/inference_methods_test.rb +311 -0
  143. data/test/test/subset_methods_test.rb +115 -0
  144. data/test/test_test.rb +233 -0
  145. data/test/workflow_test.rb +108 -0
  146. metadata +274 -0
@@ -0,0 +1,165 @@
1
+ require 'tap/support/audit'
2
+ require 'monitor'
3
+
4
+ module Tap
5
+ module Support
6
+
7
+ # BatchQueue allows thread-safe enqueing and dequeing of tasks and inputs for
8
+ # execution. Until the task is dequeued, additional enqueuements will aggregate
9
+ # the inputs into a batch. The queue order is determined solely by the task;
10
+ # all accumulated inputs are dequeued at once.
11
+ #
12
+ # # given tasks t1 and t2:
13
+ # q = BatchQueue.new
14
+ # q.enq t1, 1
15
+ # q.enq t2, :a,:b,:c
16
+ # q.enq t1, 2,3
17
+ #
18
+ # q.deq # => [t1, [1,2,3]]
19
+ # q.deq # => [t2, [:a,:b,:c]]
20
+ # q.deq # => nil
21
+ #
22
+ # All BatchQueue methods are monitored such that they may only be accessed
23
+ # by one thread at a time, in order to ensure that transactions are atomic.
24
+ class BatchQueue
25
+ include MonitorMixin
26
+
27
+ # creates a new BatchQueue
28
+ def initialize
29
+ super
30
+ self.clear
31
+ end
32
+
33
+ # Clears the BatchQueue of all tasks and inputs.
34
+ def clear
35
+ synchronize do
36
+ self.inputs = {}
37
+ self.tasks = []
38
+ end
39
+ end
40
+
41
+ # Returns the number of enqueued tasks (same as length)
42
+ def size
43
+ tasks.length
44
+ end
45
+
46
+ # Returns the number of enqueued tasks (same as size)
47
+ def length
48
+ size
49
+ end
50
+
51
+ # True if no tasks are enqueued
52
+ def empty?
53
+ tasks.empty?
54
+ end
55
+
56
+ # Enqueues the inputs to the task. If the task has not already been added to the
57
+ # queue, then the task will be pushed onto the queue and the inputs registered.
58
+ # Otherwise, the inputs will be added to the registered inputs; the task will
59
+ # not be added to the queue twice. If the task is batched, then each task in the
60
+ # batch will be enqueued in order, with the same inputs (Audit inputs will be forked
61
+ # for each task).
62
+ def enq(task, *inputs)
63
+ synchronize do
64
+ batch = task.batch
65
+ batch.each do |t|
66
+ tasks << t unless tasks.include?(t)
67
+
68
+ inputs = inputs.collect do |result|
69
+ result.kind_of?(Support::Audit) ? result._fork : result
70
+ end if batch.length > 1
71
+
72
+ self.inputs[t] = [] unless self.inputs[t]
73
+ self.inputs[t].concat(inputs)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Enqueues the task and input, then prioritizes the task to the front of the queue.
79
+ def priority_enq(task, *inputs)
80
+ synchronize do
81
+ enq(task, *inputs)
82
+ prioritize(task)
83
+ end
84
+ end
85
+
86
+ # Moves the task to the front of the queue. Does not queue a task if the task is not
87
+ # already in the queue. If the task is batched, then each task in the batch will be
88
+ # prioritized in order.
89
+ def prioritize(task)
90
+ synchronize do
91
+ task.batch.reverse_each do |t|
92
+ tasks.unshift(t) if tasks.delete(t)
93
+ end
94
+ end
95
+ end
96
+
97
+ # Moves the task to the back of the queue. Does not queue a task if the task is not
98
+ # already in the queue. If the task is batched, then each task in the batch will be
99
+ # marginalized in order.
100
+ def marginalize(task)
101
+ synchronize do
102
+ task.batch.each do |t|
103
+ tasks.push(t) if tasks.delete(t)
104
+ end
105
+ end
106
+ end
107
+
108
+ # Returns true if the task is executable with the currently registered inputs
109
+ # (as determined by task.executable?).
110
+ def executable?(task)
111
+ synchronize do
112
+ task.executable?(inputs[task])
113
+ end
114
+ end
115
+
116
+ # Dequeues the next executable task, or the specified task (even if it is not
117
+ # executable). The task and the current inputs for the task are returned in
118
+ # an array like: [task, inputs]. Returns nil if the task is not in the queue.
119
+ def deq(task=nil)
120
+ synchronize do
121
+ task = peek if task.nil?
122
+
123
+ if tasks.include?(task)
124
+ [tasks.delete(task), inputs.delete(task)]
125
+ else
126
+ nil
127
+ end
128
+ end
129
+ end
130
+
131
+ # Returns the next executable task, or nil if no queued tasks are executable.
132
+ # The task is not dequeued.
133
+ def peek
134
+ synchronize do
135
+ tasks.each do |task|
136
+ next unless executable?(task)
137
+ return task
138
+ end
139
+ nil
140
+ end
141
+ end
142
+
143
+ # Returns the number of executable tasks in the queue.
144
+ def num_executable
145
+ synchronize do
146
+ tasks.inject(0) do |count, task|
147
+ executable?(task) ? count + 1 : count
148
+ end
149
+ end
150
+ end
151
+
152
+ # Returns the number of non-executable tasks in the queue.
153
+ def num_not_executable
154
+ synchronize do
155
+ size - num_executable
156
+ end
157
+ end
158
+
159
+ protected
160
+
161
+ attr_accessor :inputs, :tasks
162
+
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,114 @@
1
+ require 'enumerator'
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Combinator provides a method for iterating over all possible combinations
7
+ # of items in the input sets.
8
+ #
9
+ # c = Combinator.new [1,2], [3,4]
10
+ # c.collect # => [[1,3], [1,4], [2,3], [2,4]]
11
+ #
12
+ # Combinator works by iteratively combining each element from the first set (a)
13
+ # with each element from the second set (b). When more than two sets are given,
14
+ # a Combinator will bundle all but the first set into a new Combinator which
15
+ # then acts as the second set.
16
+ #
17
+ # # Note: each element in a set is arrayified
18
+ # # to simplify creation of the combination.
19
+ #
20
+ # c = Combinator.new [1,2], [3,4], [5,6]
21
+ # c.a # => [[1],[2]]
22
+ # c.b.class # => Combinator
23
+ # c.b.a # => [[3],[4]]
24
+ # c.b.b # => [[5],[6]]
25
+ #
26
+ # Thus when iterating, each Combinator makes it's pairwise combinations and
27
+ # works outwards to the final multi-set combinations.
28
+ class Combinator
29
+ include Enumerable
30
+
31
+ attr_reader :a, :b
32
+
33
+ # Creates a new Combinator. Input sets must be an Array, or nil.
34
+ # Within the Array any objects are allowed for combination.
35
+ def initialize(*sets)
36
+ @a = make_set(sets.shift)
37
+ @b = make_set(*sets)
38
+ end
39
+
40
+ # Returns the sets used to initialize the Combinator, minus any
41
+ # nil or empty sets.
42
+ def sets
43
+ sets_in(a) + sets_in(b)
44
+ end
45
+
46
+ # True if the Combinator has no combinations.
47
+ def empty?
48
+ a.empty? && b.empty?
49
+ end
50
+
51
+ # Returns the number of combinations in the Combinator.
52
+ def length
53
+ if a.empty? || b.empty?
54
+ if !a.empty?
55
+ a.length
56
+ elsif !b.empty?
57
+ b.length
58
+ else
59
+ 0
60
+ end
61
+ else
62
+ a.length * b.length
63
+ end
64
+ end
65
+
66
+ # Passes each combination as an array to the input block.
67
+ def each(&block)
68
+ # if either set is empty, iterate only the non-empty set
69
+ # note, the two-step check on each ensures that if both
70
+ # sets are empty, then neither is iterated.
71
+ if a.empty? || b.empty?
72
+ if !a.empty?
73
+ a.each {|aa| yield aa }
74
+ elsif !b.empty?
75
+ b.each {|bb| yield bb }
76
+ end
77
+ else
78
+ # otherwise, iterate all combinations of both
79
+ a.each do |aa|
80
+ b.each do |bb|
81
+ yield aa + bb
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def sets_in(set)
90
+ # recursively de-arrayifies each element in the array
91
+ case set
92
+ when Combinator then set.sets
93
+ when Array then set.empty? ? [] : [set.collect {|s| s.first}]
94
+ end
95
+ end
96
+
97
+ def make_set(*sets)
98
+ # recieves an array of arrays or combinators
99
+ return Combinator.new(*sets) if sets.length > 1
100
+ return [] if sets.empty?
101
+
102
+ # recursively arrayifies each element in an array
103
+ set = sets.first
104
+ case set
105
+ when Array then set.collect {|s| [s]}
106
+ when nil then []
107
+ else
108
+ raise ArgumentError.new("sets must be arrays or nil")
109
+ end
110
+ end
111
+
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,91 @@
1
+ require 'logger'
2
+
3
+ module Tap
4
+ module Support
5
+ # Logger provides methods to extend Logger objects.
6
+ #
7
+ # The output format is designed to look nice in a command prompt, but provide useful information
8
+ # if directed at a log file. The Logger output format is like:
9
+ #
10
+ # I[datetime] type message
11
+ #
12
+ # The first letter ('I' - INFO) indicates the severity of the message, next comes the datetime of the
13
+ # log, the type of log message, then the message itself. Specify the log type and message like:
14
+ #
15
+ # logger.type "message"
16
+ #
17
+ # Typed logging occurs through +method_missing+. Any type is possible so long as the logger doesn't
18
+ # already have a method of the input type.
19
+ module Logger
20
+ Format = " %s[%s] %18s %s\n"
21
+
22
+ def self.extended(base)
23
+ # On OS X (and maybe other systems), $stdout is not sync-ed.
24
+ # Set sync to true so that writes are immediately flushed
25
+ # to the console.
26
+ base.logdev.dev.sync = true if base.logdev.dev == $stdout
27
+ end
28
+
29
+ attr_writer :format
30
+ def format
31
+ (@format ||= nil) || Format
32
+ end
33
+
34
+ # Provides direct access to the log device
35
+ def logdev
36
+ @logdev
37
+ end
38
+
39
+ def tick(mark=".")
40
+ @logdev.write mark
41
+ end
42
+
43
+ def monitor(action="beginning", msg="", &block)
44
+ begin_time = Time.now
45
+ format_add(INFO, msg, action) {|format| format.chomp("\n")}
46
+
47
+ result = yield
48
+
49
+ format_add(INFO, "#{Time.now-begin_time}s", "completed") {|format| "\n" + format}
50
+ result
51
+ end
52
+
53
+ def format_add(level, msg, action=nil, &block)
54
+ current_format = self.format
55
+ self.format = yield(current_format)
56
+ add(level, msg, action.to_s)
57
+ self.format = current_format
58
+ end
59
+
60
+ # Overrides the default format message to produce output like:
61
+ #
62
+ # I[H:M:S] type message
63
+ #
64
+ # If no progname is specified, '--' is used.
65
+ def format_message(severity, timestamp, progname, msg)
66
+ if timestamp.respond_to?(:strftime) && self.datetime_format
67
+ timestamp = timestamp.strftime(self.datetime_format)
68
+ end
69
+
70
+ format % [severity[0..0], timestamp, progname || '--' , msg]
71
+ end
72
+
73
+ # Convenience method for creating section breaks in the output, formatted like:
74
+ #
75
+ # ----- message -----
76
+ def section_break(message)
77
+ @logdev.write(" ----- #{message} -----\n")
78
+ end
79
+
80
+ private
81
+
82
+ INFO = Object::Logger::INFO
83
+
84
+ # Specifies that any unknown method calls will be treated as logging calls where the method
85
+ # is the log type, and the first argument is the message. Types are underscored before logging.
86
+ def method_missing(method, *args, &block)
87
+ add(INFO, args.first, method.to_s.underscore, &block)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,38 @@
1
+ module Tap
2
+ module Support
3
+
4
+ # Defines the minimum methods to run (ie to queue and to execute)
5
+ # an object from Tap::App, aside from execute which must be
6
+ # provided by the object itself. Does not permit use in workflow
7
+ # methods (requires results, condition, and on_complete)
8
+ #
9
+ # == Advice
10
+ #
11
+ # If you ever want to multithread the object, execute should be
12
+ # limited to one thread at a time, for example by wrapping it in
13
+ # synchronize from the MonitorMixin. You will probably get away
14
+ # without doing so unless you're adding the object to the queue
15
+ # multiple times in succession -- but be on the lookout for
16
+ # unexpected results due to unsynchronized execution.
17
+ module Rap
18
+ attr_accessor :multithread
19
+
20
+ def multithread?
21
+ multithread
22
+ end
23
+
24
+ def executable?(inputs)
25
+ true
26
+ end
27
+
28
+ def batch
29
+ @batch ||= [self]
30
+ end
31
+
32
+ def batched?
33
+ batch.length > 1
34
+ end
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,20 @@
1
+ module Tap
2
+ module Support
3
+ class RunError < RuntimeError
4
+ attr_reader :original_error, :terminate_errors
5
+
6
+ def initialize(original_error, terminate_errors)
7
+ @original_error = original_error
8
+ @terminate_errors = terminate_errors
9
+ end
10
+
11
+ def message
12
+ "#{original_error.class} #{original_error.message}"
13
+ end
14
+
15
+ def backtrace
16
+ original_error.backtrace
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,81 @@
1
+ require 'tap/support/combinator'
2
+
3
+ module Tap
4
+ module Support
5
+
6
+ # Template provides the default methods for making hash templates using Templater.
7
+ # Methods are provided to make variations of the template, as well as to pattern
8
+ # templates based on combinations of possible values.
9
+ #
10
+ module Template
11
+ attr_accessor :templater
12
+
13
+ # Provides a standard way of creating a variations of a template and takes an
14
+ # array of hashes with the specified variations. The template is treated as the
15
+ # default and variations are pushed (<<) to the templater. The template is
16
+ # cleared upon complete, so that it will be removed if used in the context of
17
+ # a Templater.
18
+ #
19
+ # t = {"default" => "default value"}.extend Template
20
+ # t.templater = []
21
+ # t.variations([
22
+ # {"default" => "non-default value"},
23
+ # {"another" => "value"}])
24
+ #
25
+ # t.templater
26
+ # # => [{"default" => "non-default value"},
27
+ # {"default" => "default value", "another" => "value"}]
28
+ #
29
+ def variations(array)
30
+ variations!(array)
31
+ self.clear
32
+ end
33
+
34
+ # Same as variations but does NOT clear the template (ie the template will
35
+ # be kept as specified, effectively acting as another variant). Note that
36
+ # variations! is called by the 'variations!!' key.
37
+ def variations!(array)
38
+ array.each { |var| templater << self.merge(var) }
39
+ end
40
+
41
+ # Provides a standard way of creating variations of a template based on all
42
+ # combinations of the keys in the input hash. The template itself is treated
43
+ # as the default and variations are pushed (<<) to the templater. The template
44
+ # is cleared upon complete, so that it will be removed if used in the context of
45
+ # a Templater.
46
+ #
47
+ # t = {"default" => "default value"}.extend Template
48
+ # t.templater = []
49
+ # t.combinations(
50
+ # "letter" => ["a", "b"],
51
+ # "number" => [1, 2])
52
+ #
53
+ # t.templater
54
+ # # => [{"default" => "default value", "letter" => "a", "number" => 1},
55
+ # {"default" => "default value", "letter" => "a", "number" => 2},
56
+ # {"default" => "default value", "letter" => "b", "number" => 1},
57
+ # {"default" => "default value", "letter" => "b", "number" => 2}]
58
+ def combinations(hash)
59
+ combinations!(hash)
60
+ self.clear
61
+ end
62
+
63
+ # Same as combinations but does NOT clear the template (ie the template will
64
+ # be kept as specified, effectively acting as another variant). Note that
65
+ # combinations! is called by the 'combinations!!' key.
66
+ def combinations!(hash)
67
+ keys, values = hash.to_a.transpose
68
+ values.collect! do |value|
69
+ value.kind_of?(Array) ? value : [value]
70
+ end
71
+
72
+ Combinator.new(*values).each do |c|
73
+ template = {}
74
+ keys.each_with_index {|key,i| template[key] = c[i] }
75
+
76
+ templater << self.merge(template)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end