taski 0.1.0 → 0.2.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/LICENSE +21 -0
- data/README.md +299 -31
- data/Steepfile +20 -0
- data/examples/complex_example.rb +109 -0
- data/examples/readme_example.rb +30 -0
- data/lib/taski/dependency_analyzer.rb +162 -0
- data/lib/taski/exceptions.rb +14 -0
- data/lib/taski/reference.rb +40 -0
- data/lib/taski/task/base.rb +60 -0
- data/lib/taski/task/define_api.rb +125 -0
- data/lib/taski/task/dependency_resolver.rb +105 -0
- data/lib/taski/task/exports_api.rb +31 -0
- data/lib/taski/task/instance_management.rb +135 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +35 -150
- data/sig/taski.rbs +60 -2
- metadata +30 -4
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
module DependencyAnalyzer
|
7
|
+
class << self
|
8
|
+
def analyze_method(klass, method_name)
|
9
|
+
return [] unless klass.instance_methods(false).include?(method_name)
|
10
|
+
|
11
|
+
method = klass.instance_method(method_name)
|
12
|
+
source_location = method.source_location
|
13
|
+
return [] unless source_location
|
14
|
+
|
15
|
+
file_path, line_number = source_location
|
16
|
+
return [] unless File.exist?(file_path)
|
17
|
+
|
18
|
+
begin
|
19
|
+
result = Prism.parse_file(file_path)
|
20
|
+
|
21
|
+
unless result.success?
|
22
|
+
warn "Taski: Parse errors in #{file_path}: #{result.errors.map(&:message).join(', ')}"
|
23
|
+
return []
|
24
|
+
end
|
25
|
+
|
26
|
+
# Handle warnings if present
|
27
|
+
if result.warnings.any?
|
28
|
+
warn "Taski: Parse warnings in #{file_path}: #{result.warnings.map(&:message).join(', ')}"
|
29
|
+
end
|
30
|
+
|
31
|
+
dependencies = []
|
32
|
+
method_node = find_method_node(result.value, method_name, line_number)
|
33
|
+
|
34
|
+
if method_node
|
35
|
+
visitor = TaskDependencyVisitor.new
|
36
|
+
visitor.visit(method_node)
|
37
|
+
dependencies = visitor.dependencies
|
38
|
+
end
|
39
|
+
|
40
|
+
dependencies.uniq
|
41
|
+
rescue IOError, SystemCallError => e
|
42
|
+
warn "Taski: Failed to read file #{file_path}: #{e.message}"
|
43
|
+
[]
|
44
|
+
rescue => e
|
45
|
+
warn "Taski: Failed to analyze method #{klass}##{method_name}: #{e.message}"
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def find_method_node(node, method_name, target_line)
|
53
|
+
return nil unless node
|
54
|
+
|
55
|
+
case node
|
56
|
+
when Prism::DefNode
|
57
|
+
if node.name == method_name && node.location.start_line <= target_line && node.location.end_line >= target_line
|
58
|
+
return node
|
59
|
+
end
|
60
|
+
when Prism::ClassNode, Prism::ModuleNode
|
61
|
+
if node.respond_to?(:body)
|
62
|
+
return find_method_node(node.body, method_name, target_line)
|
63
|
+
end
|
64
|
+
when Prism::StatementsNode
|
65
|
+
node.body.each do |child|
|
66
|
+
result = find_method_node(child, method_name, target_line)
|
67
|
+
return result if result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Recursively search child nodes
|
72
|
+
if node.respond_to?(:child_nodes)
|
73
|
+
node.child_nodes.each do |child|
|
74
|
+
result = find_method_node(child, method_name, target_line)
|
75
|
+
return result if result
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# Task dependency visitor using Prism's visitor pattern
|
83
|
+
class TaskDependencyVisitor < Prism::Visitor
|
84
|
+
attr_reader :dependencies
|
85
|
+
|
86
|
+
def initialize
|
87
|
+
@dependencies = []
|
88
|
+
@constant_cache = {}
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_constant_read_node(node)
|
92
|
+
check_task_constant(node.name.to_s)
|
93
|
+
super
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_constant_path_node(node)
|
97
|
+
const_path = extract_constant_path(node)
|
98
|
+
check_task_constant(const_path) if const_path
|
99
|
+
super
|
100
|
+
end
|
101
|
+
|
102
|
+
def visit_call_node(node)
|
103
|
+
# Check for method calls on constants (e.g., TaskA.result)
|
104
|
+
case node.receiver
|
105
|
+
when Prism::ConstantReadNode
|
106
|
+
check_task_constant(node.receiver.name.to_s)
|
107
|
+
when Prism::ConstantPathNode
|
108
|
+
const_path = extract_constant_path(node.receiver)
|
109
|
+
check_task_constant(const_path) if const_path
|
110
|
+
end
|
111
|
+
super
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def check_task_constant(const_name)
|
117
|
+
return unless const_name
|
118
|
+
|
119
|
+
# Use caching to avoid repeated constant resolution
|
120
|
+
cached_result = @constant_cache[const_name]
|
121
|
+
return cached_result if cached_result == false # Cached negative result
|
122
|
+
return @dependencies << cached_result if cached_result # Cached positive result
|
123
|
+
|
124
|
+
begin
|
125
|
+
if Object.const_defined?(const_name)
|
126
|
+
klass = Object.const_get(const_name)
|
127
|
+
if klass.is_a?(Class) && klass < Taski::Task
|
128
|
+
@constant_cache[const_name] = klass
|
129
|
+
@dependencies << klass
|
130
|
+
else
|
131
|
+
@constant_cache[const_name] = false
|
132
|
+
end
|
133
|
+
else
|
134
|
+
@constant_cache[const_name] = false
|
135
|
+
end
|
136
|
+
rescue NameError, ArgumentError
|
137
|
+
@constant_cache[const_name] = false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def extract_constant_path(node)
|
142
|
+
case node
|
143
|
+
when Prism::ConstantReadNode
|
144
|
+
node.name.to_s
|
145
|
+
when Prism::ConstantPathNode
|
146
|
+
parent_path = extract_constant_path(node.parent) if node.parent
|
147
|
+
child_name = node.name.to_s
|
148
|
+
|
149
|
+
if parent_path && child_name
|
150
|
+
"#{parent_path}::#{child_name}"
|
151
|
+
else
|
152
|
+
child_name
|
153
|
+
end
|
154
|
+
else
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
# Custom exceptions for Taski framework
|
5
|
+
|
6
|
+
# Raised when circular dependencies are detected between tasks
|
7
|
+
class CircularDependencyError < StandardError; end
|
8
|
+
|
9
|
+
# Raised when task analysis fails (e.g., constant resolution errors)
|
10
|
+
class TaskAnalysisError < StandardError; end
|
11
|
+
|
12
|
+
# Raised when task building fails during execution
|
13
|
+
class TaskBuildError < StandardError; end
|
14
|
+
end
|
@@ -0,0 +1,40 @@
|
|
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
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../exceptions'
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
# Base Task class that provides the foundation for task framework
|
7
|
+
# This module contains the core constants and basic structure
|
8
|
+
class Task
|
9
|
+
# Constants for thread-local keys and method tracking
|
10
|
+
THREAD_KEY_SUFFIX = '_building'
|
11
|
+
TASKI_ANALYZING_DEFINE_KEY = :taski_analyzing_define
|
12
|
+
ANALYZED_METHODS = [:build, :clean].freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
# === Hook Methods ===
|
16
|
+
|
17
|
+
# Hook called when build/clean methods are defined
|
18
|
+
# This triggers static analysis of dependencies
|
19
|
+
def method_added(method_name)
|
20
|
+
super
|
21
|
+
return unless ANALYZED_METHODS.include?(method_name)
|
22
|
+
# Only call if the method is available (loaded by dependency_resolver)
|
23
|
+
analyze_dependencies_at_definition if respond_to?(:analyze_dependencies_at_definition, true)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create a reference to a task class (can be used anywhere)
|
27
|
+
# @param klass [String] The class name to reference
|
28
|
+
# @return [Reference] A reference object
|
29
|
+
def ref(klass)
|
30
|
+
reference = Reference.new(klass)
|
31
|
+
# If we're in a define context, throw for dependency tracking
|
32
|
+
if Thread.current[TASKI_ANALYZING_DEFINE_KEY]
|
33
|
+
reference.tap { |ref| throw :unresolved, ref }
|
34
|
+
else
|
35
|
+
reference
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Get or create resolution state for define API
|
40
|
+
# @return [Hash] Resolution state hash
|
41
|
+
def __resolve__
|
42
|
+
@__resolve__ ||= {}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# === Instance Methods ===
|
47
|
+
|
48
|
+
# Build method that must be implemented by subclasses
|
49
|
+
# @raise [NotImplementedError] If not implemented by subclass
|
50
|
+
def build
|
51
|
+
raise NotImplementedError, "You must implement the build method in your task class"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Clean method with default empty implementation
|
55
|
+
# Subclasses can override this method to implement cleanup logic
|
56
|
+
def clean
|
57
|
+
# Default implementation does nothing
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
class Task
|
7
|
+
class << self
|
8
|
+
# === Define API ===
|
9
|
+
# Define lazy-evaluated values with dynamic dependency resolution
|
10
|
+
|
11
|
+
# Define a lazy-evaluated value using a block
|
12
|
+
# Use this API when dependencies change based on runtime conditions,
|
13
|
+
# environment-specific configurations, feature flags, or complex conditional logic
|
14
|
+
# @param name [Symbol] Name of the value
|
15
|
+
# @param block [Proc] Block that computes the value and determines dependencies at runtime
|
16
|
+
# @param options [Hash] Additional options
|
17
|
+
def define(name, block, **options)
|
18
|
+
@dependencies ||= []
|
19
|
+
@definitions ||= {}
|
20
|
+
|
21
|
+
# Create method that tracks dependencies on first call
|
22
|
+
create_tracking_method(name)
|
23
|
+
|
24
|
+
# Analyze dependencies by executing the block
|
25
|
+
dependencies = analyze_define_dependencies(block)
|
26
|
+
|
27
|
+
@dependencies += dependencies
|
28
|
+
@definitions[name] = { block:, options:, classes: dependencies }
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# === Define API Implementation ===
|
34
|
+
|
35
|
+
# Create method that tracks dependencies for define API
|
36
|
+
# @param name [Symbol] Method name to create
|
37
|
+
def create_tracking_method(name)
|
38
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
39
|
+
def self.#{name}
|
40
|
+
__resolve__[__callee__] ||= false
|
41
|
+
if __resolve__[__callee__]
|
42
|
+
# already resolved
|
43
|
+
else
|
44
|
+
__resolve__[__callee__] = true
|
45
|
+
throw :unresolved, [self, __callee__]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
RUBY
|
49
|
+
end
|
50
|
+
|
51
|
+
# Analyze dependencies in define block
|
52
|
+
# @param block [Proc] Block to analyze
|
53
|
+
# @return [Array<Hash>] Array of dependency information
|
54
|
+
def analyze_define_dependencies(block)
|
55
|
+
classes = []
|
56
|
+
|
57
|
+
# Set flag to indicate we're analyzing define dependencies
|
58
|
+
Thread.current[TASKI_ANALYZING_DEFINE_KEY] = true
|
59
|
+
|
60
|
+
loop do
|
61
|
+
klass, task = catch(:unresolved) do
|
62
|
+
block.call
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
break if klass.nil?
|
67
|
+
|
68
|
+
classes << { klass:, task: }
|
69
|
+
end
|
70
|
+
|
71
|
+
# Reset resolution state
|
72
|
+
classes.each do |task_class|
|
73
|
+
task_class[:klass].instance_variable_set(:@__resolve__, {})
|
74
|
+
end
|
75
|
+
|
76
|
+
classes
|
77
|
+
ensure
|
78
|
+
Thread.current[TASKI_ANALYZING_DEFINE_KEY] = false
|
79
|
+
end
|
80
|
+
|
81
|
+
# Create methods for values defined with define API
|
82
|
+
def create_defined_methods
|
83
|
+
@definitions ||= {}
|
84
|
+
@definitions.each do |name, definition|
|
85
|
+
create_defined_method(name, definition) unless method_defined_for_define?(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Create a single defined method (both class and instance)
|
90
|
+
# @param name [Symbol] Method name
|
91
|
+
# @param definition [Hash] Method definition information
|
92
|
+
def create_defined_method(name, definition)
|
93
|
+
# Class method with lazy evaluation
|
94
|
+
define_singleton_method(name) do
|
95
|
+
@__defined_values ||= {}
|
96
|
+
@__defined_values[name] ||= definition[:block].call
|
97
|
+
end
|
98
|
+
|
99
|
+
# Instance method that delegates to class method
|
100
|
+
define_method(name) do
|
101
|
+
@__defined_values ||= {}
|
102
|
+
@__defined_values[name] ||= self.class.send(name)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Mark as defined for this resolution
|
106
|
+
mark_method_as_defined(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Mark method as defined for this resolution cycle
|
110
|
+
# @param method_name [Symbol] Method name to mark
|
111
|
+
def mark_method_as_defined(method_name)
|
112
|
+
@__defined_for_resolve ||= Set.new
|
113
|
+
@__defined_for_resolve << method_name
|
114
|
+
end
|
115
|
+
|
116
|
+
# Check if method was already defined for define API
|
117
|
+
# @param method_name [Symbol] Method name to check
|
118
|
+
# @return [Boolean] True if already defined
|
119
|
+
def method_defined_for_define?(method_name)
|
120
|
+
@__defined_for_resolve ||= Set.new
|
121
|
+
@__defined_for_resolve.include?(method_name)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require_relative '../dependency_analyzer'
|
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
|
+
@dependencies ||= []
|
17
|
+
|
18
|
+
@dependencies.each do |task|
|
19
|
+
task_class = extract_class(task)
|
20
|
+
|
21
|
+
# Reorder in resolved list for correct priority
|
22
|
+
resolved.delete(task_class) if resolved.include?(task_class)
|
23
|
+
queue << task_class
|
24
|
+
end
|
25
|
+
|
26
|
+
# Override ref method after resolution for define API compatibility
|
27
|
+
# Note: This may cause "method redefined" warnings, which is expected behavior
|
28
|
+
if (@definitions && !@definitions.empty?) && !method_defined_for_define?(:ref)
|
29
|
+
define_singleton_method(:ref) { |klass| Object.const_get(klass) }
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create getter methods for defined values
|
33
|
+
create_defined_methods
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Resolve all dependencies in topological order with circular dependency detection
|
39
|
+
# @return [Array<Class>] Array of tasks in dependency order
|
40
|
+
def resolve_dependencies
|
41
|
+
queue = [self]
|
42
|
+
resolved = []
|
43
|
+
visited = Set.new
|
44
|
+
resolving = Set.new # Track currently resolving tasks
|
45
|
+
|
46
|
+
while queue.any?
|
47
|
+
task_class = queue.shift
|
48
|
+
next if visited.include?(task_class)
|
49
|
+
|
50
|
+
# Check for circular dependency
|
51
|
+
if resolving.include?(task_class)
|
52
|
+
raise CircularDependencyError, "Circular dependency detected involving #{task_class.name}"
|
53
|
+
end
|
54
|
+
|
55
|
+
resolving << task_class
|
56
|
+
visited << task_class
|
57
|
+
task_class.resolve(queue, resolved)
|
58
|
+
resolving.delete(task_class)
|
59
|
+
resolved << task_class unless resolved.include?(task_class)
|
60
|
+
end
|
61
|
+
|
62
|
+
resolved
|
63
|
+
end
|
64
|
+
|
65
|
+
# === Static Analysis ===
|
66
|
+
|
67
|
+
# Analyze dependencies when methods are defined
|
68
|
+
def analyze_dependencies_at_definition
|
69
|
+
dependencies = gather_static_dependencies
|
70
|
+
add_unique_dependencies(dependencies)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Gather dependencies from build and clean methods
|
74
|
+
# @return [Array<Class>] Array of dependency classes
|
75
|
+
def gather_static_dependencies
|
76
|
+
build_deps = DependencyAnalyzer.analyze_method(self, :build)
|
77
|
+
clean_deps = DependencyAnalyzer.analyze_method(self, :clean)
|
78
|
+
(build_deps + clean_deps).uniq
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add dependencies that don't already exist
|
82
|
+
# @param dep_classes [Array<Class>] Array of dependency classes
|
83
|
+
def add_unique_dependencies(dep_classes)
|
84
|
+
dep_classes.each do |dep_class|
|
85
|
+
next if dep_class == self || dependency_exists?(dep_class)
|
86
|
+
add_dependency(dep_class)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Add a single dependency
|
91
|
+
# @param dep_class [Class] Dependency class to add
|
92
|
+
def add_dependency(dep_class)
|
93
|
+
@dependencies ||= []
|
94
|
+
@dependencies << { klass: dep_class }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Check if dependency already exists
|
98
|
+
# @param dep_class [Class] Dependency class to check
|
99
|
+
# @return [Boolean] True if dependency exists
|
100
|
+
def dependency_exists?(dep_class)
|
101
|
+
(@dependencies || []).any? { |d| d[:klass] == dep_class }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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 class method to access exported value
|
19
|
+
define_singleton_method(name) do
|
20
|
+
ensure_instance_built.send(name)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Define instance method getter
|
24
|
+
define_method(name) do
|
25
|
+
instance_variable_get("@#{name}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,135 @@
|
|
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
|
+
def build
|
12
|
+
resolve_dependencies.reverse.each do |task_class|
|
13
|
+
task_class.ensure_instance_built
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Clean this task and all its dependencies in reverse order
|
18
|
+
def clean
|
19
|
+
resolve_dependencies.each do |task_class|
|
20
|
+
# Get existing instance or create new one for cleaning
|
21
|
+
instance = task_class.instance_variable_get(:@__task_instance) || task_class.new
|
22
|
+
instance.clean
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Reset task instance and cached data to prevent memory leaks
|
27
|
+
# @return [self] Returns self for method chaining
|
28
|
+
def reset!
|
29
|
+
build_monitor.synchronize do
|
30
|
+
@__task_instance = nil
|
31
|
+
@__defined_values = nil
|
32
|
+
@__defined_for_resolve = nil
|
33
|
+
clear_thread_local_state
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Refresh task state (currently just resets)
|
39
|
+
# @return [self] Returns self for method chaining
|
40
|
+
def refresh
|
41
|
+
reset!
|
42
|
+
end
|
43
|
+
|
44
|
+
# === Instance Management ===
|
45
|
+
|
46
|
+
# Ensure task instance is built (public because called from build)
|
47
|
+
# @return [Task] The built task instance
|
48
|
+
def ensure_instance_built
|
49
|
+
# Use double-checked locking pattern for thread safety
|
50
|
+
return @__task_instance if @__task_instance
|
51
|
+
|
52
|
+
build_monitor.synchronize do
|
53
|
+
# Check again after acquiring lock
|
54
|
+
return @__task_instance if @__task_instance
|
55
|
+
|
56
|
+
# Prevent infinite recursion using thread-local storage
|
57
|
+
thread_key = build_thread_key
|
58
|
+
if Thread.current[thread_key]
|
59
|
+
raise CircularDependencyError, "Circular dependency detected: #{self.name} is already being built"
|
60
|
+
end
|
61
|
+
|
62
|
+
Thread.current[thread_key] = true
|
63
|
+
begin
|
64
|
+
build_dependencies
|
65
|
+
@__task_instance = build_instance
|
66
|
+
ensure
|
67
|
+
Thread.current[thread_key] = false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
@__task_instance
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
# === Core Helper Methods ===
|
77
|
+
|
78
|
+
# Get or create build monitor for thread safety
|
79
|
+
# @return [Monitor] Thread-safe monitor object
|
80
|
+
def build_monitor
|
81
|
+
@__build_monitor ||= Monitor.new
|
82
|
+
end
|
83
|
+
|
84
|
+
# Generate thread key for recursion detection
|
85
|
+
# @return [String] Thread key for this task
|
86
|
+
def build_thread_key
|
87
|
+
"#{self.name}#{THREAD_KEY_SUFFIX}"
|
88
|
+
end
|
89
|
+
|
90
|
+
# Build and configure task instance
|
91
|
+
# @return [Task] Built task instance
|
92
|
+
def build_instance
|
93
|
+
instance = self.new
|
94
|
+
begin
|
95
|
+
instance.build
|
96
|
+
instance
|
97
|
+
rescue => e
|
98
|
+
# Log the error but don't let it crash the entire system
|
99
|
+
warn "Taski: Failed to build #{self.name}: #{e.message}"
|
100
|
+
warn "Taski: #{e.backtrace.first}" if e.backtrace
|
101
|
+
raise TaskBuildError, "Failed to build task #{self.name}: #{e.message}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Clear thread-local state for this task
|
106
|
+
def clear_thread_local_state
|
107
|
+
Thread.current.keys.each do |key|
|
108
|
+
Thread.current[key] = nil if key.to_s.include?(build_thread_key)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# === Dependency Management ===
|
113
|
+
|
114
|
+
# Build all dependencies of this task
|
115
|
+
def build_dependencies
|
116
|
+
resolve_dependencies
|
117
|
+
|
118
|
+
(@dependencies || []).each do |dep|
|
119
|
+
dep_class = extract_class(dep)
|
120
|
+
next if dep_class == self
|
121
|
+
|
122
|
+
dep_class.ensure_instance_built if dep_class.respond_to?(:ensure_instance_built)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Extract class from dependency hash
|
127
|
+
# @param dep [Hash] Dependency information
|
128
|
+
# @return [Class] The dependency class
|
129
|
+
def extract_class(dep)
|
130
|
+
klass = dep[:klass]
|
131
|
+
klass.is_a?(Reference) ? klass.deref : klass
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
data/lib/taski/version.rb
CHANGED