taski 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/.standard.yml +9 -0
- data/README.md +189 -32
- data/Rakefile +7 -1
- data/examples/complex_example.rb +107 -0
- data/examples/readme_example.rb +30 -0
- data/lib/taski/dependency_analyzer.rb +172 -0
- data/lib/taski/exceptions.rb +14 -0
- data/lib/taski/logger.rb +202 -0
- data/lib/taski/reference.rb +40 -0
- data/lib/taski/task/base.rb +60 -0
- data/lib/taski/task/define_api.rb +138 -0
- data/lib/taski/task/dependency_resolver.rb +138 -0
- data/lib/taski/task/exports_api.rb +31 -0
- data/lib/taski/task/instance_management.rb +176 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +34 -137
- data/sig/taski.rbs +97 -2
- metadata +30 -5
- data/lib/taski/utils.rb +0 -53
data/lib/taski/logger.rb
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
# Enhanced logging functionality for Taski framework
|
5
|
+
# Provides structured logging with multiple levels and context information
|
6
|
+
class Logger
|
7
|
+
# Log levels in order of severity
|
8
|
+
LEVELS = {debug: 0, info: 1, warn: 2, error: 3}.freeze
|
9
|
+
|
10
|
+
# @param level [Symbol] Minimum log level to output (:debug, :info, :warn, :error)
|
11
|
+
# @param output [IO] Output destination (default: $stdout)
|
12
|
+
# @param format [Symbol] Log format (:simple, :structured, :json)
|
13
|
+
def initialize(level: :info, output: $stdout, format: :structured)
|
14
|
+
@level = level
|
15
|
+
@output = output
|
16
|
+
@format = format
|
17
|
+
@start_time = Time.now
|
18
|
+
end
|
19
|
+
|
20
|
+
# Log debug message with optional context
|
21
|
+
# @param message [String] Log message
|
22
|
+
# @param context [Hash] Additional context information
|
23
|
+
def debug(message, **context)
|
24
|
+
log(:debug, message, context)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Log info message with optional context
|
28
|
+
# @param message [String] Log message
|
29
|
+
# @param context [Hash] Additional context information
|
30
|
+
def info(message, **context)
|
31
|
+
log(:info, message, context)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Log warning message with optional context
|
35
|
+
# @param message [String] Log message
|
36
|
+
# @param context [Hash] Additional context information
|
37
|
+
def warn(message, **context)
|
38
|
+
log(:warn, message, context)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Log error message with optional context
|
42
|
+
# @param message [String] Log message
|
43
|
+
# @param context [Hash] Additional context information
|
44
|
+
def error(message, **context)
|
45
|
+
log(:error, message, context)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Log task build start event
|
49
|
+
# @param task_name [String] Name of the task being built
|
50
|
+
# @param dependencies [Array] List of task dependencies
|
51
|
+
def task_build_start(task_name, dependencies: [])
|
52
|
+
info("Task build started",
|
53
|
+
task: task_name,
|
54
|
+
dependencies: dependencies.size,
|
55
|
+
dependency_names: dependencies.map { |dep| dep.is_a?(Hash) ? dep[:klass].inspect : dep.inspect })
|
56
|
+
end
|
57
|
+
|
58
|
+
# Log task build completion event
|
59
|
+
# @param task_name [String] Name of the task that was built
|
60
|
+
# @param duration [Float] Build duration in seconds
|
61
|
+
def task_build_complete(task_name, duration: nil)
|
62
|
+
context = {task: task_name}
|
63
|
+
context[:duration_ms] = (duration * 1000).round(2) if duration
|
64
|
+
info("Task build completed", **context)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Log task build failure event
|
68
|
+
# @param task_name [String] Name of the task that failed
|
69
|
+
# @param error [Exception] The error that occurred
|
70
|
+
# @param duration [Float] Duration before failure in seconds
|
71
|
+
def task_build_failed(task_name, error:, duration: nil)
|
72
|
+
context = {
|
73
|
+
task: task_name,
|
74
|
+
error_class: error.class.name,
|
75
|
+
error_message: error.message
|
76
|
+
}
|
77
|
+
context[:duration_ms] = (duration * 1000).round(2) if duration
|
78
|
+
context[:backtrace] = error.backtrace&.first(3) if error.backtrace
|
79
|
+
error("Task build failed", **context)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Log dependency resolution event
|
83
|
+
# @param task_name [String] Name of the task resolving dependencies
|
84
|
+
# @param resolved_count [Integer] Number of dependencies resolved
|
85
|
+
def dependency_resolved(task_name, resolved_count:)
|
86
|
+
debug("Dependencies resolved",
|
87
|
+
task: task_name,
|
88
|
+
resolved_dependencies: resolved_count)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Log circular dependency detection
|
92
|
+
# @param cycle_path [Array] The circular dependency path
|
93
|
+
def circular_dependency_detected(cycle_path)
|
94
|
+
error("Circular dependency detected",
|
95
|
+
cycle: cycle_path.map { |klass| klass.name || klass.inspect },
|
96
|
+
cycle_length: cycle_path.size)
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
# Core logging method
|
102
|
+
# @param level [Symbol] Log level
|
103
|
+
# @param message [String] Log message
|
104
|
+
# @param context [Hash] Additional context
|
105
|
+
def log(level, message, context)
|
106
|
+
return unless should_log?(level)
|
107
|
+
|
108
|
+
case @format
|
109
|
+
when :simple
|
110
|
+
log_simple(level, message, context)
|
111
|
+
when :structured
|
112
|
+
log_structured(level, message, context)
|
113
|
+
when :json
|
114
|
+
log_json(level, message, context)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Check if message should be logged based on current level
|
119
|
+
# @param level [Symbol] Message level to check
|
120
|
+
# @return [Boolean] True if message should be logged
|
121
|
+
def should_log?(level)
|
122
|
+
LEVELS[@level] <= LEVELS[level]
|
123
|
+
end
|
124
|
+
|
125
|
+
# Simple log format: [LEVEL] message
|
126
|
+
def log_simple(level, message, context)
|
127
|
+
@output.puts "[#{level.upcase}] #{message}"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Structured log format with timestamp and context
|
131
|
+
def log_structured(level, message, context)
|
132
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")
|
133
|
+
elapsed = ((Time.now - @start_time) * 1000).round(1)
|
134
|
+
|
135
|
+
line = "[#{timestamp}] [#{elapsed}ms] #{level.to_s.upcase.ljust(5)} Taski: #{message}"
|
136
|
+
|
137
|
+
unless context.empty?
|
138
|
+
context_parts = context.map do |key, value|
|
139
|
+
"#{key}=#{format_value(value)}"
|
140
|
+
end
|
141
|
+
line += " (#{context_parts.join(", ")})"
|
142
|
+
end
|
143
|
+
|
144
|
+
@output.puts line
|
145
|
+
end
|
146
|
+
|
147
|
+
# JSON log format for structured logging systems
|
148
|
+
def log_json(level, message, context)
|
149
|
+
require "json"
|
150
|
+
|
151
|
+
log_entry = {
|
152
|
+
timestamp: Time.now.iso8601(3),
|
153
|
+
level: level.to_s,
|
154
|
+
logger: "taski",
|
155
|
+
message: message,
|
156
|
+
elapsed_ms: ((Time.now - @start_time) * 1000).round(1)
|
157
|
+
}.merge(context)
|
158
|
+
|
159
|
+
@output.puts JSON.generate(log_entry)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Format values for structured logging
|
163
|
+
def format_value(value)
|
164
|
+
case value
|
165
|
+
when String
|
166
|
+
(value.length > 50) ? "#{value[0..47]}..." : value
|
167
|
+
when Array
|
168
|
+
(value.size > 5) ? "[#{value[0..4].join(", ")}, ...]" : value.inspect
|
169
|
+
when Hash
|
170
|
+
(value.size > 3) ? "{#{value.keys[0..2].join(", ")}, ...}" : value.inspect
|
171
|
+
else
|
172
|
+
value.inspect
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class << self
|
178
|
+
# Get the current logger instance
|
179
|
+
# @return [Logger] Current logger instance
|
180
|
+
def logger
|
181
|
+
@logger ||= Logger.new
|
182
|
+
end
|
183
|
+
|
184
|
+
# Configure the logger with new settings
|
185
|
+
# @param level [Symbol] Log level (:debug, :info, :warn, :error)
|
186
|
+
# @param output [IO] Output destination
|
187
|
+
# @param format [Symbol] Log format (:simple, :structured, :json)
|
188
|
+
def configure_logger(level: :info, output: $stdout, format: :structured)
|
189
|
+
@logger = Logger.new(level: level, output: output, format: format)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Set logger to quiet mode (only errors)
|
193
|
+
def quiet!
|
194
|
+
@logger = Logger.new(level: :error, output: @logger&.instance_variable_get(:@output) || $stdout)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Set logger to verbose mode (all messages)
|
198
|
+
def verbose!
|
199
|
+
@logger = Logger.new(level: :debug, output: @logger&.instance_variable_get(:@output) || $stdout)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
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,138 @@
|
|
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
|
+
# Ensure ref method is defined first time define is called
|
20
|
+
create_ref_method_if_needed
|
21
|
+
|
22
|
+
# Create method that tracks dependencies on first call
|
23
|
+
create_tracking_method(name)
|
24
|
+
|
25
|
+
# Analyze dependencies by executing the block
|
26
|
+
dependencies = analyze_define_dependencies(block)
|
27
|
+
|
28
|
+
@dependencies += dependencies
|
29
|
+
@definitions[name] = {block:, options:, classes: dependencies}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# === Define API Implementation ===
|
35
|
+
|
36
|
+
# Create ref method if needed to avoid redefinition warnings
|
37
|
+
def create_ref_method_if_needed
|
38
|
+
return if method_defined_for_define?(:ref)
|
39
|
+
|
40
|
+
define_singleton_method(:ref) { |klass| Object.const_get(klass) }
|
41
|
+
mark_method_as_defined(:ref)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create method that tracks dependencies for define API
|
45
|
+
# @param name [Symbol] Method name to create
|
46
|
+
def create_tracking_method(name)
|
47
|
+
# Only create tracking method during dependency analysis
|
48
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
49
|
+
def self.#{name}
|
50
|
+
__resolve__[__callee__] ||= false
|
51
|
+
if __resolve__[__callee__]
|
52
|
+
# already resolved
|
53
|
+
else
|
54
|
+
__resolve__[__callee__] = true
|
55
|
+
throw :unresolved, [self, __callee__]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
RUBY
|
59
|
+
end
|
60
|
+
|
61
|
+
# Analyze dependencies in define block
|
62
|
+
# @param block [Proc] Block to analyze
|
63
|
+
# @return [Array<Hash>] Array of dependency information
|
64
|
+
def analyze_define_dependencies(block)
|
65
|
+
classes = []
|
66
|
+
|
67
|
+
# Set flag to indicate we're analyzing define dependencies
|
68
|
+
Thread.current[TASKI_ANALYZING_DEFINE_KEY] = true
|
69
|
+
|
70
|
+
loop do
|
71
|
+
klass, task = catch(:unresolved) do
|
72
|
+
block.call
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
|
76
|
+
break if klass.nil?
|
77
|
+
|
78
|
+
classes << {klass:, task:}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Reset resolution state
|
82
|
+
classes.each do |task_class|
|
83
|
+
task_class[:klass].instance_variable_set(:@__resolve__, {})
|
84
|
+
end
|
85
|
+
|
86
|
+
classes
|
87
|
+
ensure
|
88
|
+
Thread.current[TASKI_ANALYZING_DEFINE_KEY] = false
|
89
|
+
end
|
90
|
+
|
91
|
+
# Create methods for values defined with define API
|
92
|
+
def create_defined_methods
|
93
|
+
@definitions ||= {}
|
94
|
+
@definitions.each do |name, definition|
|
95
|
+
create_defined_method(name, definition) unless method_defined_for_define?(name)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Create a single defined method (both class and instance)
|
100
|
+
# @param name [Symbol] Method name
|
101
|
+
# @param definition [Hash] Method definition information
|
102
|
+
def create_defined_method(name, definition)
|
103
|
+
# Remove tracking method first to avoid redefinition warnings
|
104
|
+
singleton_class.undef_method(name) if singleton_class.method_defined?(name)
|
105
|
+
|
106
|
+
# Class method with lazy evaluation
|
107
|
+
define_singleton_method(name) do
|
108
|
+
@__defined_values ||= {}
|
109
|
+
@__defined_values[name] ||= definition[:block].call
|
110
|
+
end
|
111
|
+
|
112
|
+
# Instance method that delegates to class method
|
113
|
+
define_method(name) do
|
114
|
+
@__defined_values ||= {}
|
115
|
+
@__defined_values[name] ||= self.class.send(name)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Mark as defined for this resolution
|
119
|
+
mark_method_as_defined(name)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Mark method as defined for this resolution cycle
|
123
|
+
# @param method_name [Symbol] Method name to mark
|
124
|
+
def mark_method_as_defined(method_name)
|
125
|
+
@__defined_for_resolve ||= Set.new
|
126
|
+
@__defined_for_resolve << method_name
|
127
|
+
end
|
128
|
+
|
129
|
+
# Check if method was already defined for define API
|
130
|
+
# @param method_name [Symbol] Method name to check
|
131
|
+
# @return [Boolean] True if already defined
|
132
|
+
def method_defined_for_define?(method_name)
|
133
|
+
@__defined_for_resolve ||= Set.new
|
134
|
+
@__defined_for_resolve.include?(method_name)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../dependency_analyzer"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
class Task
|
7
|
+
class << self
|
8
|
+
# === Dependency Resolution ===
|
9
|
+
|
10
|
+
# Resolve method for dependency graph (called by resolve_dependencies)
|
11
|
+
# @param queue [Array] Queue of tasks to process
|
12
|
+
# @param resolved [Array] Array of resolved tasks
|
13
|
+
# @return [self] Returns self for method chaining
|
14
|
+
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
|
29
|
+
end
|
30
|
+
|
31
|
+
# Resolve all dependencies in topological order with circular dependency detection
|
32
|
+
# @return [Array<Class>] Array of tasks in dependency order
|
33
|
+
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
|
+
path_names = cycle_path.map { |klass| klass.name || klass.to_s }
|
84
|
+
|
85
|
+
message = "Circular dependency detected!\n"
|
86
|
+
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
87
|
+
message += "Detailed dependency chain:\n"
|
88
|
+
|
89
|
+
cycle_path.each_cons(2).with_index do |(from, to), index|
|
90
|
+
message += " #{index + 1}. #{from.name} depends on → #{to.name}\n"
|
91
|
+
end
|
92
|
+
|
93
|
+
message
|
94
|
+
end
|
95
|
+
|
96
|
+
public
|
97
|
+
|
98
|
+
# === Static Analysis ===
|
99
|
+
|
100
|
+
# Analyze dependencies when methods are defined
|
101
|
+
def analyze_dependencies_at_definition
|
102
|
+
dependencies = gather_static_dependencies
|
103
|
+
add_unique_dependencies(dependencies)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Gather dependencies from build and clean methods
|
107
|
+
# @return [Array<Class>] Array of dependency classes
|
108
|
+
def gather_static_dependencies
|
109
|
+
build_deps = DependencyAnalyzer.analyze_method(self, :build)
|
110
|
+
clean_deps = DependencyAnalyzer.analyze_method(self, :clean)
|
111
|
+
(build_deps + clean_deps).uniq
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add dependencies that don't already exist
|
115
|
+
# @param dep_classes [Array<Class>] Array of dependency classes
|
116
|
+
def add_unique_dependencies(dep_classes)
|
117
|
+
dep_classes.each do |dep_class|
|
118
|
+
next if dep_class == self || dependency_exists?(dep_class)
|
119
|
+
add_dependency(dep_class)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Add a single dependency
|
124
|
+
# @param dep_class [Class] Dependency class to add
|
125
|
+
def add_dependency(dep_class)
|
126
|
+
@dependencies ||= []
|
127
|
+
@dependencies << {klass: dep_class}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if dependency already exists
|
131
|
+
# @param dep_class [Class] Dependency class to check
|
132
|
+
# @return [Boolean] True if dependency exists
|
133
|
+
def dependency_exists?(dep_class)
|
134
|
+
(@dependencies || []).any? { |d| d[:klass] == dep_class }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
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
|