taski 0.2.3 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +52 -4
- data/examples/README.md +13 -1
- data/examples/section_configuration.rb +212 -0
- data/examples/tree_demo.rb +125 -0
- data/lib/taski/dependency_analyzer.rb +65 -38
- data/lib/taski/exceptions.rb +3 -0
- data/lib/taski/logger.rb +7 -62
- data/lib/taski/logging/formatter_factory.rb +34 -0
- data/lib/taski/logging/formatter_interface.rb +19 -0
- data/lib/taski/logging/json_formatter.rb +26 -0
- data/lib/taski/logging/simple_formatter.rb +16 -0
- data/lib/taski/logging/structured_formatter.rb +44 -0
- data/lib/taski/progress/display_colors.rb +17 -0
- data/lib/taski/progress/display_manager.rb +115 -0
- data/lib/taski/progress/output_capture.rb +105 -0
- data/lib/taski/progress/spinner_animation.rb +46 -0
- data/lib/taski/progress/task_formatter.rb +25 -0
- data/lib/taski/progress/task_status.rb +38 -0
- data/lib/taski/progress/terminal_controller.rb +35 -0
- data/lib/taski/progress_display.rb +23 -320
- data/lib/taski/section.rb +268 -0
- data/lib/taski/task/base.rb +11 -32
- data/lib/taski/task/dependency_resolver.rb +4 -64
- data/lib/taski/task/instance_management.rb +28 -15
- data/lib/taski/tree_colors.rb +91 -0
- data/lib/taski/utils/dependency_resolver_helper.rb +85 -0
- data/lib/taski/utils/tree_display_helper.rb +71 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +4 -0
- metadata +18 -1
@@ -1,356 +1,59 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "progress/terminal_controller"
|
4
|
+
require_relative "progress/spinner_animation"
|
5
|
+
require_relative "progress/output_capture"
|
6
|
+
require_relative "progress/display_manager"
|
4
7
|
|
5
8
|
module Taski
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
MOVE_UP_AND_CLEAR = "#{MOVE_UP}#{CLEAR_LINE}"
|
9
|
+
# Backward compatibility aliases
|
10
|
+
TerminalController = Progress::TerminalController
|
11
|
+
SpinnerAnimation = Progress::SpinnerAnimation
|
12
|
+
OutputCapture = Progress::OutputCapture
|
13
|
+
TaskStatus = Progress::TaskStatus
|
12
14
|
|
13
|
-
|
14
|
-
@output = output
|
15
|
-
end
|
16
|
-
|
17
|
-
def clear_lines(count)
|
18
|
-
return if count == 0
|
19
|
-
|
20
|
-
count.times { @output.print MOVE_UP_AND_CLEAR }
|
21
|
-
end
|
22
|
-
|
23
|
-
def puts(text)
|
24
|
-
@output.puts text
|
25
|
-
end
|
26
|
-
|
27
|
-
def print(text)
|
28
|
-
@output.print text
|
29
|
-
end
|
30
|
-
|
31
|
-
def flush
|
32
|
-
@output.flush
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Spinner animation with dots-style characters
|
37
|
-
class SpinnerAnimation
|
38
|
-
SPINNER_CHARS = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
|
39
|
-
FRAME_DELAY = 0.1
|
40
|
-
|
41
|
-
def initialize
|
42
|
-
@frame = 0
|
43
|
-
@running = false
|
44
|
-
@thread = nil
|
45
|
-
end
|
46
|
-
|
47
|
-
def start(terminal, task_name, &display_callback)
|
48
|
-
return if @running
|
49
|
-
|
50
|
-
@running = true
|
51
|
-
@frame = 0
|
52
|
-
|
53
|
-
@thread = Thread.new do
|
54
|
-
while @running
|
55
|
-
current_char = SPINNER_CHARS[@frame % SPINNER_CHARS.length]
|
56
|
-
display_callback&.call(current_char, task_name)
|
57
|
-
|
58
|
-
@frame += 1
|
59
|
-
sleep FRAME_DELAY
|
60
|
-
end
|
61
|
-
rescue
|
62
|
-
# Silently handle thread errors
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def stop
|
67
|
-
@running = false
|
68
|
-
@thread&.join(0.2)
|
69
|
-
@thread = nil
|
70
|
-
end
|
71
|
-
|
72
|
-
def running?
|
73
|
-
@running
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
# Captures stdout and maintains last N lines like tail -f
|
78
|
-
class OutputCapture
|
79
|
-
MAX_LINES = 10
|
80
|
-
DISPLAY_LINES = 5
|
81
|
-
|
82
|
-
def initialize(main_output)
|
83
|
-
@main_output = main_output
|
84
|
-
@buffer = []
|
85
|
-
@capturing = false
|
86
|
-
@original_stdout = nil
|
87
|
-
@pipe_reader = nil
|
88
|
-
@pipe_writer = nil
|
89
|
-
@capture_thread = nil
|
90
|
-
end
|
91
|
-
|
92
|
-
def start
|
93
|
-
return if @capturing
|
94
|
-
|
95
|
-
@buffer.clear
|
96
|
-
setup_stdout_redirection
|
97
|
-
@capturing = true
|
98
|
-
|
99
|
-
start_capture_thread
|
100
|
-
end
|
101
|
-
|
102
|
-
def stop
|
103
|
-
return unless @capturing
|
104
|
-
|
105
|
-
@capturing = false
|
106
|
-
|
107
|
-
# Restore stdout
|
108
|
-
restore_stdout
|
109
|
-
|
110
|
-
# Clean up pipes and thread
|
111
|
-
cleanup_capture_thread
|
112
|
-
cleanup_pipes
|
113
|
-
end
|
114
|
-
|
115
|
-
def last_lines
|
116
|
-
@buffer.last(DISPLAY_LINES)
|
117
|
-
end
|
118
|
-
|
119
|
-
def capturing?
|
120
|
-
@capturing
|
121
|
-
end
|
122
|
-
|
123
|
-
private
|
124
|
-
|
125
|
-
def setup_stdout_redirection
|
126
|
-
@original_stdout = $stdout
|
127
|
-
@pipe_reader, @pipe_writer = IO.pipe
|
128
|
-
$stdout = @pipe_writer
|
129
|
-
end
|
130
|
-
|
131
|
-
def restore_stdout
|
132
|
-
return unless @original_stdout
|
133
|
-
|
134
|
-
$stdout = @original_stdout
|
135
|
-
@original_stdout = nil
|
136
|
-
end
|
137
|
-
|
138
|
-
def start_capture_thread
|
139
|
-
@capture_thread = Thread.new do
|
140
|
-
while (line = @pipe_reader.gets)
|
141
|
-
line = line.chomp
|
142
|
-
next if line.empty?
|
143
|
-
next if skip_line?(line)
|
144
|
-
|
145
|
-
add_line_to_buffer(line)
|
146
|
-
end
|
147
|
-
rescue IOError
|
148
|
-
# Pipe closed, normal termination
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def skip_line?(line)
|
153
|
-
# Skip logger lines (they appear separately)
|
154
|
-
line.match?(/^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\]/)
|
155
|
-
end
|
156
|
-
|
157
|
-
def add_line_to_buffer(line)
|
158
|
-
@buffer << line
|
159
|
-
@buffer.shift while @buffer.length > MAX_LINES
|
160
|
-
end
|
161
|
-
|
162
|
-
def cleanup_capture_thread
|
163
|
-
@capture_thread&.join(0.1)
|
164
|
-
@capture_thread = nil
|
165
|
-
end
|
166
|
-
|
167
|
-
def cleanup_pipes
|
168
|
-
[@pipe_writer, @pipe_reader].each do |pipe|
|
169
|
-
pipe&.close
|
170
|
-
rescue IOError
|
171
|
-
# Already closed, ignore
|
172
|
-
end
|
173
|
-
@pipe_writer = @pipe_reader = nil
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
# Represents task execution status
|
178
|
-
class TaskStatus
|
179
|
-
attr_reader :name, :duration, :error
|
180
|
-
|
181
|
-
def initialize(name:, duration: nil, error: nil)
|
182
|
-
@name = name
|
183
|
-
@duration = duration
|
184
|
-
@error = error
|
185
|
-
end
|
186
|
-
|
187
|
-
def success?
|
188
|
-
@error.nil?
|
189
|
-
end
|
190
|
-
|
191
|
-
def failure?
|
192
|
-
!success?
|
193
|
-
end
|
194
|
-
|
195
|
-
def duration_ms
|
196
|
-
return nil unless @duration
|
197
|
-
(@duration * 1000).round(1)
|
198
|
-
end
|
199
|
-
|
200
|
-
def icon
|
201
|
-
success? ? "✅" : "❌"
|
202
|
-
end
|
203
|
-
|
204
|
-
def format_duration
|
205
|
-
return "" unless duration_ms
|
206
|
-
"(#{duration_ms}ms)"
|
207
|
-
end
|
208
|
-
end
|
209
|
-
|
210
|
-
# Main progress display controller
|
15
|
+
# Main progress display controller - refactored for better separation of concerns
|
211
16
|
class ProgressDisplay
|
212
|
-
|
213
|
-
COLORS = {
|
214
|
-
reset: "\033[0m",
|
215
|
-
bold: "\033[1m",
|
216
|
-
dim: "\033[2m",
|
217
|
-
cyan: "\033[36m",
|
218
|
-
green: "\033[32m",
|
219
|
-
red: "\033[31m"
|
220
|
-
}.freeze
|
221
|
-
|
222
|
-
def initialize(output: $stdout, force_enable: nil)
|
17
|
+
def initialize(output: $stdout, enable: true, include_captured_output: nil)
|
223
18
|
@output = output
|
224
|
-
@terminal = TerminalController.new(output)
|
225
|
-
@spinner = SpinnerAnimation.new
|
226
|
-
@output_capture = OutputCapture.new(output)
|
19
|
+
@terminal = Progress::TerminalController.new(output)
|
20
|
+
@spinner = Progress::SpinnerAnimation.new
|
21
|
+
@output_capture = Progress::OutputCapture.new(output)
|
227
22
|
|
228
|
-
#
|
229
|
-
|
23
|
+
# Default to including captured output in test environments (when output != $stdout)
|
24
|
+
# This ensures test output is visible in the test output stream
|
25
|
+
include_captured_output = include_captured_output.nil? ? (output != $stdout) : include_captured_output
|
26
|
+
@display_manager = Progress::DisplayManager.new(@terminal, @spinner, @output_capture, include_captured_output: include_captured_output)
|
230
27
|
|
231
|
-
@
|
232
|
-
@current_display_lines = 0
|
28
|
+
@enabled = ENV["TASKI_PROGRESS_DISABLE"] != "1" && enable
|
233
29
|
end
|
234
30
|
|
235
31
|
def start_task(task_name, dependencies: [])
|
236
|
-
puts "DEBUG: start_task called for #{task_name}, enabled: #{@enabled}" if ENV["TASKI_DEBUG"]
|
237
32
|
return unless @enabled
|
238
33
|
|
239
|
-
|
240
|
-
@output_capture.start
|
241
|
-
|
242
|
-
start_spinner_display(task_name)
|
34
|
+
@display_manager.start_task_display(task_name)
|
243
35
|
end
|
244
36
|
|
245
37
|
def complete_task(task_name, duration:)
|
246
38
|
return unless @enabled
|
247
39
|
|
248
|
-
|
249
|
-
finish_task(status)
|
40
|
+
@display_manager.complete_task_display(task_name, duration: duration)
|
250
41
|
end
|
251
42
|
|
252
43
|
def fail_task(task_name, error:, duration:)
|
253
44
|
return unless @enabled
|
254
45
|
|
255
|
-
|
256
|
-
finish_task(status)
|
46
|
+
@display_manager.fail_task_display(task_name, error: error, duration: duration)
|
257
47
|
end
|
258
48
|
|
259
49
|
def clear
|
260
50
|
return unless @enabled
|
261
51
|
|
262
|
-
@
|
263
|
-
@output_capture.stop
|
264
|
-
clear_current_display
|
265
|
-
|
266
|
-
# Display final summary of all completed tasks
|
267
|
-
if @completed_tasks.any?
|
268
|
-
@completed_tasks.each do |status|
|
269
|
-
@terminal.puts format_completed_task(status)
|
270
|
-
end
|
271
|
-
@terminal.flush
|
272
|
-
end
|
273
|
-
|
274
|
-
@completed_tasks.clear
|
275
|
-
@current_display_lines = 0
|
52
|
+
@display_manager.clear_all_displays
|
276
53
|
end
|
277
54
|
|
278
55
|
def enabled?
|
279
56
|
@enabled
|
280
57
|
end
|
281
|
-
|
282
|
-
private
|
283
|
-
|
284
|
-
def start_spinner_display(task_name)
|
285
|
-
@spinner.start(@terminal, task_name) do |spinner_char, name|
|
286
|
-
display_current_state(spinner_char, name)
|
287
|
-
end
|
288
|
-
end
|
289
|
-
|
290
|
-
def display_current_state(spinner_char, task_name)
|
291
|
-
clear_current_display
|
292
|
-
|
293
|
-
lines_count = 0
|
294
|
-
|
295
|
-
# Only display current task with spinner (no past completed tasks during execution)
|
296
|
-
@terminal.puts format_current_task(spinner_char, task_name)
|
297
|
-
lines_count += 1
|
298
|
-
|
299
|
-
# Display output lines
|
300
|
-
@output_capture.last_lines.each do |line|
|
301
|
-
@terminal.puts format_output_line(line)
|
302
|
-
lines_count += 1
|
303
|
-
end
|
304
|
-
|
305
|
-
@current_display_lines = lines_count
|
306
|
-
@terminal.flush
|
307
|
-
end
|
308
|
-
|
309
|
-
def finish_task(status)
|
310
|
-
@spinner.stop
|
311
|
-
|
312
|
-
# Capture output before stopping
|
313
|
-
captured_output = @output_capture.last_lines
|
314
|
-
@output_capture.stop
|
315
|
-
clear_current_display
|
316
|
-
|
317
|
-
# In test environments (when terminal is StringIO), include captured output
|
318
|
-
if @terminal.is_a?(StringIO) && captured_output.any?
|
319
|
-
captured_output.each do |line|
|
320
|
-
@terminal.puts line.chomp
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
|
-
@completed_tasks << status
|
325
|
-
display_final_state
|
326
|
-
end
|
327
|
-
|
328
|
-
def display_final_state
|
329
|
-
# Only display the newly completed task (last one)
|
330
|
-
if @completed_tasks.any?
|
331
|
-
latest_task = @completed_tasks.last
|
332
|
-
@terminal.puts format_completed_task(latest_task)
|
333
|
-
end
|
334
|
-
@terminal.flush
|
335
|
-
@current_display_lines = 1 # Only one line for the latest task
|
336
|
-
end
|
337
|
-
|
338
|
-
def format_completed_task(status)
|
339
|
-
color = status.success? ? COLORS[:green] : COLORS[:red]
|
340
|
-
"#{color}#{COLORS[:bold]}#{status.icon} #{status.name}#{COLORS[:reset]} #{COLORS[:dim]}#{status.format_duration}#{COLORS[:reset]}"
|
341
|
-
end
|
342
|
-
|
343
|
-
def format_current_task(spinner_char, task_name)
|
344
|
-
"#{COLORS[:cyan]}#{spinner_char}#{COLORS[:reset]} #{COLORS[:bold]}#{task_name}#{COLORS[:reset]}"
|
345
|
-
end
|
346
|
-
|
347
|
-
def format_output_line(line)
|
348
|
-
" #{COLORS[:dim]}#{line}#{COLORS[:reset]}"
|
349
|
-
end
|
350
|
-
|
351
|
-
def clear_current_display
|
352
|
-
@terminal.clear_lines(@current_display_lines)
|
353
|
-
@current_display_lines = 0
|
354
|
-
end
|
355
58
|
end
|
356
59
|
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "dependency_analyzer"
|
4
|
+
require_relative "utils"
|
5
|
+
require_relative "utils/tree_display_helper"
|
6
|
+
require_relative "utils/dependency_resolver_helper"
|
7
|
+
|
8
|
+
module Taski
|
9
|
+
# Section provides an interface abstraction layer for dynamic implementation selection
|
10
|
+
# while maintaining static analysis capabilities
|
11
|
+
class Section
|
12
|
+
class << self
|
13
|
+
# === Dependency Resolution ===
|
14
|
+
|
15
|
+
# Resolve method for dependency graph (called by resolve_dependencies)
|
16
|
+
# @param queue [Array] Queue of tasks to process
|
17
|
+
# @param resolved [Array] Array of resolved tasks
|
18
|
+
# @return [self] Returns self for method chaining
|
19
|
+
def resolve(queue, resolved)
|
20
|
+
resolve_common(queue, resolved)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Resolve all dependencies in topological order
|
24
|
+
# @return [Array<Class>] Array of tasks in dependency order
|
25
|
+
def resolve_dependencies
|
26
|
+
resolve_dependencies_common
|
27
|
+
end
|
28
|
+
|
29
|
+
# Analyze dependencies when accessing interface methods
|
30
|
+
def analyze_dependencies_for_interfaces
|
31
|
+
interface_exports.each do |interface_method|
|
32
|
+
dependencies = gather_static_dependencies_for_interface(interface_method)
|
33
|
+
add_unique_dependencies(dependencies)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Gather dependencies from interface method implementation
|
40
|
+
def gather_static_dependencies_for_interface(interface_method)
|
41
|
+
# For sections, we analyze the impl method
|
42
|
+
# Try instance method first, then class method
|
43
|
+
if impl_defined?
|
44
|
+
# For instance method, we can't analyze dependencies statically
|
45
|
+
# So we return empty array
|
46
|
+
[]
|
47
|
+
else
|
48
|
+
DependencyAnalyzer.analyze_method(self, :impl)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Add dependencies that don't already exist
|
53
|
+
def add_unique_dependencies(dep_classes)
|
54
|
+
dep_classes.each do |dep_class|
|
55
|
+
next if dep_class == self || dependency_exists?(dep_class)
|
56
|
+
add_dependency(dep_class)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add a single dependency
|
61
|
+
def add_dependency(dep_class)
|
62
|
+
@dependencies ||= []
|
63
|
+
@dependencies << {klass: dep_class}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if dependency already exists
|
67
|
+
def dependency_exists?(dep_class)
|
68
|
+
(@dependencies || []).any? { |d| d[:klass] == dep_class }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Extract class from dependency specification
|
72
|
+
def extract_class(task)
|
73
|
+
case task
|
74
|
+
when Class
|
75
|
+
task
|
76
|
+
when Hash
|
77
|
+
task[:klass]
|
78
|
+
else
|
79
|
+
task
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
public
|
84
|
+
|
85
|
+
# === Instance Management (minimal for Section) ===
|
86
|
+
|
87
|
+
# Ensure section is available (no actual building needed)
|
88
|
+
# @return [self] Returns self for compatibility with Task interface
|
89
|
+
def ensure_instance_built
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
# Build method for compatibility (Section doesn't build instances)
|
94
|
+
# @param args [Hash] Optional arguments (ignored for sections)
|
95
|
+
# @return [self] Returns self
|
96
|
+
def build(**args)
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Reset method for compatibility (Section doesn't have state to reset)
|
101
|
+
# @return [self] Returns self
|
102
|
+
def reset!
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Display dependency tree for this section
|
107
|
+
# @param prefix [String] Current indentation prefix
|
108
|
+
# @param visited [Set] Set of visited classes to prevent infinite loops
|
109
|
+
# @param color [Boolean] Whether to use color output
|
110
|
+
# @return [String] Formatted dependency tree
|
111
|
+
def tree(prefix = "", visited = Set.new, color: TreeColors.enabled?)
|
112
|
+
should_return_early, early_result, new_visited = handle_circular_dependency_check(visited, self, prefix)
|
113
|
+
return early_result if should_return_early
|
114
|
+
|
115
|
+
# Get section name with fallback for anonymous classes
|
116
|
+
section_name = name || to_s
|
117
|
+
colored_section_name = color ? TreeColors.section(section_name) : section_name
|
118
|
+
result = "#{prefix}#{colored_section_name}\n"
|
119
|
+
|
120
|
+
# Add possible implementations (一般化 - detect from nested Task classes)
|
121
|
+
possible_implementations = find_possible_implementations
|
122
|
+
if possible_implementations.any?
|
123
|
+
impl_names = possible_implementations.map { |impl| extract_implementation_name(impl) }
|
124
|
+
impl_text = "[One of: #{impl_names.join(", ")}]"
|
125
|
+
colored_impl_text = color ? TreeColors.implementations(impl_text) : impl_text
|
126
|
+
connector = color ? TreeColors.connector("└── ") : "└── "
|
127
|
+
result += "#{prefix}#{connector}#{colored_impl_text}\n"
|
128
|
+
end
|
129
|
+
|
130
|
+
dependencies = @dependencies || []
|
131
|
+
result += render_dependencies_tree(dependencies, prefix, new_visited, color)
|
132
|
+
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
# Define interface methods for this section
|
137
|
+
def interface(*names)
|
138
|
+
if names.empty?
|
139
|
+
raise ArgumentError, "interface requires at least one method name"
|
140
|
+
end
|
141
|
+
|
142
|
+
@interface_exports = names
|
143
|
+
|
144
|
+
# Create accessor methods for each interface name
|
145
|
+
names.each do |name|
|
146
|
+
define_singleton_method(name) do
|
147
|
+
# Get implementation class
|
148
|
+
implementation_class = get_implementation_class
|
149
|
+
|
150
|
+
# Check if implementation is nil
|
151
|
+
if implementation_class.nil?
|
152
|
+
raise SectionImplementationError,
|
153
|
+
"impl returned nil. " \
|
154
|
+
"Make sure impl returns a Task class."
|
155
|
+
end
|
156
|
+
|
157
|
+
# Validate that it's a Task class
|
158
|
+
unless implementation_class.is_a?(Class) && implementation_class < Taski::Task
|
159
|
+
raise SectionImplementationError,
|
160
|
+
"impl must return a Task class, got #{implementation_class.class}. " \
|
161
|
+
"Make sure impl returns a class that inherits from Taski::Task."
|
162
|
+
end
|
163
|
+
|
164
|
+
# Build the implementation and call the method
|
165
|
+
implementation = implementation_class.build
|
166
|
+
|
167
|
+
begin
|
168
|
+
implementation.send(name)
|
169
|
+
rescue NoMethodError
|
170
|
+
raise SectionImplementationError,
|
171
|
+
"Implementation does not provide required method '#{name}'. " \
|
172
|
+
"Make sure the implementation class has a '#{name}' method or " \
|
173
|
+
"exports :#{name} declaration."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Get the interface exports for this section
|
180
|
+
def interface_exports
|
181
|
+
@interface_exports || []
|
182
|
+
end
|
183
|
+
|
184
|
+
# Check if impl method is defined (as instance method)
|
185
|
+
def impl_defined?
|
186
|
+
instance_methods(false).include?(:impl)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Get implementation class from instance method
|
190
|
+
def get_implementation_class
|
191
|
+
if impl_defined?
|
192
|
+
# Create a temporary instance to call impl method
|
193
|
+
allocate.impl
|
194
|
+
else
|
195
|
+
# Fall back to class method if exists
|
196
|
+
impl
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Override const_set to auto-add exports to nested Task classes
|
201
|
+
def const_set(name, value)
|
202
|
+
result = super
|
203
|
+
|
204
|
+
# If the constant is a Task class and we have interface exports,
|
205
|
+
# automatically add exports to avoid duplication
|
206
|
+
if value.is_a?(Class) && value < Taski::Task && !interface_exports.empty?
|
207
|
+
# Add exports declaration to the nested task
|
208
|
+
exports_list = interface_exports
|
209
|
+
value.class_eval do
|
210
|
+
exports(*exports_list)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
result
|
215
|
+
end
|
216
|
+
|
217
|
+
# Apply auto-exports to all nested Task classes
|
218
|
+
# Call this method after defining nested Task classes to automatically add exports
|
219
|
+
def apply_auto_exports
|
220
|
+
constants.each do |const_name|
|
221
|
+
const_value = const_get(const_name)
|
222
|
+
if const_value.is_a?(Class) && const_value < Taski::Task && !interface_exports.empty?
|
223
|
+
exports_list = interface_exports
|
224
|
+
const_value.class_eval do
|
225
|
+
exports(*exports_list) unless @exports_defined
|
226
|
+
@exports_defined = true
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Find possible implementation classes by scanning nested Task classes
|
233
|
+
def find_possible_implementations
|
234
|
+
task_classes = []
|
235
|
+
constants.each do |const_name|
|
236
|
+
const_value = const_get(const_name)
|
237
|
+
if task_class?(const_value)
|
238
|
+
task_classes << const_value
|
239
|
+
end
|
240
|
+
end
|
241
|
+
task_classes
|
242
|
+
end
|
243
|
+
|
244
|
+
# Extract readable name from implementation class
|
245
|
+
def extract_implementation_name(impl_class)
|
246
|
+
class_name = impl_class.name
|
247
|
+
return impl_class.to_s unless class_name&.include?("::")
|
248
|
+
|
249
|
+
class_name.split("::").last
|
250
|
+
end
|
251
|
+
|
252
|
+
# Check if a constant value is a Task class
|
253
|
+
def task_class?(const_value)
|
254
|
+
const_value.is_a?(Class) && const_value < Taski::Task
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
include Utils::TreeDisplayHelper
|
260
|
+
include Utils::DependencyResolverHelper
|
261
|
+
|
262
|
+
# Subclasses should override this method to select appropriate implementation
|
263
|
+
def impl
|
264
|
+
raise NotImplementedError, "Subclass must implement impl"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
data/lib/taski/task/base.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../exceptions"
|
4
|
+
require_relative "../utils/tree_display_helper"
|
4
5
|
|
5
6
|
module Taski
|
6
7
|
# Base Task class that provides the foundation for task framework
|
@@ -46,38 +47,15 @@ module Taski
|
|
46
47
|
# @param prefix [String] Current indentation prefix
|
47
48
|
# @param visited [Set] Set of visited classes to prevent infinite loops
|
48
49
|
# @return [String] Formatted dependency tree
|
49
|
-
def tree(prefix = "", visited = Set.new)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
dependencies.each_with_index do |dep, index|
|
59
|
-
dep_class = extract_class(dep)
|
60
|
-
is_last = index == dependencies.length - 1
|
61
|
-
|
62
|
-
connector = is_last ? "└── " : "├── "
|
63
|
-
child_prefix = prefix + (is_last ? " " : "│ ")
|
64
|
-
|
65
|
-
# For the dependency itself, we want to use the connector
|
66
|
-
# For its children, we want to use the child_prefix
|
67
|
-
dep_tree = dep_class.tree(child_prefix, visited)
|
68
|
-
# Replace the first line (which has child_prefix) with the proper connector
|
69
|
-
dep_lines = dep_tree.lines
|
70
|
-
if dep_lines.any?
|
71
|
-
# Replace the first line prefix with connector
|
72
|
-
first_line = dep_lines[0]
|
73
|
-
fixed_first_line = first_line.sub(/^#{Regexp.escape(child_prefix)}/, prefix + connector)
|
74
|
-
result += fixed_first_line
|
75
|
-
# Add the rest of the lines as-is
|
76
|
-
result += dep_lines[1..].join if dep_lines.length > 1
|
77
|
-
else
|
78
|
-
result += "#{prefix}#{connector}#{dep_class.name}\n"
|
79
|
-
end
|
80
|
-
end
|
50
|
+
def tree(prefix = "", visited = Set.new, color: TreeColors.enabled?)
|
51
|
+
should_return_early, early_result, new_visited = handle_circular_dependency_check(visited, self, prefix)
|
52
|
+
return early_result if should_return_early
|
53
|
+
|
54
|
+
task_name = color ? TreeColors.task(name) : name
|
55
|
+
result = "#{prefix}#{task_name}\n"
|
56
|
+
|
57
|
+
dependencies = @dependencies || []
|
58
|
+
result += render_dependencies_tree(dependencies, prefix, new_visited, color)
|
81
59
|
|
82
60
|
result
|
83
61
|
end
|
@@ -85,6 +63,7 @@ module Taski
|
|
85
63
|
private
|
86
64
|
|
87
65
|
include Utils::DependencyUtils
|
66
|
+
include Utils::TreeDisplayHelper
|
88
67
|
private :extract_class
|
89
68
|
end
|
90
69
|
|