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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d04adfab160e07e101e7d6364796d5607a896010396950ee5d479824bb4c422e
4
- data.tar.gz: aeb0ea3476853608c0ef77e80e907c79db7ea62c1aaead8a22d8232d7e4c13aa
3
+ metadata.gz: 4e44ea93464ceaf8994d761cec75cf4d32df7cdb8f90d25b4cfcae5be7f8a586
4
+ data.tar.gz: 8a1b5fae85bbffb28756f2e13b3f98c2cb8023c76346b5da722ac55e87ef3cc5
5
5
  SHA512:
6
- metadata.gz: b3201ee69bc45ae1e58569bf1a98647ff7e38adabf0d04334805fdbac816468d420b1d460113970e1fc9394e6f9e6ca2fe4c700e0c390b9032366a01690c7762
7
- data.tar.gz: 81a9796f7234f8f66216b61b7ea676b40614bb8258b8a6d6a0244e5d016319ea1492f7817324e1669a6ede1eeaf3f0ad4d8b71fd210168022838f66047348bff
6
+ metadata.gz: '037285b1bc19edd6d674f6748674fa8443c0cb40d5199de86bdfa3696c1597308d69ff5236a7114c43cf666a27e62f5c4111c5a36301736c4fdac98cdd65b4c4'
7
+ data.tar.gz: afa967d630b56f9de1f4974879533572d06c42d574dce6bea69da9635ce69e06251af555168ff0b3475e95d5a1d6d07e8951e6052f0f6e04291d07b15d23239b
data/.standard.yml ADDED
@@ -0,0 +1,9 @@
1
+ ruby_version: 3.2
2
+
3
+ ignore:
4
+ - 'pkg/**/*'
5
+ - 'vendor/**/*'
6
+ - 'bin/**/*'
7
+
8
+ # Allow eval for dynamic method creation in define API
9
+ parallel: true
data/README.md CHANGED
@@ -1,68 +1,225 @@
1
1
  # Taski
2
2
 
3
- **Taski** is a Ruby-based task runner designed for small, composable processing steps.
4
- In Taski, you define tasks as Ruby classes that expose named values through `define`. Dependencies between tasks are established automatically when one task references the result of another—no need for explicit dependency declarations.
3
+ [![CI](https://github.com/ahogappa/taski/workflows/CI/badge.svg)](https://github.com/ahogappa/taski/actions/workflows/ci.yml)
4
+ [![Codecov](https://codecov.io/gh/ahogappa/taski/branch/master/graph/badge.svg)](https://codecov.io/gh/ahogappa/taski)
5
+ [![Gem Version](https://badge.fury.io/rb/taski.svg)](https://badge.fury.io/rb/taski)
5
6
 
6
- Tasks are executed in a topologically sorted order, ensuring that tasks are built only after their inputs are available. Reverse execution is also supported, making it easy to clean up intermediate files or revert changes after a build.
7
+ > **🚧 Development Status:** Taski is currently under active development. Not yet recommended for production use.
7
8
 
8
- > **🚧 Development Status:** Taski is currently under active development and the API may change.
9
+ **Taski** is a Ruby framework for building task dependency graphs with automatic resolution and execution. It provides two APIs: static dependencies through **Exports** and dynamic dependencies through **Define**.
9
10
 
10
- > **⚠️ Limitation:** Circular dependencies are **not** supported at this time.
11
+ > **Name Origin**: "Taski" comes from the Japanese word "襷" (tasuki), a sash used in relay races. Just like how runners pass the sash to the next teammate, tasks in Taski pass dependencies to one another in a continuous chain.
11
12
 
12
- > **ℹ️ Note:** Taski does **not** infer dependencies from file contents or behavior. Instead, dependencies are implicitly established via references between task definitions.
13
+ ## 🚀 Quick Start
13
14
 
14
- ### Features
15
+ ```ruby
16
+ require 'taski'
15
17
 
16
- - Define tasks using Ruby classes
17
- - Implicit dependencies via reference to other task outputs
18
- - Topological execution order
19
- - Reverse execution for cleanup
20
- - Built entirely in Ruby
18
+ # Static dependency using Exports API
19
+ class DatabaseSetup < Taski::Task
20
+ exports :connection_string
21
21
 
22
- ### Example
22
+ def build
23
+ @connection_string = "postgresql://localhost/myapp"
24
+ puts "Database configured"
25
+ end
26
+ end
27
+
28
+ class APIServer < Taski::Task
29
+ def build
30
+ puts "Starting API with #{DatabaseSetup.connection_string}"
31
+ end
32
+ end
33
+
34
+ APIServer.build
35
+ # => Database configured
36
+ # => Starting API with postgresql://localhost/myapp
37
+ ```
38
+
39
+ ## 📚 API Guide
40
+
41
+ ### Exports API - Static Dependencies
42
+
43
+ For simple, predictable dependencies:
23
44
 
24
45
  ```ruby
25
- class TaskA < Taski::Task
26
- define :task_a_result, -> { "Task A" }
46
+ class ConfigLoader < Taski::Task
47
+ exports :app_name, :version
27
48
 
28
49
  def build
29
- puts 'Processing...'
50
+ @app_name = "MyApp"
51
+ @version = "1.0.0"
52
+ puts "Config loaded: #{@app_name} v#{@version}"
30
53
  end
31
54
  end
32
55
 
33
- class TaskB < Taski::Task
34
- define :simple_task, -> { "Task result is #{TaskA.task_a_result}" }
56
+ class Deployment < Taski::Task
57
+ def build
58
+ @deploy_url = "https://#{ConfigLoader.app_name}.example.com"
59
+ puts "Deploying to #{@deploy_url}"
60
+ end
61
+ end
62
+
63
+ Deployment.build
64
+ # => Config loaded: MyApp v1.0.0
65
+ # => Deploying to https://MyApp.example.com
66
+ ```
67
+
68
+ ### Define API - Dynamic Dependencies
69
+
70
+ For dependencies that change based on runtime conditions:
71
+
72
+ ```ruby
73
+ class EnvironmentConfig < Taski::Task
74
+ define :database_service, -> {
75
+ case ENV['RAILS_ENV']
76
+ when 'production'
77
+ "production-db.example.com"
78
+ else
79
+ "localhost:5432"
80
+ end
81
+ }
35
82
 
36
83
  def build
37
- puts simple_task
84
+ puts "Using database: #{database_service}"
85
+ puts "Environment: #{ENV['RAILS_ENV'] || 'development'}"
38
86
  end
39
87
  end
40
88
 
41
- TaskB.build
42
- # => Processing...
43
- # => Task result is Task A
89
+ EnvironmentConfig.build
90
+ # => Using database: localhost:5432
91
+ # => Environment: development
92
+
93
+ ENV['RAILS_ENV'] = 'production'
94
+ EnvironmentConfig.reset!
95
+ EnvironmentConfig.build
96
+ # => Using database: production-db.example.com
97
+ # => Environment: production
44
98
  ```
45
99
 
46
- ## Installation
100
+ ### When to Use Each API
47
101
 
48
- Install the gem and add to the application's Gemfile by executing:
102
+ - **Define API**: Best for dynamic runtime dependencies. Cannot contain side effects in definition blocks.
103
+ - **Exports API**: Ideal for static dependencies. Supports side effects in build methods.
49
104
 
50
- ```bash
51
- bundle add taski
105
+ | Use Case | API | Example |
106
+ |----------|-----|---------|
107
+ | Configuration values | Exports | File paths, settings |
108
+ | Environment-specific logic | Define | Different services per env |
109
+ | Side effects | Exports | Database connections, I/O |
110
+ | Conditional processing | Define | Algorithm selection |
111
+
112
+ ## ✨ Key Features
113
+
114
+ - **Automatic Dependency Resolution**: Dependencies detected through static analysis
115
+ - **Thread-Safe**: Safe for concurrent access
116
+ - **Circular Dependency Detection**: Clear error messages with detailed paths
117
+ - **Granular Execution**: Build individual tasks or complete graphs
118
+ - **Memory Management**: Built-in reset mechanisms
119
+
120
+ ### Granular Task Execution
121
+
122
+ Execute any task individually - Taski builds only required dependencies:
123
+
124
+ ```ruby
125
+ # Build specific components
126
+ ConfigLoader.build # Builds only ConfigLoader
127
+ # => Config loaded: MyApp v1.0.0
128
+
129
+ EnvironmentConfig.build # Builds EnvironmentConfig and its dependencies
130
+ # => Using database: localhost:5432
131
+ # => Environment: development
132
+
133
+ # Access values (triggers build if needed)
134
+ puts ConfigLoader.version # Builds ConfigLoader if not built
135
+ # => 1.0.0
136
+ ```
137
+
138
+ ### Lifecycle Management
139
+
140
+ Tasks can define both build and clean methods. Clean operations run in reverse dependency order:
141
+
142
+ ```ruby
143
+ class DatabaseSetup < Taski::Task
144
+ exports :connection
145
+
146
+ def build
147
+ @connection = "db-connection"
148
+ puts "Database connected"
149
+ end
150
+
151
+ def clean
152
+ puts "Database disconnected"
153
+ end
154
+ end
155
+
156
+ class WebServer < Taski::Task
157
+ def build
158
+ puts "Web server started with #{DatabaseSetup.connection}"
159
+ end
160
+
161
+ def clean
162
+ puts "Web server stopped"
163
+ end
164
+ end
165
+
166
+ WebServer.build
167
+ # => Database connected
168
+ # => Web server started with db-connection
169
+
170
+ WebServer.clean
171
+ # => Web server stopped
172
+ # => Database disconnected
173
+ ```
174
+
175
+ ### Error Handling
176
+
177
+ ```ruby
178
+ begin
179
+ TaskWithCircularDep.build
180
+ rescue Taski::CircularDependencyError => e
181
+ puts "Circular dependency: #{e.message}"
182
+ end
183
+ # => Circular dependency: Circular dependency detected!
184
+ # => Cycle: TaskA → TaskB → TaskA
185
+ # =>
186
+ # => The dependency chain is:
187
+ # => 1. TaskA is trying to build → TaskB
188
+ # => 2. TaskB is trying to build → TaskA
52
189
  ```
53
190
 
54
- If bundler is not being used to manage dependencies, install the gem by executing:
191
+ ## 📦 Installation
192
+
193
+ ```ruby
194
+ gem 'taski'
195
+ ```
55
196
 
56
197
  ```bash
57
- gem install taski
198
+ bundle install
58
199
  ```
59
200
 
60
- ## Development
201
+ ## 🧪 Testing
202
+
203
+ ```bash
204
+ bundle exec rake test
205
+ ```
61
206
 
62
- After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
207
+ ## 🏛️ Architecture
63
208
 
64
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
209
+ - **Task Base**: Core framework
210
+ - **Exports API**: Static dependency resolution
211
+ - **Define API**: Dynamic dependency resolution
212
+ - **Instance Management**: Thread-safe lifecycle
213
+ - **Dependency Resolver**: Topological sorting
65
214
 
66
215
  ## Contributing
67
216
 
68
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/taski.
217
+ Bug reports and pull requests welcome at https://github.com/ahogappa/taski.
218
+
219
+ ## License
220
+
221
+ MIT License
222
+
223
+ ---
224
+
225
+ **Taski** - Build dependency graphs with elegant Ruby code. 🚀
data/Rakefile CHANGED
@@ -10,4 +10,10 @@ Rake::TestTask.new(:test) do |t|
10
10
  t.verbose = true
11
11
  end
12
12
 
13
- task default: %i[test]
13
+ begin
14
+ require "standard/rake"
15
+ rescue LoadError
16
+ # Standard not available
17
+ end
18
+
19
+ task default: %i[test standard]
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env ruby
2
+ # Complex example from README showing both APIs
3
+
4
+ require_relative "../lib/taski"
5
+
6
+ # Mock classes for the example
7
+ class ProductionDB < Taski::Task
8
+ exports :connection_string
9
+ def build
10
+ @connection_string = "postgres://prod-server/app"
11
+ end
12
+ end
13
+
14
+ class TestDB < Taski::Task
15
+ exports :connection_string
16
+ def build
17
+ @connection_string = "postgres://test-server/app_test"
18
+ end
19
+ end
20
+
21
+ module FeatureFlag
22
+ def self.enabled?(flag)
23
+ ENV["FEATURE_#{flag.to_s.upcase}"] == "true"
24
+ end
25
+ end
26
+
27
+ class RedisService < Taski::Task
28
+ exports :configuration
29
+ def build
30
+ @configuration = "redis://localhost:6379"
31
+ end
32
+ end
33
+
34
+ # Environment configuration using Define API
35
+ class Environment < Taski::Task
36
+ define :database_url, -> {
37
+ case ENV["RAILS_ENV"]
38
+ when "production"
39
+ ProductionDB.connection_string
40
+ when "test"
41
+ TestDB.connection_string
42
+ else
43
+ "sqlite3://development.db"
44
+ end
45
+ }
46
+
47
+ define :redis_config, -> {
48
+ if FeatureFlag.enabled?(:redis_cache)
49
+ RedisService.configuration
50
+ end
51
+ }
52
+
53
+ def build
54
+ # Environment configuration is handled by define blocks
55
+ end
56
+ end
57
+
58
+ # Static configuration using Exports API
59
+ class AppConfig < Taski::Task
60
+ exports :app_name, :version, :port
61
+
62
+ def build
63
+ @app_name = "MyWebApp"
64
+ @version = "2.1.0"
65
+ @port = ENV.fetch("PORT", 3000).to_i
66
+ end
67
+ end
68
+
69
+ # Application startup combining both APIs
70
+ class Application < Taski::Task
71
+ def build
72
+ puts "Starting #{AppConfig.app_name} v#{AppConfig.version}"
73
+ puts "Database: #{Environment.database_url}"
74
+ puts "Redis: #{Environment.redis_config || "disabled"}"
75
+ puts "Port: #{AppConfig.port}"
76
+ end
77
+
78
+ def clean
79
+ puts "Shutting down #{AppConfig.app_name}..."
80
+ end
81
+ end
82
+
83
+ # Test different environments
84
+ puts "=== Complex Example ==="
85
+
86
+ puts "\n1. Development Environment (default):"
87
+ ENV.delete("RAILS_ENV")
88
+ ENV.delete("FEATURE_REDIS_CACHE")
89
+ Application.build
90
+ Application.reset!
91
+
92
+ puts "\n2. Test Environment:"
93
+ ENV["RAILS_ENV"] = "test"
94
+ # Reset Environment to re-evaluate define blocks
95
+ Environment.reset!
96
+ Application.build
97
+ Application.reset!
98
+
99
+ puts "\n3. Production with Redis:"
100
+ ENV["RAILS_ENV"] = "production"
101
+ ENV["FEATURE_REDIS_CACHE"] = "true"
102
+ # Reset Environment to re-evaluate define blocks
103
+ Environment.reset!
104
+ Application.build
105
+
106
+ puts "\n4. Cleanup:"
107
+ Application.clean
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # Quick Start example from README
3
+
4
+ require_relative "../lib/taski"
5
+
6
+ # Simple static dependency using Exports API
7
+ class DatabaseSetup < Taski::Task
8
+ exports :connection_string
9
+
10
+ def build
11
+ @connection_string = "postgresql://localhost/myapp"
12
+ puts "Database configured"
13
+ end
14
+ end
15
+
16
+ class APIServer < Taski::Task
17
+ exports :port
18
+
19
+ def build
20
+ # Automatic dependency: DatabaseSetup will be built first
21
+ puts "Starting API with #{DatabaseSetup.connection_string}"
22
+ @port = 3000
23
+ end
24
+ end
25
+
26
+ # Execute - dependencies are resolved automatically
27
+ puts "=== Quick Start Example ==="
28
+ APIServer.build
29
+
30
+ puts "\nResult: APIServer running on port #{APIServer.port}"
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Taski
6
+ module DependencyAnalyzer
7
+ class << self
8
+ def analyze_method(klass, method_name)
9
+ return [] unless klass.instance_methods(false).include?(method_name)
10
+
11
+ method = klass.instance_method(method_name)
12
+ source_location = method.source_location
13
+ return [] unless source_location
14
+
15
+ file_path, line_number = source_location
16
+ return [] unless File.exist?(file_path)
17
+
18
+ begin
19
+ result = Prism.parse_file(file_path)
20
+
21
+ unless result.success?
22
+ Taski.logger.error("Parse errors in source file",
23
+ file: file_path,
24
+ errors: result.errors.map(&:message),
25
+ method: "#{klass}##{method_name}")
26
+ return []
27
+ end
28
+
29
+ # Handle warnings if present
30
+ if result.warnings.any?
31
+ Taski.logger.warn("Parse warnings in source file",
32
+ file: file_path,
33
+ warnings: result.warnings.map(&:message),
34
+ method: "#{klass}##{method_name}")
35
+ end
36
+
37
+ dependencies = []
38
+ method_node = find_method_node(result.value, method_name, line_number)
39
+
40
+ if method_node
41
+ visitor = TaskDependencyVisitor.new
42
+ visitor.visit(method_node)
43
+ dependencies = visitor.dependencies
44
+ end
45
+
46
+ dependencies.uniq
47
+ rescue IOError, SystemCallError => e
48
+ Taski.logger.error("Failed to read source file",
49
+ file: file_path,
50
+ error: e.message,
51
+ method: "#{klass}##{method_name}")
52
+ []
53
+ rescue => e
54
+ Taski.logger.error("Failed to analyze method dependencies",
55
+ class: klass.name,
56
+ method: method_name,
57
+ error: e.message,
58
+ error_class: e.class.name)
59
+ []
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def find_method_node(node, method_name, target_line)
66
+ return nil unless node
67
+
68
+ case node
69
+ when Prism::DefNode
70
+ if node.name == method_name && node.location.start_line <= target_line && node.location.end_line >= target_line
71
+ return node
72
+ end
73
+ when Prism::ClassNode, Prism::ModuleNode
74
+ if node.respond_to?(:body)
75
+ return find_method_node(node.body, method_name, target_line)
76
+ end
77
+ when Prism::StatementsNode
78
+ node.body.each do |child|
79
+ result = find_method_node(child, method_name, target_line)
80
+ return result if result
81
+ end
82
+ end
83
+
84
+ # Recursively search child nodes
85
+ if node.respond_to?(:child_nodes)
86
+ node.child_nodes.each do |child|
87
+ result = find_method_node(child, method_name, target_line)
88
+ return result if result
89
+ end
90
+ end
91
+
92
+ nil
93
+ end
94
+
95
+ # Task dependency visitor using Prism's visitor pattern
96
+ class TaskDependencyVisitor < Prism::Visitor
97
+ attr_reader :dependencies
98
+
99
+ def initialize
100
+ @dependencies = []
101
+ @constant_cache = {}
102
+ end
103
+
104
+ def visit_constant_read_node(node)
105
+ check_task_constant(node.name.to_s)
106
+ super
107
+ end
108
+
109
+ def visit_constant_path_node(node)
110
+ const_path = extract_constant_path(node)
111
+ check_task_constant(const_path) if const_path
112
+ super
113
+ end
114
+
115
+ def visit_call_node(node)
116
+ # Check for method calls on constants (e.g., TaskA.result)
117
+ case node.receiver
118
+ when Prism::ConstantReadNode
119
+ check_task_constant(node.receiver.name.to_s)
120
+ when Prism::ConstantPathNode
121
+ const_path = extract_constant_path(node.receiver)
122
+ check_task_constant(const_path) if const_path
123
+ end
124
+ super
125
+ end
126
+
127
+ private
128
+
129
+ def check_task_constant(const_name)
130
+ return unless const_name
131
+
132
+ # Use caching to avoid repeated constant resolution
133
+ cached_result = @constant_cache[const_name]
134
+ return cached_result if cached_result == false # Cached negative result
135
+ return @dependencies << cached_result if cached_result # Cached positive result
136
+
137
+ begin
138
+ if Object.const_defined?(const_name)
139
+ klass = Object.const_get(const_name)
140
+ if klass.is_a?(Class) && klass < Taski::Task
141
+ @constant_cache[const_name] = klass
142
+ @dependencies << klass
143
+ else
144
+ @constant_cache[const_name] = false
145
+ end
146
+ else
147
+ @constant_cache[const_name] = false
148
+ end
149
+ rescue NameError, ArgumentError
150
+ @constant_cache[const_name] = false
151
+ end
152
+ end
153
+
154
+ def extract_constant_path(node)
155
+ case node
156
+ when Prism::ConstantReadNode
157
+ node.name.to_s
158
+ when Prism::ConstantPathNode
159
+ parent_path = extract_constant_path(node.parent) if node.parent
160
+ child_name = node.name.to_s
161
+
162
+ if parent_path && child_name
163
+ "#{parent_path}::#{child_name}"
164
+ else
165
+ child_name
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -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