taski 0.3.1 → 0.4.1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -271
  3. data/Steepfile +19 -0
  4. data/docs/advanced-features.md +625 -0
  5. data/docs/api-guide.md +509 -0
  6. data/docs/error-handling.md +684 -0
  7. data/examples/README.md +98 -42
  8. data/examples/context_demo.rb +118 -0
  9. data/examples/data_pipeline_demo.rb +231 -0
  10. data/examples/parallel_progress_demo.rb +72 -0
  11. data/examples/quick_start.rb +4 -4
  12. data/examples/reexecution_demo.rb +127 -0
  13. data/examples/{section_configuration.rb → section_demo.rb} +49 -60
  14. data/lib/taski/context.rb +50 -0
  15. data/lib/taski/execution/coordinator.rb +63 -0
  16. data/lib/taski/execution/parallel_progress_display.rb +201 -0
  17. data/lib/taski/execution/registry.rb +72 -0
  18. data/lib/taski/execution/task_wrapper.rb +255 -0
  19. data/lib/taski/section.rb +26 -254
  20. data/lib/taski/static_analysis/analyzer.rb +46 -0
  21. data/lib/taski/static_analysis/dependency_graph.rb +90 -0
  22. data/lib/taski/static_analysis/visitor.rb +130 -0
  23. data/lib/taski/task.rb +199 -0
  24. data/lib/taski/version.rb +1 -1
  25. data/lib/taski.rb +68 -39
  26. data/rbs_collection.lock.yaml +116 -0
  27. data/rbs_collection.yaml +19 -0
  28. data/sig/taski.rbs +269 -62
  29. metadata +36 -32
  30. data/examples/advanced_patterns.rb +0 -119
  31. data/examples/progress_demo.rb +0 -166
  32. data/examples/tree_demo.rb +0 -205
  33. data/lib/taski/dependency_analyzer.rb +0 -232
  34. data/lib/taski/exceptions.rb +0 -17
  35. data/lib/taski/logger.rb +0 -158
  36. data/lib/taski/logging/formatter_factory.rb +0 -34
  37. data/lib/taski/logging/formatter_interface.rb +0 -19
  38. data/lib/taski/logging/json_formatter.rb +0 -26
  39. data/lib/taski/logging/simple_formatter.rb +0 -16
  40. data/lib/taski/logging/structured_formatter.rb +0 -44
  41. data/lib/taski/progress/display_colors.rb +0 -17
  42. data/lib/taski/progress/display_manager.rb +0 -117
  43. data/lib/taski/progress/output_capture.rb +0 -105
  44. data/lib/taski/progress/spinner_animation.rb +0 -49
  45. data/lib/taski/progress/task_formatter.rb +0 -25
  46. data/lib/taski/progress/task_status.rb +0 -38
  47. data/lib/taski/progress/terminal_controller.rb +0 -35
  48. data/lib/taski/progress_display.rb +0 -57
  49. data/lib/taski/reference.rb +0 -40
  50. data/lib/taski/task/base.rb +0 -91
  51. data/lib/taski/task/define_api.rb +0 -156
  52. data/lib/taski/task/dependency_resolver.rb +0 -73
  53. data/lib/taski/task/exports_api.rb +0 -29
  54. data/lib/taski/task/instance_management.rb +0 -201
  55. data/lib/taski/tree_colors.rb +0 -91
  56. data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
  57. data/lib/taski/utils/tree_display_helper.rb +0 -68
  58. data/lib/taski/utils.rb +0 -107
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "progress/terminal_controller"
4
- require_relative "progress/spinner_animation"
5
- require_relative "progress/output_capture"
6
- require_relative "progress/display_manager"
7
-
8
- module Taski
9
- # Backward compatibility aliases
10
- TerminalController = Progress::TerminalController
11
- SpinnerAnimation = Progress::SpinnerAnimation
12
- OutputCapture = Progress::OutputCapture
13
- TaskStatus = Progress::TaskStatus
14
-
15
- # Main progress display controller - refactored for better separation of concerns
16
- class ProgressDisplay
17
- def initialize(output: $stdout, enable: true, include_captured_output: nil)
18
- @output = output
19
- @terminal = Progress::TerminalController.new(output)
20
- @spinner = Progress::SpinnerAnimation.new
21
- @output_capture = Progress::OutputCapture.new(output)
22
-
23
- include_captured_output = include_captured_output.nil? ? (output != $stdout) : include_captured_output
24
- @display_manager = Progress::DisplayManager.new(@terminal, @spinner, @output_capture, include_captured_output: include_captured_output)
25
-
26
- @enabled = ENV["TASKI_PROGRESS_DISABLE"] != "1" && enable
27
- end
28
-
29
- def start_task(task_name, dependencies: [])
30
- return unless @enabled
31
-
32
- @display_manager.start_task_display(task_name)
33
- end
34
-
35
- def complete_task(task_name, duration:)
36
- return unless @enabled
37
-
38
- @display_manager.complete_task_display(task_name, duration: duration)
39
- end
40
-
41
- def fail_task(task_name, error:, duration:)
42
- return unless @enabled
43
-
44
- @display_manager.fail_task_display(task_name, error: error, duration: duration)
45
- end
46
-
47
- def clear
48
- return unless @enabled
49
-
50
- @display_manager.clear_all_displays
51
- end
52
-
53
- def enabled?
54
- @enabled
55
- end
56
- end
57
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "exceptions"
4
-
5
- module Taski
6
- # Reference class for task references
7
- #
8
- # Used to create lazy references to task classes by name,
9
- # which is useful for dependency tracking and metaprogramming.
10
- class Reference
11
- # @param klass [String] The name of the class to reference
12
- def initialize(klass)
13
- @klass = klass
14
- end
15
-
16
- # Dereference to get the actual class object
17
- # @return [Class] The referenced class
18
- # @raise [TaskAnalysisError] If the constant cannot be resolved
19
- def deref
20
- Object.const_get(@klass)
21
- rescue NameError => e
22
- raise TaskAnalysisError, "Cannot resolve constant '#{@klass}': #{e.message}"
23
- end
24
-
25
- # Compare reference with another object
26
- # @param other [Object] Object to compare with
27
- # @return [Boolean] True if the referenced class equals the other object
28
- def ==(other)
29
- Object.const_get(@klass) == other
30
- rescue NameError
31
- false
32
- end
33
-
34
- # String representation of the reference
35
- # @return [String] Reference representation
36
- def inspect
37
- "&#{@klass}"
38
- end
39
- end
40
- end
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../exceptions"
4
- require_relative "../utils/tree_display_helper"
5
-
6
- module Taski
7
- # Base Task class that provides the foundation for task framework
8
- # This module contains the core constants and basic structure
9
- class Task
10
- # Constants for thread-local keys and method tracking
11
- THREAD_KEY_SUFFIX = "_building"
12
- TASKI_ANALYZING_DEFINE_KEY = :taski_analyzing_define
13
- ANALYZED_METHODS = [:build, :clean].freeze
14
-
15
- class << self
16
- # === Hook Methods ===
17
-
18
- # Hook called when build/clean methods are defined
19
- # This triggers static analysis of dependencies
20
- def method_added(method_name)
21
- super
22
- return unless ANALYZED_METHODS.include?(method_name)
23
- # Avoid calling before dependency_resolver module is loaded
24
- analyze_dependencies_at_definition if respond_to?(:analyze_dependencies_at_definition, true)
25
- end
26
-
27
- # Create a reference to a task class (can be used anywhere)
28
- # @param klass [String] The class name to reference
29
- # @return [Reference] A reference object
30
- def ref(klass)
31
- reference = Reference.new(klass)
32
- # Use throw/catch mechanism for dependency collection during define API analysis
33
- # This allows catching unresolved references without unwinding the entire call stack
34
- if Thread.current[TASKI_ANALYZING_DEFINE_KEY]
35
- reference.tap { |ref| throw :unresolved, ref }
36
- else
37
- reference
38
- end
39
- end
40
-
41
- # Get or create resolution state for define API
42
- # @return [Hash] Resolution state hash
43
- def __resolve__
44
- @__resolve__ ||= {}
45
- end
46
-
47
- # Display dependency tree for this task
48
- # @param prefix [String] Current indentation prefix
49
- # @param visited [Set] Set of visited classes to prevent infinite loops
50
- # @return [String] Formatted dependency tree
51
- def tree(prefix = "", visited = Set.new, color: TreeColors.enabled?)
52
- should_return_early, early_result, new_visited = handle_circular_dependency_check(visited, self, prefix)
53
- return early_result if should_return_early
54
-
55
- task_name = color ? TreeColors.task(name) : name
56
- result = "#{prefix}#{task_name}\n"
57
-
58
- dependencies = @dependencies || []
59
- result += render_dependencies_tree(dependencies, prefix, new_visited, color)
60
-
61
- result
62
- end
63
-
64
- private
65
-
66
- include Utils::DependencyUtils
67
- include Utils::TreeDisplayHelper
68
- private :extract_class
69
- end
70
-
71
- # === Instance Methods ===
72
-
73
- # Build method that must be implemented by subclasses
74
- # @raise [NotImplementedError] If not implemented by subclass
75
- def build
76
- raise NotImplementedError, "You must implement the build method in your task class"
77
- end
78
-
79
- # Access build arguments passed to parametrized builds
80
- # @return [Hash] Build arguments or empty hash if none provided
81
- def build_args
82
- @build_args || {}
83
- end
84
-
85
- # Clean method with default empty implementation
86
- # Subclasses can override this method to implement cleanup logic
87
- def clean
88
- # Default implementation does nothing - allows optional cleanup in subclasses
89
- end
90
- end
91
- end
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Taski
4
- class Task
5
- class << self
6
- # === Define API ===
7
- # Define lazy-evaluated values with dynamic dependency resolution
8
-
9
- # Define a lazy-evaluated value using a block
10
- # Use this API when dependencies change based on runtime conditions,
11
- # environment-specific configurations, feature flags, or complex conditional logic
12
- # @param name [Symbol] Name of the value
13
- # @param block [Proc] Block that computes the value and determines dependencies at runtime
14
- # @param options [Hash] Additional options
15
- def define(name, block, **options)
16
- @dependencies ||= []
17
- @definitions ||= {}
18
-
19
- # Enable forward declarations by creating ref method on first define usage
20
- # This allows tasks to reference other tasks before they're defined
21
- create_ref_method_if_needed
22
-
23
- # Create method that tracks dependencies on first call
24
- create_tracking_method(name)
25
-
26
- # Analyze dependencies by executing the block
27
- dependencies = analyze_define_dependencies(block)
28
-
29
- @dependencies += dependencies
30
- @definitions[name] = {block:, options:, classes: dependencies}
31
- end
32
-
33
- private
34
-
35
- # === Define API Implementation ===
36
-
37
- # Create ref method if needed to avoid redefinition warnings
38
- def create_ref_method_if_needed
39
- return if method_defined_for_define?(:ref)
40
-
41
- define_singleton_method(:ref) do |klass_name|
42
- # During dependency analysis, track as dependency but defer resolution
43
- if Thread.current[TASKI_ANALYZING_DEFINE_KEY]
44
- # Create Reference object for deferred resolution
45
- reference = Taski::Reference.new(klass_name)
46
-
47
- # Track as dependency by throwing unresolved
48
- throw :unresolved, [reference, :deref]
49
- else
50
- # At runtime, resolve to actual class
51
- Object.const_get(klass_name)
52
- end
53
- end
54
- mark_method_as_defined(:ref)
55
- end
56
-
57
- # Create method that tracks dependencies for define API
58
- # @param name [Symbol] Method name to create
59
- def create_tracking_method(name)
60
- # Only create tracking method during dependency analysis
61
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
62
- def self.#{name}
63
- __resolve__[__callee__] ||= false
64
- if __resolve__[__callee__]
65
- # already resolved - prevents infinite recursion
66
- else
67
- __resolve__[__callee__] = true
68
- throw :unresolved, [self, __callee__]
69
- end
70
- end
71
- RUBY
72
- end
73
-
74
- # Analyze dependencies in define block
75
- # @param block [Proc] Block to analyze
76
- # @return [Array<Hash>] Array of dependency information
77
- def analyze_define_dependencies(block)
78
- classes = []
79
-
80
- # Set flag to indicate we're analyzing define dependencies
81
- Thread.current[TASKI_ANALYZING_DEFINE_KEY] = true
82
-
83
- loop do
84
- klass, task = catch(:unresolved) do
85
- block.call
86
- nil
87
- end
88
-
89
- break if klass.nil?
90
-
91
- classes << {klass:, task:}
92
- end
93
-
94
- # Reset resolution state
95
- classes.each do |task_class|
96
- klass = task_class[:klass]
97
- # Reference objects are stateless but Task classes store analysis state
98
- # Selective reset prevents errors while ensuring clean state for next analysis
99
- if klass.respond_to?(:instance_variable_set) && !klass.is_a?(Taski::Reference)
100
- klass.instance_variable_set(:@__resolve__, {})
101
- end
102
- end
103
-
104
- classes
105
- ensure
106
- Thread.current[TASKI_ANALYZING_DEFINE_KEY] = false
107
- end
108
-
109
- # Create methods for values defined with define API
110
- def create_defined_methods
111
- @definitions ||= {}
112
- @definitions.each do |name, definition|
113
- create_defined_method(name, definition) unless method_defined_for_define?(name)
114
- end
115
- end
116
-
117
- # Create a single defined method (both class and instance)
118
- # @param name [Symbol] Method name
119
- # @param definition [Hash] Method definition information
120
- def create_defined_method(name, definition)
121
- # Remove tracking method first to avoid redefinition warnings
122
- singleton_class.undef_method(name) if singleton_class.method_defined?(name)
123
-
124
- # Class method with lazy evaluation
125
- define_singleton_method(name) do
126
- @__defined_values ||= {}
127
- @__defined_values[name] ||= definition[:block].call
128
- end
129
-
130
- # Instance method that delegates to class method
131
- define_method(name) do
132
- @__defined_values ||= {}
133
- @__defined_values[name] ||= self.class.send(name)
134
- end
135
-
136
- # Mark as defined for this resolution
137
- mark_method_as_defined(name)
138
- end
139
-
140
- # Mark method as defined for this resolution cycle
141
- # @param method_name [Symbol] Method name to mark
142
- def mark_method_as_defined(method_name)
143
- @__defined_for_resolve ||= Set.new
144
- @__defined_for_resolve << method_name
145
- end
146
-
147
- # Check if method was already defined for define API
148
- # @param method_name [Symbol] Method name to check
149
- # @return [Boolean] True if already defined
150
- def method_defined_for_define?(method_name)
151
- @__defined_for_resolve ||= Set.new
152
- @__defined_for_resolve.include?(method_name)
153
- end
154
- end
155
- end
156
- end
@@ -1,73 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../dependency_analyzer"
4
- require_relative "../utils/dependency_resolver_helper"
5
-
6
- module Taski
7
- class Task
8
- class << self
9
- # === Dependency Resolution ===
10
-
11
- # Resolve method for dependency graph (called by resolve_dependencies)
12
- # @param queue [Array] Queue of tasks to process
13
- # @param resolved [Array] Array of resolved tasks
14
- # @return [self] Returns self for method chaining
15
- def resolve(queue, resolved)
16
- resolve_common(queue, resolved, custom_hook: -> { create_defined_methods })
17
- end
18
-
19
- # Resolve all dependencies in topological order with circular dependency detection
20
- # @return [Array<Class>] Array of tasks in dependency order
21
- def resolve_dependencies
22
- resolve_dependencies_common
23
- end
24
-
25
- public
26
-
27
- # === Static Analysis ===
28
-
29
- # Analyze dependencies when methods are defined
30
- def analyze_dependencies_at_definition
31
- dependencies = gather_static_dependencies
32
- add_unique_dependencies(dependencies)
33
- end
34
-
35
- # Gather dependencies from build and clean methods
36
- # @return [Array<Class>] Array of dependency classes
37
- def gather_static_dependencies
38
- build_deps = DependencyAnalyzer.analyze_method(self, :build)
39
- clean_deps = DependencyAnalyzer.analyze_method(self, :clean)
40
- (build_deps + clean_deps).uniq
41
- end
42
-
43
- # Add dependencies that don't already exist
44
- # @param dep_classes [Array<Class>] Array of dependency classes
45
- def add_unique_dependencies(dep_classes)
46
- dep_classes.each do |dep_class|
47
- next if dep_class == self || dependency_exists?(dep_class)
48
- add_dependency(dep_class)
49
- end
50
- end
51
-
52
- # Add a single dependency
53
- # @param dep_class [Class] Dependency class to add
54
- def add_dependency(dep_class)
55
- @dependencies ||= []
56
- @dependencies << {klass: dep_class}
57
- end
58
-
59
- # Check if dependency already exists
60
- # @param dep_class [Class] Dependency class to check
61
- # @return [Boolean] True if dependency exists
62
- def dependency_exists?(dep_class)
63
- (@dependencies || []).any? { |d| d[:klass] == dep_class }
64
- end
65
-
66
- private
67
-
68
- include Utils::DependencyUtils
69
- include Utils::DependencyResolverHelper
70
- private :extract_class
71
- end
72
- end
73
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Taski
4
- class Task
5
- class << self
6
- # === Exports API ===
7
- # Export instance variables as class methods for static dependencies
8
-
9
- # Export instance variables as both class and instance methods
10
- # @param names [Array<Symbol>] Names of instance variables to export
11
- def exports(*names)
12
- @exports ||= []
13
- @exports += names
14
-
15
- names.each do |name|
16
- next if respond_to?(name)
17
-
18
- define_singleton_method(name) do
19
- ensure_instance_built.send(name)
20
- end
21
-
22
- define_method(name) do
23
- instance_variable_get("@#{name}")
24
- end
25
- end
26
- end
27
- end
28
- end
29
- end
@@ -1,201 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "monitor"
4
-
5
- module Taski
6
- class Task
7
- class << self
8
- # === Lifecycle Management ===
9
-
10
- # Build this task and all its dependencies
11
- # @param args [Hash] Optional arguments for parametrized builds
12
- # @return [Task] Returns task instance (singleton or temporary)
13
- def build(**args)
14
- if args.empty?
15
- resolve_dependencies.reverse_each do |task_class|
16
- task_class.ensure_instance_built
17
- end
18
- # Return the singleton instance for consistency
19
- instance_variable_get(:@__task_instance)
20
- else
21
- build_with_args(args)
22
- end
23
- end
24
-
25
- # Clean this task and all its dependencies in reverse order
26
- def clean
27
- resolve_dependencies.each do |task_class|
28
- # Get existing instance or create new one for cleaning
29
- instance = task_class.instance_variable_get(:@__task_instance) || task_class.new
30
- instance.clean
31
- end
32
- end
33
-
34
- # Reset task instance and cached data to prevent memory leaks
35
- # @return [self] Returns self for method chaining
36
- def reset!
37
- build_monitor.synchronize do
38
- @__task_instance = nil
39
- @__defined_values = nil
40
- @__defined_for_resolve = nil
41
- clear_thread_local_state
42
- end
43
- self
44
- end
45
-
46
- # Refresh task state
47
- # @return [self] Returns self for method chaining
48
- def refresh
49
- reset!
50
- end
51
-
52
- # === Parametrized Build Support ===
53
-
54
- # Build temporary instance with arguments
55
- # @param args [Hash] Build arguments
56
- # @return [Task] Temporary task instance
57
- def build_with_args(args)
58
- # Resolve dependencies first (same as normal build)
59
- resolve_dependencies.reverse_each do |task_class|
60
- task_class.ensure_instance_built
61
- end
62
-
63
- # Create temporary instance with arguments
64
- temp_instance = new
65
- temp_instance.instance_variable_set(:@build_args, args)
66
-
67
- # Build with logging using common utility
68
- Utils::TaskBuildHelpers.with_build_logging(name.to_s,
69
- dependencies: @dependencies || [],
70
- args: args) do
71
- temp_instance.build
72
- temp_instance
73
- end
74
- end
75
-
76
- private :build_with_args
77
-
78
- # === Instance Management ===
79
-
80
- # Ensure task instance is built (public because called from build)
81
- # @return [Task] The built task instance
82
- def ensure_instance_built
83
- # Double-checked locking prevents lock contention in multi-threaded builds
84
- # First check avoids expensive synchronization when instance already exists
85
- return @__task_instance if @__task_instance
86
-
87
- build_monitor.synchronize do
88
- return @__task_instance if @__task_instance
89
-
90
- check_circular_dependency
91
- create_and_build_instance
92
- end
93
-
94
- @__task_instance
95
- end
96
-
97
- private
98
-
99
- # === Instance Management Helper Methods ===
100
-
101
- # Check for circular dependencies and raise error if detected
102
- # @raise [CircularDependencyError] If circular dependency is detected
103
- def check_circular_dependency
104
- thread_key = build_thread_key
105
- if Thread.current[thread_key]
106
- # Build dependency path for better error message
107
- cycle_path = build_current_dependency_path
108
- raise CircularDependencyError, build_runtime_circular_dependency_message(cycle_path)
109
- end
110
- end
111
-
112
- # Create and build instance with proper thread-local state management
113
- # @return [void] Sets @__task_instance
114
- def create_and_build_instance
115
- thread_key = build_thread_key
116
- Thread.current[thread_key] = true
117
- begin
118
- build_dependencies
119
- @__task_instance = build_instance
120
- ensure
121
- Thread.current[thread_key] = false
122
- end
123
- end
124
-
125
- # === Core Helper Methods ===
126
-
127
- # Get or create build monitor for thread safety
128
- # @return [Monitor] Thread-safe monitor object
129
- def build_monitor
130
- @__build_monitor ||= Monitor.new
131
- end
132
-
133
- # Generate thread key for recursion detection
134
- # @return [String] Thread key for this task
135
- def build_thread_key
136
- "#{name}#{THREAD_KEY_SUFFIX}"
137
- end
138
-
139
- # Build and configure task instance
140
- # @return [Task] Built task instance
141
- def build_instance
142
- instance = new
143
- Utils::TaskBuildHelpers.with_build_logging(name.to_s,
144
- dependencies: @dependencies || []) do
145
- instance.build
146
- instance
147
- end
148
- end
149
-
150
- # Clear thread-local state for this task
151
- def clear_thread_local_state
152
- Thread.current.keys.each do |key|
153
- Thread.current[key] = nil if key.to_s.include?(build_thread_key)
154
- end
155
- end
156
-
157
- # === Dependency Management ===
158
-
159
- # Build all dependencies of this task
160
- def build_dependencies
161
- resolve_dependencies
162
-
163
- (@dependencies || []).each do |dep|
164
- dep_class = extract_class(dep)
165
- next if dep_class == self
166
-
167
- dep_class.ensure_instance_built if dep_class.respond_to?(:ensure_instance_built)
168
- end
169
- end
170
-
171
- private
172
-
173
- # Build current dependency path from thread-local storage
174
- # @return [Array<Class>] Array of classes in the current build path
175
- def build_current_dependency_path
176
- path = []
177
- Thread.current.keys.each do |key|
178
- if key.to_s.end_with?(THREAD_KEY_SUFFIX) && Thread.current[key]
179
- class_name = key.to_s.sub(THREAD_KEY_SUFFIX, "")
180
- begin
181
- path << Object.const_get(class_name)
182
- rescue NameError
183
- # Skip if class not found
184
- end
185
- end
186
- end
187
- path << self
188
- end
189
-
190
- # Build runtime circular dependency error message
191
- # @param cycle_path [Array<Class>] The circular dependency path
192
- # @return [String] Formatted error message
193
- def build_runtime_circular_dependency_message(cycle_path)
194
- Utils::CircularDependencyHelpers.build_error_message(cycle_path, "runtime")
195
- end
196
-
197
- include Utils::DependencyUtils
198
- private :extract_class
199
- end
200
- end
201
- end