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.
@@ -0,0 +1,176 @@
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
+ # Build dependency path for better error message
60
+ cycle_path = build_current_dependency_path
61
+ raise CircularDependencyError, build_runtime_circular_dependency_message(cycle_path)
62
+ end
63
+
64
+ Thread.current[thread_key] = true
65
+ begin
66
+ build_dependencies
67
+ @__task_instance = build_instance
68
+ ensure
69
+ Thread.current[thread_key] = false
70
+ end
71
+ end
72
+
73
+ @__task_instance
74
+ end
75
+
76
+ private
77
+
78
+ # === Core Helper Methods ===
79
+
80
+ # Get or create build monitor for thread safety
81
+ # @return [Monitor] Thread-safe monitor object
82
+ def build_monitor
83
+ @__build_monitor ||= Monitor.new
84
+ end
85
+
86
+ # Generate thread key for recursion detection
87
+ # @return [String] Thread key for this task
88
+ def build_thread_key
89
+ "#{name}#{THREAD_KEY_SUFFIX}"
90
+ end
91
+
92
+ # Build and configure task instance
93
+ # @return [Task] Built task instance
94
+ def build_instance
95
+ instance = new
96
+ build_start_time = Time.now
97
+ begin
98
+ Taski.logger.task_build_start(name.to_s, dependencies: @dependencies || [])
99
+ instance.build
100
+ duration = Time.now - build_start_time
101
+ Taski.logger.task_build_complete(name.to_s, duration: duration)
102
+ instance
103
+ rescue => e
104
+ duration = Time.now - build_start_time
105
+ # Log the error with full context
106
+ Taski.logger.task_build_failed(name.to_s, error: e, duration: duration)
107
+ raise TaskBuildError, "Failed to build task #{name}: #{e.message}"
108
+ end
109
+ end
110
+
111
+ # Clear thread-local state for this task
112
+ def clear_thread_local_state
113
+ Thread.current.keys.each do |key|
114
+ Thread.current[key] = nil if key.to_s.include?(build_thread_key)
115
+ end
116
+ end
117
+
118
+ # === Dependency Management ===
119
+
120
+ # Build all dependencies of this task
121
+ def build_dependencies
122
+ resolve_dependencies
123
+
124
+ (@dependencies || []).each do |dep|
125
+ dep_class = extract_class(dep)
126
+ next if dep_class == self
127
+
128
+ dep_class.ensure_instance_built if dep_class.respond_to?(:ensure_instance_built)
129
+ end
130
+ end
131
+
132
+ # Build current dependency path from thread-local storage
133
+ # @return [Array<Class>] Array of classes in the current build path
134
+ def build_current_dependency_path
135
+ path = []
136
+ Thread.current.keys.each do |key|
137
+ if key.to_s.end_with?(THREAD_KEY_SUFFIX) && Thread.current[key]
138
+ class_name = key.to_s.sub(THREAD_KEY_SUFFIX, "")
139
+ begin
140
+ path << Object.const_get(class_name)
141
+ rescue NameError
142
+ # Skip if class not found
143
+ end
144
+ end
145
+ end
146
+ path << self
147
+ end
148
+
149
+ # Build runtime circular dependency error message
150
+ # @param cycle_path [Array<Class>] The circular dependency path
151
+ # @return [String] Formatted error message
152
+ def build_runtime_circular_dependency_message(cycle_path)
153
+ path_names = cycle_path.map { |klass| klass.name || klass.to_s }
154
+
155
+ message = "Circular dependency detected!\n"
156
+ message += "Cycle: #{path_names.join(" → ")}\n\n"
157
+ message += "The dependency chain is:\n"
158
+
159
+ cycle_path.each_cons(2).with_index do |(from, to), index|
160
+ message += " #{index + 1}. #{from.name} is trying to build → #{to.name}\n"
161
+ end
162
+
163
+ message += "\nThis creates an infinite loop that cannot be resolved."
164
+ message
165
+ end
166
+
167
+ # Extract class from dependency hash
168
+ # @param dep [Hash] Dependency information
169
+ # @return [Class] The dependency class
170
+ def extract_class(dep)
171
+ klass = dep[:klass]
172
+ klass.is_a?(Reference) ? klass.deref : klass
173
+ end
174
+ end
175
+ end
176
+ 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.1.1"
4
+ VERSION = "0.2.1"
5
5
  end
data/lib/taski.rb CHANGED
@@ -1,143 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
5
+ # Load core components
3
6
  require_relative "taski/version"
7
+ require_relative "taski/exceptions"
8
+ require_relative "taski/logger"
9
+ require_relative "taski/reference"
10
+ require_relative "taski/dependency_analyzer"
11
+
12
+ # Load Task class components
13
+ require_relative "taski/task/base"
14
+ require_relative "taski/task/exports_api"
15
+ require_relative "taski/task/define_api"
16
+ require_relative "taski/task/instance_management"
17
+ require_relative "taski/task/dependency_resolver"
4
18
 
5
19
  module Taski
6
- class Reference
7
- def initialize(klass)
8
- @klass = klass
9
- end
10
-
11
- def deref
12
- Object.const_get(@klass)
13
- end
14
-
15
- def inspect
16
- "&#{@klass}"
17
- end
18
- end
19
-
20
- class Task
21
- class << self
22
- def ref(klass)
23
- ref = Reference.new(klass)
24
- throw :unresolved, ref
25
- end
26
-
27
- def define(name, block, **options)
28
- @dependencies ||= []
29
- @definitions ||= {}
30
-
31
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
32
- def self.#{name}
33
- __resolve__[__callee__] ||= false
34
- if __resolve__[__callee__]
35
- # already resolved
36
- else
37
- __resolve__[__callee__] = true
38
- throw :unresolved, [self, __callee__]
39
- end
40
- end
41
- RUBY
42
-
43
- classes = []
44
- loop do
45
- klass, task = catch(:unresolved) do
46
- block.call
47
- nil
48
- end
49
-
50
- if klass.nil?
51
- classes.each do |task_class|
52
- task_class[:klass].class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
53
- __resolve__ = {}
54
- RUBY
55
- end
56
-
57
- break
58
- else
59
- classes << { klass:, task: }
60
- end
61
- end
62
-
63
- @dependencies += classes
64
- @definitions[name] = { block:, options:, classes: }
65
- end
66
-
67
- def build
68
- resolve_dependencies.reverse.each do |task_class|
69
- task_class.new.build
70
- end
71
- end
72
-
73
- def clean
74
- resolve_dependencies.each do |task_class|
75
- task_class.new.clean
76
- end
77
- end
78
-
79
- def refresh
80
- # TODO
81
- end
82
-
83
- def resolve(queue, resolved)
84
- @dependencies.each do |task|
85
- if task[:klass].is_a?(Reference)
86
- task[:klass].deref
87
- else
88
- task[:klass]
89
- end => task_class
90
-
91
- # increase priority
92
- if resolved.include?(task_class)
93
- resolved.delete(task_class)
94
- end
95
- queue << task_class
96
- end
97
-
98
- # override
99
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
100
- def self.ref(klass)
101
- Object.const_get(klass)
102
- end
103
- RUBY
104
-
105
- @definitions.each do |name, (_block, _options)|
106
- # override
107
- class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
108
- def self.#{name}
109
- @__#{name} ||= @definitions[:#{name}][:block].call
110
- end
111
- RUBY
112
-
113
- define_method(name) do
114
- unless instance_variable_defined?("@__#{name}")
115
- instance_variable_set("@__#{name}", self.class.send(name))
116
- end
117
- instance_variable_get("@__#{name}")
118
- end
119
- end
120
-
121
- self
122
- end
123
-
124
- private
125
-
126
- def __resolve__
127
- @__resolve__ ||= {}
128
- end
129
-
130
- def resolve_dependencies
131
- queue = [self]
132
- resolved = []
133
-
134
- while queue.any?
135
- resolved << task_class = queue.shift
136
- task_class.resolve(queue, resolved)
137
- end
138
-
139
- resolved
140
- end
141
- end
142
- end
20
+ # Main module for the Taski task framework
21
+ #
22
+ # Taski provides a framework for defining and managing task dependencies
23
+ # with two complementary APIs:
24
+ # 1. Exports API - Export instance variables as class methods (static dependencies)
25
+ # 2. Define API - Define lazy-evaluated values with dynamic dependency resolution
26
+ #
27
+ # Use Define API when:
28
+ # - Dependencies change based on runtime conditions
29
+ # - Environment-specific configurations
30
+ # - Feature flags determine which classes to use
31
+ # - Complex conditional logic determines dependencies
32
+ #
33
+ # Features:
34
+ # - Automatic dependency resolution (static and dynamic)
35
+ # - Static analysis of method dependencies
36
+ # - Runtime dependency resolution for conditional logic
37
+ # - Thread-safe task building
38
+ # - Circular dependency detection
39
+ # - Memory leak prevention
143
40
  end
data/sig/taski.rbs CHANGED
@@ -1,4 +1,99 @@
1
1
  module Taski
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end
3
+
4
+ # Custom exceptions
5
+ class CircularDependencyError < StandardError
6
+ end
7
+
8
+ class TaskAnalysisError < StandardError
9
+ end
10
+
11
+ class TaskBuildError < StandardError
12
+ end
13
+
14
+ # Reference class for task references
15
+ class Reference
16
+ @klass: String
17
+
18
+ def initialize: (String klass) -> void
19
+ def deref: () -> Class
20
+ def ==: (untyped other) -> bool
21
+ def inspect: () -> String
22
+ end
23
+
24
+ # Main Task class
25
+ class Task
26
+ # Constants
27
+ THREAD_KEY_SUFFIX: String
28
+ TASKI_ANALYZING_DEFINE_KEY: Symbol
29
+ ANALYZED_METHODS: Array[Symbol]
30
+
31
+ # Hook methods
32
+ def self.method_added: (Symbol method_name) -> void
33
+
34
+ # Public API methods
35
+ def self.exports: (*Symbol names) -> void
36
+ def self.define: (Symbol name) { () -> untyped } -> void
37
+ def self.build: () -> void
38
+ def self.clean: () -> void
39
+ def self.reset!: () -> self
40
+ def self.refresh: () -> self
41
+ def self.resolve: (Array[untyped] queue, Array[untyped] resolved) -> self
42
+ def self.ref: (String | Class klass) -> (Reference | Class)
43
+ def self.ensure_instance_built: () -> Task
44
+
45
+ # Define API methods
46
+ def self.__resolve__: () -> Hash[Symbol, untyped]
47
+ def self.resolve_dependencies: () -> void
48
+
49
+ # Instance methods
50
+ def build: () -> void
51
+ def clean: () -> void
52
+
53
+ # Allow dynamic method definitions
54
+ def method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
55
+ def respond_to_missing?: (Symbol name, bool include_private) -> bool
56
+
57
+ # Allow dynamic class method definitions
58
+ def self.method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
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
73
+ end
74
+
75
+ # Dependency analyzer module
76
+ module DependencyAnalyzer
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]
98
+ end
99
+ end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taski
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ahogappa
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-05-18 00:00:00.000000000 Z
11
- dependencies: []
10
+ date: 2025-06-21 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.4'
12
26
  description: Taski is a Ruby-based task runner currently under development. It allows
13
27
  you to define small, composable tasks along with the outputs they depend on. Taski
14
28
  statically resolves dependencies and executes tasks in the correct topological order,
@@ -21,11 +35,22 @@ executables: []
21
35
  extensions: []
22
36
  extra_rdoc_files: []
23
37
  files:
38
+ - ".standard.yml"
24
39
  - LICENSE
25
40
  - README.md
26
41
  - Rakefile
42
+ - examples/complex_example.rb
43
+ - examples/readme_example.rb
27
44
  - lib/taski.rb
28
- - lib/taski/utils.rb
45
+ - lib/taski/dependency_analyzer.rb
46
+ - lib/taski/exceptions.rb
47
+ - lib/taski/logger.rb
48
+ - lib/taski/reference.rb
49
+ - lib/taski/task/base.rb
50
+ - lib/taski/task/define_api.rb
51
+ - lib/taski/task/dependency_resolver.rb
52
+ - lib/taski/task/exports_api.rb
53
+ - lib/taski/task/instance_management.rb
29
54
  - lib/taski/version.rb
30
55
  - sig/taski.rbs
31
56
  homepage: https://github.com/ahogappa/taski
@@ -41,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
41
66
  requirements:
42
67
  - - ">="
43
68
  - !ruby/object:Gem::Version
44
- version: 3.1.0
69
+ version: 3.2.0
45
70
  required_rubygems_version: !ruby/object:Gem::Requirement
46
71
  requirements:
47
72
  - - ">="
data/lib/taski/utils.rb DELETED
@@ -1,53 +0,0 @@
1
- require 'fileutils'
2
- require 'tmpdir'
3
-
4
- module Taski
5
- class Utils < FileUtils
6
- def rm
7
-
8
- end
9
-
10
- def rm_f
11
- end
12
-
13
- def rm_rf
14
-
15
- end
16
-
17
- def cp
18
-
19
- end
20
-
21
- def cp_r
22
-
23
- end
24
-
25
- def mkdir
26
-
27
- end
28
-
29
- def mkdir_p
30
-
31
- end
32
-
33
- def mktmpdir
34
-
35
- end
36
-
37
- def cmd(command, info = nil, ret = false)
38
- puts "exec: #{info}" if info
39
- puts command
40
-
41
- if ret
42
- ret = `#{command}`.chomp
43
- if $?.exited?
44
- ret
45
- else
46
- raise "Failed to execute command: #{command}"
47
- end
48
- else
49
- system command, exception: true
50
- end
51
- end
52
- end
53
- end