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.
@@ -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
- @dependencies ||= []
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
- queue = [self]
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
- # Prevent infinite recursion using thread-local storage
93
- thread_key = build_thread_key
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.2.3"
4
+ VERSION = "0.3.0"
5
5
  end
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.2.3
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