taski 0.2.0 → 0.2.2
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 +112 -195
- data/Rakefile +7 -1
- data/examples/README.md +57 -0
- data/examples/{complex_example.rb → advanced_patterns.rb} +31 -21
- data/examples/progress_demo.rb +166 -0
- data/examples/{readme_example.rb → quick_start.rb} +15 -4
- data/examples/tree_demo.rb +80 -0
- data/lib/taski/dependency_analyzer.rb +18 -8
- data/lib/taski/exceptions.rb +4 -4
- data/lib/taski/logger.rb +213 -0
- data/lib/taski/progress_display.rb +356 -0
- data/lib/taski/reference.rb +3 -3
- data/lib/taski/task/base.rb +54 -3
- data/lib/taski/task/define_api.rb +36 -7
- data/lib/taski/task/dependency_resolver.rb +39 -11
- data/lib/taski/task/exports_api.rb +1 -1
- data/lib/taski/task/instance_management.rb +75 -20
- data/lib/taski/utils.rb +107 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +7 -5
- data/sig/taski.rbs +39 -2
- metadata +12 -6
- data/Steepfile +0 -20
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "monitor"
|
4
4
|
|
5
5
|
module Taski
|
6
6
|
class Task
|
@@ -8,9 +8,19 @@ module Taski
|
|
8
8
|
# === Lifecycle Management ===
|
9
9
|
|
10
10
|
# Build this task and all its dependencies
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
+
# Traditional build: singleton instance with caching
|
16
|
+
resolve_dependencies.reverse_each do |task_class|
|
17
|
+
task_class.ensure_instance_built
|
18
|
+
end
|
19
|
+
# Return the singleton instance for consistency
|
20
|
+
instance_variable_get(:@__task_instance)
|
21
|
+
else
|
22
|
+
# Parametrized build: temporary instance without caching
|
23
|
+
build_with_args(args)
|
14
24
|
end
|
15
25
|
end
|
16
26
|
|
@@ -41,6 +51,32 @@ module Taski
|
|
41
51
|
reset!
|
42
52
|
end
|
43
53
|
|
54
|
+
# === Parametrized Build Support ===
|
55
|
+
|
56
|
+
# Build temporary instance with arguments
|
57
|
+
# @param args [Hash] Build arguments
|
58
|
+
# @return [Task] Temporary task instance
|
59
|
+
def build_with_args(args)
|
60
|
+
# Resolve dependencies first (same as normal build)
|
61
|
+
resolve_dependencies.reverse_each do |task_class|
|
62
|
+
task_class.ensure_instance_built
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create temporary instance with arguments
|
66
|
+
temp_instance = new
|
67
|
+
temp_instance.instance_variable_set(:@build_args, args)
|
68
|
+
|
69
|
+
# Build with logging using common utility
|
70
|
+
Utils::TaskBuildHelpers.with_build_logging(name.to_s,
|
71
|
+
dependencies: @dependencies || [],
|
72
|
+
args: args) do
|
73
|
+
temp_instance.build
|
74
|
+
temp_instance
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
private :build_with_args
|
79
|
+
|
44
80
|
# === Instance Management ===
|
45
81
|
|
46
82
|
# Ensure task instance is built (public because called from build)
|
@@ -56,7 +92,9 @@ module Taski
|
|
56
92
|
# Prevent infinite recursion using thread-local storage
|
57
93
|
thread_key = build_thread_key
|
58
94
|
if Thread.current[thread_key]
|
59
|
-
|
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)
|
60
98
|
end
|
61
99
|
|
62
100
|
Thread.current[thread_key] = true
|
@@ -84,21 +122,17 @@ module Taski
|
|
84
122
|
# Generate thread key for recursion detection
|
85
123
|
# @return [String] Thread key for this task
|
86
124
|
def build_thread_key
|
87
|
-
"#{
|
125
|
+
"#{name}#{THREAD_KEY_SUFFIX}"
|
88
126
|
end
|
89
127
|
|
90
128
|
# Build and configure task instance
|
91
129
|
# @return [Task] Built task instance
|
92
130
|
def build_instance
|
93
|
-
instance =
|
94
|
-
|
131
|
+
instance = new
|
132
|
+
Utils::TaskBuildHelpers.with_build_logging(name.to_s,
|
133
|
+
dependencies: @dependencies || []) do
|
95
134
|
instance.build
|
96
135
|
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
136
|
end
|
103
137
|
end
|
104
138
|
|
@@ -123,13 +157,34 @@ module Taski
|
|
123
157
|
end
|
124
158
|
end
|
125
159
|
|
126
|
-
|
127
|
-
|
128
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
|
160
|
+
private
|
161
|
+
|
162
|
+
# Build current dependency path from thread-local storage
|
163
|
+
# @return [Array<Class>] Array of classes in the current build path
|
164
|
+
def build_current_dependency_path
|
165
|
+
path = []
|
166
|
+
Thread.current.keys.each do |key|
|
167
|
+
if key.to_s.end_with?(THREAD_KEY_SUFFIX) && Thread.current[key]
|
168
|
+
class_name = key.to_s.sub(THREAD_KEY_SUFFIX, "")
|
169
|
+
begin
|
170
|
+
path << Object.const_get(class_name)
|
171
|
+
rescue NameError
|
172
|
+
# Skip if class not found
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
path << self
|
177
|
+
end
|
178
|
+
|
179
|
+
# Build runtime circular dependency error message
|
180
|
+
# @param cycle_path [Array<Class>] The circular dependency path
|
181
|
+
# @return [String] Formatted error message
|
182
|
+
def build_runtime_circular_dependency_message(cycle_path)
|
183
|
+
Utils::CircularDependencyHelpers.build_error_message(cycle_path, "runtime")
|
132
184
|
end
|
185
|
+
|
186
|
+
include Utils::DependencyUtils
|
187
|
+
private :extract_class
|
133
188
|
end
|
134
189
|
end
|
135
|
-
end
|
190
|
+
end
|
data/lib/taski/utils.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Taski
|
4
|
+
# Common utility functions for the Taski framework
|
5
|
+
module Utils
|
6
|
+
# Handle circular dependency error message generation
|
7
|
+
module CircularDependencyHelpers
|
8
|
+
# Build detailed error message for circular dependencies
|
9
|
+
# @param cycle_path [Array<Class>] The circular dependency path
|
10
|
+
# @param context [String] Context of the error (dependency, runtime)
|
11
|
+
# @return [String] Formatted error message
|
12
|
+
def self.build_error_message(cycle_path, context = "dependency")
|
13
|
+
path_names = cycle_path.map { |klass| klass.name || klass.to_s }
|
14
|
+
|
15
|
+
message = "Circular dependency detected!\n"
|
16
|
+
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
17
|
+
message += "The #{context} chain is:\n"
|
18
|
+
|
19
|
+
cycle_path.each_cons(2).with_index do |(from, to), index|
|
20
|
+
action = (context == "dependency") ? "depends on" : "is trying to build"
|
21
|
+
message += " #{index + 1}. #{from.name} #{action} → #{to.name}\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
message += "\nThis creates an infinite loop that cannot be resolved." if context == "dependency"
|
25
|
+
message
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Common dependency utility functions
|
30
|
+
module DependencyUtils
|
31
|
+
# Extract class from dependency hash
|
32
|
+
# @param dep [Hash] Dependency information
|
33
|
+
# @return [Class] The dependency class
|
34
|
+
def extract_class(dep)
|
35
|
+
klass = dep[:klass]
|
36
|
+
klass.is_a?(Reference) ? klass.deref : klass
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Common task build utility functions
|
41
|
+
module TaskBuildHelpers
|
42
|
+
# Format arguments hash for display in error messages
|
43
|
+
# @param args [Hash] Arguments hash
|
44
|
+
# @return [String] Formatted arguments string
|
45
|
+
def self.format_args(args)
|
46
|
+
return "" if args.nil? || args.empty?
|
47
|
+
|
48
|
+
formatted_pairs = args.map do |key, value|
|
49
|
+
"#{key}: #{value.inspect}"
|
50
|
+
end
|
51
|
+
"{#{formatted_pairs.join(", ")}}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute block with comprehensive build logging and progress display
|
55
|
+
# @param task_name [String] Name of the task being built
|
56
|
+
# @param dependencies [Array] List of dependencies
|
57
|
+
# @param args [Hash] Build arguments for parametrized builds
|
58
|
+
# @yield Block to execute with logging
|
59
|
+
# @return [Object] Result of the block execution
|
60
|
+
def self.with_build_logging(task_name, dependencies: [], args: nil)
|
61
|
+
build_start_time = Time.now
|
62
|
+
|
63
|
+
begin
|
64
|
+
# Traditional logging first (before any stdout redirection)
|
65
|
+
Taski.logger.task_build_start(task_name, dependencies: dependencies, args: args)
|
66
|
+
|
67
|
+
# Show progress display if enabled (this may redirect stdout)
|
68
|
+
Taski.progress_display&.start_task(task_name, dependencies: dependencies)
|
69
|
+
|
70
|
+
result = yield
|
71
|
+
duration = Time.now - build_start_time
|
72
|
+
|
73
|
+
# Complete progress display first (this restores stdout)
|
74
|
+
Taski.progress_display&.complete_task(task_name, duration: duration)
|
75
|
+
|
76
|
+
# Then do logging (on restored stdout)
|
77
|
+
begin
|
78
|
+
Taski.logger.task_build_complete(task_name, duration: duration)
|
79
|
+
rescue IOError
|
80
|
+
# If logger fails due to closed stream, write to STDERR instead
|
81
|
+
warn "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")}] INFO Taski: Task build completed (task=#{task_name}, duration_ms=#{(duration * 1000).round(2)})"
|
82
|
+
end
|
83
|
+
|
84
|
+
result
|
85
|
+
rescue => e
|
86
|
+
duration = Time.now - build_start_time
|
87
|
+
|
88
|
+
# Complete progress display first (with error)
|
89
|
+
Taski.progress_display&.fail_task(task_name, error: e, duration: duration)
|
90
|
+
|
91
|
+
# Then do error logging (on restored stdout)
|
92
|
+
begin
|
93
|
+
Taski.logger.task_build_failed(task_name, error: e, duration: duration)
|
94
|
+
rescue IOError
|
95
|
+
# If logger fails due to closed stream, write to STDERR instead
|
96
|
+
warn "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S.%3N")}] ERROR Taski: Task build failed (task=#{task_name}, error=#{e.message}, duration_ms=#{(duration * 1000).round(2)})"
|
97
|
+
end
|
98
|
+
|
99
|
+
error_message = "Failed to build task #{task_name}"
|
100
|
+
error_message += " with args #{format_args(args)}" if args && !args.empty?
|
101
|
+
error_message += ": #{e.message}"
|
102
|
+
raise TaskBuildError, error_message
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require 'monitor'
|
3
|
+
require "monitor"
|
5
4
|
|
6
5
|
# Load core components
|
7
6
|
require_relative "taski/version"
|
8
7
|
require_relative "taski/exceptions"
|
8
|
+
require_relative "taski/logger"
|
9
|
+
require_relative "taski/progress_display"
|
9
10
|
require_relative "taski/reference"
|
10
11
|
require_relative "taski/dependency_analyzer"
|
12
|
+
require_relative "taski/utils"
|
11
13
|
|
12
14
|
# Load Task class components
|
13
15
|
require_relative "taski/task/base"
|
@@ -18,7 +20,7 @@ require_relative "taski/task/dependency_resolver"
|
|
18
20
|
|
19
21
|
module Taski
|
20
22
|
# Main module for the Taski task framework
|
21
|
-
#
|
23
|
+
#
|
22
24
|
# Taski provides a framework for defining and managing task dependencies
|
23
25
|
# with two complementary APIs:
|
24
26
|
# 1. Exports API - Export instance variables as class methods (static dependencies)
|
@@ -26,7 +28,7 @@ module Taski
|
|
26
28
|
#
|
27
29
|
# Use Define API when:
|
28
30
|
# - Dependencies change based on runtime conditions
|
29
|
-
# - Environment-specific configurations
|
31
|
+
# - Environment-specific configurations
|
30
32
|
# - Feature flags determine which classes to use
|
31
33
|
# - Complex conditional logic determines dependencies
|
32
34
|
#
|
@@ -37,4 +39,4 @@ module Taski
|
|
37
39
|
# - Thread-safe task building
|
38
40
|
# - Circular dependency detection
|
39
41
|
# - Memory leak prevention
|
40
|
-
end
|
42
|
+
end
|
data/sig/taski.rbs
CHANGED
@@ -33,15 +33,19 @@ module Taski
|
|
33
33
|
|
34
34
|
# Public API methods
|
35
35
|
def self.exports: (*Symbol names) -> void
|
36
|
-
def self.define: (Symbol name
|
36
|
+
def self.define: (Symbol name) { () -> untyped } -> void
|
37
37
|
def self.build: () -> void
|
38
38
|
def self.clean: () -> void
|
39
39
|
def self.reset!: () -> self
|
40
40
|
def self.refresh: () -> self
|
41
41
|
def self.resolve: (Array[untyped] queue, Array[untyped] resolved) -> self
|
42
|
-
def self.ref: (String klass) -> Reference
|
42
|
+
def self.ref: (String | Class klass) -> (Reference | Class)
|
43
43
|
def self.ensure_instance_built: () -> Task
|
44
44
|
|
45
|
+
# Define API methods
|
46
|
+
def self.__resolve__: () -> Hash[Symbol, untyped]
|
47
|
+
def self.resolve_dependencies: () -> void
|
48
|
+
|
45
49
|
# Instance methods
|
46
50
|
def build: () -> void
|
47
51
|
def clean: () -> void
|
@@ -53,10 +57,43 @@ module Taski
|
|
53
57
|
# Allow dynamic class method definitions
|
54
58
|
def self.method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
|
55
59
|
def self.respond_to_missing?: (Symbol name, bool include_private) -> bool
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Private class methods for dependency resolution
|
64
|
+
def self.resolve_queue: (Array[Class] queue, Array[Class] resolved) -> Array[Class]
|
65
|
+
def self.detect_circular_dependencies: (Array[Class] queue, Array[Class] resolved) -> void
|
66
|
+
def self.build_instance: (Class task_class) -> void
|
67
|
+
|
68
|
+
# Private class methods for Define API
|
69
|
+
def self.create_defined_method: (Symbol name) { () -> untyped } -> void
|
70
|
+
def self.create_ref_method_if_needed: () -> void
|
71
|
+
def self.method_defined_for_define?: (Symbol method_name) -> bool
|
72
|
+
def self.mark_method_as_defined: (Symbol method_name) -> void
|
56
73
|
end
|
57
74
|
|
58
75
|
# Dependency analyzer module
|
59
76
|
module DependencyAnalyzer
|
60
77
|
def self.analyze_method: (Class klass, Symbol method_name) -> Array[Class]
|
78
|
+
|
79
|
+
# Task dependency visitor for AST analysis
|
80
|
+
class TaskDependencyVisitor < Prism::Visitor
|
81
|
+
@dependencies: Array[Class]
|
82
|
+
|
83
|
+
def initialize: () -> void
|
84
|
+
def dependencies: () -> Array[Class]
|
85
|
+
def visit_call_node: (Prism::CallNode node) -> void
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def extract_class_from_constant: (String constant_name) -> Class?
|
90
|
+
def safe_constantize: (String name) -> Class?
|
91
|
+
def extract_class_from_ref_call: (Prism::CallNode node) -> Class?
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def self.parse_method_code: (Class klass, Symbol method_name) -> Prism::ParseResult?
|
97
|
+
def self.extract_dependencies_from_ast: (Prism::Node ast) -> Array[Class]
|
61
98
|
end
|
62
99
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taski
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ahogappa
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-06-
|
10
|
+
date: 2025-06-27 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: prism
|
@@ -35,21 +35,27 @@ executables: []
|
|
35
35
|
extensions: []
|
36
36
|
extra_rdoc_files: []
|
37
37
|
files:
|
38
|
+
- ".standard.yml"
|
38
39
|
- LICENSE
|
39
40
|
- README.md
|
40
41
|
- Rakefile
|
41
|
-
-
|
42
|
-
- examples/
|
43
|
-
- examples/
|
42
|
+
- examples/README.md
|
43
|
+
- examples/advanced_patterns.rb
|
44
|
+
- examples/progress_demo.rb
|
45
|
+
- examples/quick_start.rb
|
46
|
+
- examples/tree_demo.rb
|
44
47
|
- lib/taski.rb
|
45
48
|
- lib/taski/dependency_analyzer.rb
|
46
49
|
- lib/taski/exceptions.rb
|
50
|
+
- lib/taski/logger.rb
|
51
|
+
- lib/taski/progress_display.rb
|
47
52
|
- lib/taski/reference.rb
|
48
53
|
- lib/taski/task/base.rb
|
49
54
|
- lib/taski/task/define_api.rb
|
50
55
|
- lib/taski/task/dependency_resolver.rb
|
51
56
|
- lib/taski/task/exports_api.rb
|
52
57
|
- lib/taski/task/instance_management.rb
|
58
|
+
- lib/taski/utils.rb
|
53
59
|
- lib/taski/version.rb
|
54
60
|
- sig/taski.rbs
|
55
61
|
homepage: https://github.com/ahogappa/taski
|
@@ -65,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
65
71
|
requirements:
|
66
72
|
- - ">="
|
67
73
|
- !ruby/object:Gem::Version
|
68
|
-
version: 3.
|
74
|
+
version: 3.2.0
|
69
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
76
|
requirements:
|
71
77
|
- - ">="
|
data/Steepfile
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
# Steepfile
|
2
|
-
|
3
|
-
D = Steep::Diagnostic
|
4
|
-
|
5
|
-
target :lib do
|
6
|
-
signature "sig"
|
7
|
-
|
8
|
-
check "lib"
|
9
|
-
|
10
|
-
library "monitor", "prism"
|
11
|
-
|
12
|
-
# Configure diagnostics with lenient settings for metaprogramming-heavy code
|
13
|
-
configure_code_diagnostics do |hash|
|
14
|
-
hash[D::Ruby::UnannotatedEmptyCollection] = :information
|
15
|
-
hash[D::Ruby::UnknownInstanceVariable] = :information
|
16
|
-
hash[D::Ruby::FallbackAny] = :information
|
17
|
-
hash[D::Ruby::NoMethod] = :warning
|
18
|
-
hash[D::Ruby::UndeclaredMethodDefinition] = :information
|
19
|
-
end
|
20
|
-
end
|