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.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'monitor'
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
- def build
12
- resolve_dependencies.reverse.each do |task_class|
13
- task_class.ensure_instance_built
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
- raise CircularDependencyError, "Circular dependency detected: #{self.name} is already being built"
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
- "#{self.name}#{THREAD_KEY_SUFFIX}"
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 = self.new
94
- begin
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
- # 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
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Taski
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
data/lib/taski.rb CHANGED
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
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, Proc block, **untyped options) -> void
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.0
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-15 00:00:00.000000000 Z
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
- - Steepfile
42
- - examples/complex_example.rb
43
- - examples/readme_example.rb
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.1.0
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