tap 0.7.9

Sign up to get free protection for your applications and to get access to all the features.
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