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.
@@ -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
@@ -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
- return "#{prefix}#{name} (circular)\n" if visited.include?(self)
51
-
52
- visited = visited.dup
53
- visited << self
54
-
55
- result = "#{prefix}#{name}\n"
56
-
57
- dependencies = (@dependencies || []).uniq { |dep| extract_class(dep) }
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
- @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