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,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
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
module Utils
|
5
|
+
# Helper module for tree display functionality
|
6
|
+
# Provides common logic for displaying dependency trees
|
7
|
+
module TreeDisplayHelper
|
8
|
+
private
|
9
|
+
|
10
|
+
# Render dependencies as tree structure
|
11
|
+
# @param dependencies [Array] Array of dependency objects
|
12
|
+
# @param prefix [String] Current indentation prefix
|
13
|
+
# @param visited [Set] Set of visited classes
|
14
|
+
# @param color [Boolean] Whether to use color output
|
15
|
+
# @return [String] Formatted dependency tree string
|
16
|
+
def render_dependencies_tree(dependencies, prefix, visited, color)
|
17
|
+
result = ""
|
18
|
+
|
19
|
+
dependencies = dependencies.uniq { |dep| extract_class(dep) }
|
20
|
+
dependencies.each_with_index do |dep, index|
|
21
|
+
dep_class = extract_class(dep)
|
22
|
+
is_last = index == dependencies.length - 1
|
23
|
+
|
24
|
+
connector_text = is_last ? "└── " : "├── "
|
25
|
+
connector = color ? TreeColors.connector(connector_text) : connector_text
|
26
|
+
child_prefix_text = is_last ? " " : "│ "
|
27
|
+
child_prefix = prefix + (color ? TreeColors.connector(child_prefix_text) : child_prefix_text)
|
28
|
+
|
29
|
+
# For the dependency itself, we want to use the connector
|
30
|
+
# For its children, we want to use the child_prefix
|
31
|
+
dep_tree = if dep_class.respond_to?(:tree)
|
32
|
+
dep_class.tree(child_prefix, visited, color: color)
|
33
|
+
else
|
34
|
+
"#{child_prefix}#{dep_class.name}\n"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Replace the first line (which has child_prefix) with the proper connector
|
38
|
+
dep_lines = dep_tree.lines
|
39
|
+
if dep_lines.any?
|
40
|
+
# Replace the first line prefix with connector
|
41
|
+
first_line = dep_lines[0]
|
42
|
+
fixed_first_line = first_line.sub(/^#{Regexp.escape(child_prefix)}/, prefix + connector)
|
43
|
+
result += fixed_first_line
|
44
|
+
# Add the rest of the lines as-is
|
45
|
+
result += dep_lines[1..].join if dep_lines.length > 1
|
46
|
+
else
|
47
|
+
dep_name = color ? TreeColors.task(dep_class.name) : dep_class.name
|
48
|
+
result += "#{prefix}#{connector}#{dep_name}\n"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
# Check for circular dependencies and handle visited set
|
56
|
+
# @param visited [Set] Set of visited classes
|
57
|
+
# @param current_class [Class] Current class being processed
|
58
|
+
# @param prefix [String] Current indentation prefix
|
59
|
+
# @return [Array] Returns [should_return_early, result_string, new_visited_set]
|
60
|
+
def handle_circular_dependency_check(visited, current_class, prefix)
|
61
|
+
if visited.include?(current_class)
|
62
|
+
return [true, "#{prefix}#{current_class.name} (circular)\n", visited]
|
63
|
+
end
|
64
|
+
|
65
|
+
new_visited = visited.dup
|
66
|
+
new_visited << current_class
|
67
|
+
[false, nil, new_visited]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
@@ -10,6 +10,7 @@ require_relative "taski/progress_display"
|
|
10
10
|
require_relative "taski/reference"
|
11
11
|
require_relative "taski/dependency_analyzer"
|
12
12
|
require_relative "taski/utils"
|
13
|
+
require_relative "taski/tree_colors"
|
13
14
|
|
14
15
|
# Load Task class components
|
15
16
|
require_relative "taski/task/base"
|
@@ -18,6 +19,9 @@ require_relative "taski/task/define_api"
|
|
18
19
|
require_relative "taski/task/instance_management"
|
19
20
|
require_relative "taski/task/dependency_resolver"
|
20
21
|
|
22
|
+
# Load Section class
|
23
|
+
require_relative "taski/section"
|
24
|
+
|
21
25
|
module Taski
|
22
26
|
# Main module for the Taski task framework
|
23
27
|
#
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taski
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ahogappa
|
@@ -43,19 +43,36 @@ files:
|
|
43
43
|
- examples/advanced_patterns.rb
|
44
44
|
- examples/progress_demo.rb
|
45
45
|
- examples/quick_start.rb
|
46
|
+
- examples/section_configuration.rb
|
46
47
|
- examples/tree_demo.rb
|
47
48
|
- lib/taski.rb
|
48
49
|
- lib/taski/dependency_analyzer.rb
|
49
50
|
- lib/taski/exceptions.rb
|
50
51
|
- lib/taski/logger.rb
|
52
|
+
- lib/taski/logging/formatter_factory.rb
|
53
|
+
- lib/taski/logging/formatter_interface.rb
|
54
|
+
- lib/taski/logging/json_formatter.rb
|
55
|
+
- lib/taski/logging/simple_formatter.rb
|
56
|
+
- lib/taski/logging/structured_formatter.rb
|
57
|
+
- lib/taski/progress/display_colors.rb
|
58
|
+
- lib/taski/progress/display_manager.rb
|
59
|
+
- lib/taski/progress/output_capture.rb
|
60
|
+
- lib/taski/progress/spinner_animation.rb
|
61
|
+
- lib/taski/progress/task_formatter.rb
|
62
|
+
- lib/taski/progress/task_status.rb
|
63
|
+
- lib/taski/progress/terminal_controller.rb
|
51
64
|
- lib/taski/progress_display.rb
|
52
65
|
- lib/taski/reference.rb
|
66
|
+
- lib/taski/section.rb
|
53
67
|
- lib/taski/task/base.rb
|
54
68
|
- lib/taski/task/define_api.rb
|
55
69
|
- lib/taski/task/dependency_resolver.rb
|
56
70
|
- lib/taski/task/exports_api.rb
|
57
71
|
- lib/taski/task/instance_management.rb
|
72
|
+
- lib/taski/tree_colors.rb
|
58
73
|
- lib/taski/utils.rb
|
74
|
+
- lib/taski/utils/dependency_resolver_helper.rb
|
75
|
+
- lib/taski/utils/tree_display_helper.rb
|
59
76
|
- lib/taski/version.rb
|
60
77
|
- sig/taski.rbs
|
61
78
|
homepage: https://github.com/ahogappa/taski
|