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,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