taski 0.1.1 → 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.
@@ -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
@@ -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.0"
5
5
  end
data/lib/taski.rb CHANGED
@@ -1,143 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "taski/version"
4
-
5
- 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
3
+ require 'set'
4
+ require 'monitor'
72
5
 
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 = []
6
+ # Load core components
7
+ require_relative "taski/version"
8
+ require_relative "taski/exceptions"
9
+ require_relative "taski/reference"
10
+ require_relative "taski/dependency_analyzer"
133
11
 
134
- while queue.any?
135
- resolved << task_class = queue.shift
136
- task_class.resolve(queue, resolved)
137
- end
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"
138
18
 
139
- resolved
140
- end
141
- end
142
- end
143
- end
19
+ module Taski
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
40
+ end