taski 0.2.2 → 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 +130 -7
- 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 +104 -45
- 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 +20 -3
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../dependency_analyzer"
|
4
|
+
require_relative "../utils/dependency_resolver_helper"
|
4
5
|
|
5
6
|
module Taski
|
6
7
|
class Task
|
@@ -12,75 +13,13 @@ module Taski
|
|
12
13
|
# @param resolved [Array] Array of resolved tasks
|
13
14
|
# @return [self] Returns self for method chaining
|
14
15
|
def resolve(queue, resolved)
|
15
|
-
|
16
|
-
|
17
|
-
@dependencies.each do |task|
|
18
|
-
task_class = extract_class(task)
|
19
|
-
|
20
|
-
# Reorder in resolved list for correct priority
|
21
|
-
resolved.delete(task_class) if resolved.include?(task_class)
|
22
|
-
queue << task_class
|
23
|
-
end
|
24
|
-
|
25
|
-
# Create getter methods for defined values
|
26
|
-
create_defined_methods
|
27
|
-
|
28
|
-
self
|
16
|
+
resolve_common(queue, resolved, custom_hook: -> { create_defined_methods })
|
29
17
|
end
|
30
18
|
|
31
19
|
# Resolve all dependencies in topological order with circular dependency detection
|
32
20
|
# @return [Array<Class>] Array of tasks in dependency order
|
33
21
|
def resolve_dependencies
|
34
|
-
|
35
|
-
resolved = []
|
36
|
-
visited = Set.new
|
37
|
-
resolving = Set.new # Track currently resolving tasks
|
38
|
-
path_map = {self => []} # Track paths to each task
|
39
|
-
|
40
|
-
while queue.any?
|
41
|
-
task_class = queue.shift
|
42
|
-
next if visited.include?(task_class)
|
43
|
-
|
44
|
-
# Check for circular dependency
|
45
|
-
if resolving.include?(task_class)
|
46
|
-
# Build error message with path information
|
47
|
-
cycle_path = build_cycle_path(task_class, path_map)
|
48
|
-
raise CircularDependencyError, build_circular_dependency_message(cycle_path)
|
49
|
-
end
|
50
|
-
|
51
|
-
resolving << task_class
|
52
|
-
visited << task_class
|
53
|
-
|
54
|
-
# Store current path for dependencies
|
55
|
-
current_path = path_map[task_class] || []
|
56
|
-
|
57
|
-
# Let task resolve its dependencies
|
58
|
-
task_class.resolve(queue, resolved)
|
59
|
-
|
60
|
-
# Track paths for each dependency
|
61
|
-
task_class.instance_variable_get(:@dependencies)&.each do |dep|
|
62
|
-
dep_class = extract_class(dep)
|
63
|
-
path_map[dep_class] = current_path + [task_class] unless path_map.key?(dep_class)
|
64
|
-
end
|
65
|
-
|
66
|
-
resolving.delete(task_class)
|
67
|
-
resolved << task_class unless resolved.include?(task_class)
|
68
|
-
end
|
69
|
-
|
70
|
-
resolved
|
71
|
-
end
|
72
|
-
|
73
|
-
private
|
74
|
-
|
75
|
-
# Build the cycle path from path tracking information
|
76
|
-
def build_cycle_path(task_class, path_map)
|
77
|
-
path = path_map[task_class] || []
|
78
|
-
path + [task_class]
|
79
|
-
end
|
80
|
-
|
81
|
-
# Build detailed error message for circular dependencies
|
82
|
-
def build_circular_dependency_message(cycle_path)
|
83
|
-
Utils::CircularDependencyHelpers.build_error_message(cycle_path, "dependency")
|
22
|
+
resolve_dependencies_common
|
84
23
|
end
|
85
24
|
|
86
25
|
public
|
@@ -127,6 +66,7 @@ module Taski
|
|
127
66
|
private
|
128
67
|
|
129
68
|
include Utils::DependencyUtils
|
69
|
+
include Utils::DependencyResolverHelper
|
130
70
|
private :extract_class
|
131
71
|
end
|
132
72
|
end
|
@@ -89,21 +89,8 @@ module Taski
|
|
89
89
|
# Check again after acquiring lock
|
90
90
|
return @__task_instance if @__task_instance
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
if Thread.current[thread_key]
|
95
|
-
# Build dependency path for better error message
|
96
|
-
cycle_path = build_current_dependency_path
|
97
|
-
raise CircularDependencyError, build_runtime_circular_dependency_message(cycle_path)
|
98
|
-
end
|
99
|
-
|
100
|
-
Thread.current[thread_key] = true
|
101
|
-
begin
|
102
|
-
build_dependencies
|
103
|
-
@__task_instance = build_instance
|
104
|
-
ensure
|
105
|
-
Thread.current[thread_key] = false
|
106
|
-
end
|
92
|
+
check_circular_dependency
|
93
|
+
create_and_build_instance
|
107
94
|
end
|
108
95
|
|
109
96
|
@__task_instance
|
@@ -111,6 +98,32 @@ module Taski
|
|
111
98
|
|
112
99
|
private
|
113
100
|
|
101
|
+
# === Instance Management Helper Methods ===
|
102
|
+
|
103
|
+
# Check for circular dependencies and raise error if detected
|
104
|
+
# @raise [CircularDependencyError] If circular dependency is detected
|
105
|
+
def check_circular_dependency
|
106
|
+
thread_key = build_thread_key
|
107
|
+
if Thread.current[thread_key]
|
108
|
+
# Build dependency path for better error message
|
109
|
+
cycle_path = build_current_dependency_path
|
110
|
+
raise CircularDependencyError, build_runtime_circular_dependency_message(cycle_path)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Create and build instance with proper thread-local state management
|
115
|
+
# @return [void] Sets @__task_instance
|
116
|
+
def create_and_build_instance
|
117
|
+
thread_key = build_thread_key
|
118
|
+
Thread.current[thread_key] = true
|
119
|
+
begin
|
120
|
+
build_dependencies
|
121
|
+
@__task_instance = build_instance
|
122
|
+
ensure
|
123
|
+
Thread.current[thread_key] = false
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
114
127
|
# === Core Helper Methods ===
|
115
128
|
|
116
129
|
# Get or create build monitor for thread safety
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
# Color utilities for tree display
|
5
|
+
# Provides ANSI color codes for enhanced tree visualization
|
6
|
+
class TreeColors
|
7
|
+
# ANSI color codes
|
8
|
+
COLORS = {
|
9
|
+
red: "\e[31m",
|
10
|
+
green: "\e[32m",
|
11
|
+
yellow: "\e[33m",
|
12
|
+
blue: "\e[34m",
|
13
|
+
magenta: "\e[35m",
|
14
|
+
cyan: "\e[36m",
|
15
|
+
gray: "\e[90m",
|
16
|
+
reset: "\e[0m",
|
17
|
+
bold: "\e[1m"
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# Check if colors should be enabled
|
22
|
+
# @return [Boolean] true if colors should be used
|
23
|
+
def enabled?
|
24
|
+
return @enabled unless @enabled.nil?
|
25
|
+
@enabled = tty? && !no_color?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Enable or disable colors
|
29
|
+
# @param value [Boolean] whether to enable colors
|
30
|
+
attr_writer :enabled
|
31
|
+
|
32
|
+
# Colorize text for Section names (blue)
|
33
|
+
# @param text [String] text to colorize
|
34
|
+
# @return [String] colorized text
|
35
|
+
def section(text)
|
36
|
+
colorize(text, :blue, bold: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Colorize text for Task names (green)
|
40
|
+
# @param text [String] text to colorize
|
41
|
+
# @return [String] colorized text
|
42
|
+
def task(text)
|
43
|
+
colorize(text, :green)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Colorize text for implementation candidates (yellow)
|
47
|
+
# @param text [String] text to colorize
|
48
|
+
# @return [String] colorized text
|
49
|
+
def implementations(text)
|
50
|
+
colorize(text, :yellow)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Colorize tree connectors (gray)
|
54
|
+
# @param text [String] text to colorize
|
55
|
+
# @return [String] colorized text
|
56
|
+
def connector(text)
|
57
|
+
colorize(text, :gray)
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Apply color to text
|
63
|
+
# @param text [String] text to colorize
|
64
|
+
# @param color [Symbol] color name
|
65
|
+
# @param bold [Boolean] whether to make text bold
|
66
|
+
# @return [String] colorized text
|
67
|
+
def colorize(text, color, bold: false)
|
68
|
+
return text unless enabled?
|
69
|
+
|
70
|
+
result = ""
|
71
|
+
result += COLORS[:bold] if bold
|
72
|
+
result += COLORS[color]
|
73
|
+
result += text
|
74
|
+
result += COLORS[:reset]
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
# Check if output is a TTY
|
79
|
+
# @return [Boolean] true if stdout is a TTY
|
80
|
+
def tty?
|
81
|
+
$stdout.tty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Check if NO_COLOR environment variable is set
|
85
|
+
# @return [Boolean] true if colors should be disabled
|
86
|
+
def no_color?
|
87
|
+
ENV.key?("NO_COLOR")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Utils
|
5
|
+
# Helper module for dependency resolution functionality
|
6
|
+
# Provides common logic for resolving dependencies and detecting circular dependencies
|
7
|
+
module DependencyResolverHelper
|
8
|
+
private
|
9
|
+
|
10
|
+
# Resolve all dependencies in topological order with circular dependency detection
|
11
|
+
# @return [Array<Class>] Array of tasks in dependency order
|
12
|
+
def resolve_dependencies_common
|
13
|
+
queue = [self]
|
14
|
+
resolved = []
|
15
|
+
visited = Set.new
|
16
|
+
resolving = Set.new
|
17
|
+
path_map = {self => []}
|
18
|
+
|
19
|
+
while queue.any?
|
20
|
+
task_class = queue.shift
|
21
|
+
next if visited.include?(task_class)
|
22
|
+
|
23
|
+
if resolving.include?(task_class)
|
24
|
+
cycle_path = build_cycle_path(task_class, path_map)
|
25
|
+
raise CircularDependencyError, build_circular_dependency_message(cycle_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
resolving << task_class
|
29
|
+
visited << task_class
|
30
|
+
|
31
|
+
current_path = path_map[task_class] || []
|
32
|
+
task_class.resolve(queue, resolved)
|
33
|
+
|
34
|
+
task_class.instance_variable_get(:@dependencies)&.each do |dep|
|
35
|
+
dep_class = extract_class(dep)
|
36
|
+
path_map[dep_class] = current_path + [task_class] unless path_map.key?(dep_class)
|
37
|
+
end
|
38
|
+
|
39
|
+
resolving.delete(task_class)
|
40
|
+
resolved << task_class unless resolved.include?(task_class)
|
41
|
+
end
|
42
|
+
|
43
|
+
resolved
|
44
|
+
end
|
45
|
+
|
46
|
+
# Resolve method for dependency graph (called by resolve_dependencies)
|
47
|
+
# @param queue [Array] Queue of tasks to process
|
48
|
+
# @param resolved [Array] Array of resolved tasks
|
49
|
+
# @param options [Hash] Optional parameters for customization
|
50
|
+
# @return [self] Returns self for method chaining
|
51
|
+
def resolve_common(queue, resolved, options = {})
|
52
|
+
@dependencies ||= []
|
53
|
+
|
54
|
+
@dependencies.each do |task|
|
55
|
+
task_class = extract_class(task)
|
56
|
+
|
57
|
+
# Reorder in resolved list for correct priority
|
58
|
+
resolved.delete(task_class) if resolved.include?(task_class)
|
59
|
+
queue << task_class
|
60
|
+
end
|
61
|
+
|
62
|
+
# Call custom hook if provided
|
63
|
+
options[:custom_hook]&.call
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Build the cycle path from path tracking information
|
69
|
+
# @param task_class [Class] Current task class
|
70
|
+
# @param path_map [Hash] Map of paths to each task
|
71
|
+
# @return [Array] Cycle path array
|
72
|
+
def build_cycle_path(task_class, path_map)
|
73
|
+
path = path_map[task_class] || []
|
74
|
+
path + [task_class]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Build detailed error message for circular dependencies
|
78
|
+
# @param cycle_path [Array] Array representing the circular dependency path
|
79
|
+
# @return [String] Formatted error message
|
80
|
+
def build_circular_dependency_message(cycle_path)
|
81
|
+
Utils::CircularDependencyHelpers.build_error_message(cycle_path, "dependency")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|