roast-ai 0.1.0 → 0.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/cla.yml +1 -1
- data/.gitignore +1 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +3 -1
- data/Gemfile +0 -1
- data/Gemfile.lock +3 -4
- data/README.md +419 -5
- data/Rakefile +1 -6
- data/docs/INSTRUMENTATION.md +202 -0
- data/examples/api_workflow/README.md +85 -0
- data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
- data/examples/api_workflow/generate_report/prompt.md +10 -0
- data/examples/api_workflow/prompt.md +10 -0
- data/examples/api_workflow/transform_data/prompt.md +10 -0
- data/examples/api_workflow/workflow.yml +30 -0
- data/examples/grading/format_result.rb +25 -9
- data/examples/grading/js_test_runner +31 -0
- data/examples/grading/rb_test_runner +19 -0
- data/examples/grading/read_dependencies/prompt.md +14 -0
- data/examples/grading/run_coverage.rb +2 -2
- data/examples/grading/workflow.yml +3 -12
- data/examples/instrumentation.rb +76 -0
- data/examples/rspec_to_minitest/README.md +68 -0
- data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
- data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
- data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
- data/examples/rspec_to_minitest/workflow.md +10 -0
- data/examples/rspec_to_minitest/workflow.yml +40 -0
- data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
- data/lib/roast/helpers/prompt_loader.rb +2 -0
- data/lib/roast/resources/api_resource.rb +137 -0
- data/lib/roast/resources/base_resource.rb +47 -0
- data/lib/roast/resources/directory_resource.rb +40 -0
- data/lib/roast/resources/file_resource.rb +33 -0
- data/lib/roast/resources/none_resource.rb +29 -0
- data/lib/roast/resources/url_resource.rb +63 -0
- data/lib/roast/resources.rb +100 -0
- data/lib/roast/tools/coding_agent.rb +69 -0
- data/lib/roast/tools.rb +1 -0
- data/lib/roast/version.rb +1 -1
- data/lib/roast/workflow/base_step.rb +21 -17
- data/lib/roast/workflow/base_workflow.rb +69 -17
- data/lib/roast/workflow/configuration.rb +83 -8
- data/lib/roast/workflow/configuration_parser.rb +218 -3
- data/lib/roast/workflow/file_state_repository.rb +156 -0
- data/lib/roast/workflow/prompt_step.rb +16 -0
- data/lib/roast/workflow/session_manager.rb +82 -0
- data/lib/roast/workflow/state_repository.rb +21 -0
- data/lib/roast/workflow/workflow_executor.rb +99 -9
- data/lib/roast/workflow.rb +4 -0
- data/lib/roast.rb +2 -5
- data/roast.gemspec +1 -1
- data/schema/workflow.json +12 -0
- metadata +34 -6
- data/.rspec +0 -1
@@ -0,0 +1,68 @@
|
|
1
|
+
# RSpec to Minitest Migration Workflow
|
2
|
+
|
3
|
+
This workflow demonstrates how to automate the migration of RSpec tests to their Minitest equivalents, following a structured approach to ensure proper test coverage and functionality.
|
4
|
+
|
5
|
+
## Workflow Overview
|
6
|
+
|
7
|
+
The workflow consists of three main steps:
|
8
|
+
|
9
|
+
1. **Analyze Spec**: Understand the purpose and structure of the RSpec test, including its dependencies and testing patterns.
|
10
|
+
2. **Create Minitest**: Generate a new Minitest file with equivalent test coverage and assertions.
|
11
|
+
3. **Run and Improve**: Execute the Minitest file and iteratively improve it until all tests pass.
|
12
|
+
|
13
|
+
## Prerequisites
|
14
|
+
|
15
|
+
- Ruby environment with both RSpec and Minitest gems installed
|
16
|
+
- Access to the original codebase being tested
|
17
|
+
- Ability to run tests in the target environment
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
To use this workflow:
|
22
|
+
|
23
|
+
1. Configure the target pattern in `workflow.yml` to match the RSpec files you want to convert (or pass in via CLI --target option):
|
24
|
+
```yaml
|
25
|
+
target: "path/to/specs/**/*_spec.rb"
|
26
|
+
```
|
27
|
+
|
28
|
+
2. Run the workflow with:
|
29
|
+
```
|
30
|
+
roast execute examples/rspec_to_minitest/workflow.yml
|
31
|
+
```
|
32
|
+
|
33
|
+
3. Review the generated Minitest files and ensure they're correctly placed in your test directory.
|
34
|
+
|
35
|
+
## Implementation Details
|
36
|
+
|
37
|
+
The workflow leverages the following tools:
|
38
|
+
|
39
|
+
- Standard file operations (read/write)
|
40
|
+
- Code search capabilities to find related files
|
41
|
+
- Command execution to run tests
|
42
|
+
- CodingAgent for iterative improvements using AI-powered coding assistance
|
43
|
+
|
44
|
+
## Required Tool: CodingAgent
|
45
|
+
|
46
|
+
This workflow introduces a new tool called `CodingAgent` which leverages Claude Code to perform code-related tasks:
|
47
|
+
|
48
|
+
1. Running tests
|
49
|
+
2. Analyzing errors and failures
|
50
|
+
3. Making iterative improvements to code
|
51
|
+
|
52
|
+
The CodingAgent tool is implemented in `lib/roast/tools/coding_agent.rb`.
|
53
|
+
|
54
|
+
## Conversion Mappings
|
55
|
+
|
56
|
+
The workflow handles these common RSpec to Minitest conversions:
|
57
|
+
|
58
|
+
| RSpec Feature | Minitest Equivalent |
|
59
|
+
|---------------|---------------------|
|
60
|
+
| `describe/context` | Test class |
|
61
|
+
| `it` blocks | `test_*` methods |
|
62
|
+
| `before/after` | `setup/teardown` methods |
|
63
|
+
| `let/let!` | Instance variables or helper methods |
|
64
|
+
| `expect(x).to eq(y)` | `assert_equal y, x` |
|
65
|
+
| `expect(x).to be_truthy` | `assert x` |
|
66
|
+
| `expect(x).to be_falsey` | `refute x` |
|
67
|
+
| `expect { ... }.to raise_error` | `assert_raises { ... }` |
|
68
|
+
| Mocks/doubles | Minitest mocking or Mocha |
|
@@ -0,0 +1,30 @@
|
|
1
|
+
In this first step, try to understand the purpose and dependencies of the spec we will be migrating.
|
2
|
+
|
3
|
+
1. Read the provided RSpec file carefully to understand:
|
4
|
+
- The purpose of the test suite
|
5
|
+
- The subject under test (SUT)
|
6
|
+
- Test structure and organization
|
7
|
+
- Dependencies and fixtures used
|
8
|
+
- Mocks, stubs, and doubles
|
9
|
+
|
10
|
+
2. Use your tools to search for the SUT implementation and any other important dependent files so that they will be in the context for future steps in this process.
|
11
|
+
- Dependencies include fixtures
|
12
|
+
- Note that test/fixtures already has quite a bit of fixture files present
|
13
|
+
- If any fixtures are missing, copy them over when you write the new test file later
|
14
|
+
|
15
|
+
3. Identify RSpec-specific features being used, such as:
|
16
|
+
- describe/context blocks
|
17
|
+
- before/after hooks
|
18
|
+
- let and let! declarations
|
19
|
+
- expect(...).to syntax and matchers
|
20
|
+
- shared examples/contexts
|
21
|
+
- metadata and tags
|
22
|
+
|
23
|
+
4. Provide a summary of your analysis, including:
|
24
|
+
- Purpose of the test suite
|
25
|
+
- Main subject under test
|
26
|
+
- Key dependencies
|
27
|
+
- Testing patterns used
|
28
|
+
- Any potentially challenging aspects for Minitest conversion
|
29
|
+
|
30
|
+
This analysis will guide the next steps of creating an equivalent Minitest implementation.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
You are a Ruby testing expert assisting with migrating RSpec tests to Minitest.
|
2
|
+
|
3
|
+
In this step, you'll create a new Minitest file that replicates the functionality of the analyzed RSpec test.
|
4
|
+
|
5
|
+
## Your tasks:
|
6
|
+
|
7
|
+
1. Convert the RSpec test to an equivalent Minitest test, following these guidelines:
|
8
|
+
- Replace RSpec's `describe`/`context` blocks with Minitest test classes
|
9
|
+
- Convert `it` blocks to Minitest test methods (prefixed with `test_`)
|
10
|
+
- Transform `before`/`after` hooks to `setup`/`teardown` methods
|
11
|
+
- Replace `let`/`let!` declarations with instance variables or helper methods
|
12
|
+
- Convert `expect(...).to` assertions to Minitest assertions
|
13
|
+
- Replace RSpec matchers with equivalent Minitest assertions
|
14
|
+
- Handle mocks and stubs using Minitest's mocking capabilities and Mocha
|
15
|
+
|
16
|
+
2. Follow Minitest conventions:
|
17
|
+
- Name the file with `_test.rb` suffix instead of `_spec.rb`
|
18
|
+
- Create a class that inherits from `ActiveSupport::TestCase`
|
19
|
+
- Use that class's `test "description of the test` method to declare tests kind of like RSpec does
|
20
|
+
- Use Minitest's assertion methods (`assert`, `assert_equal`, etc.)
|
21
|
+
- Implement proper setup and teardown methods as needed
|
22
|
+
|
23
|
+
3. Pay attention to:
|
24
|
+
- Maintaining test coverage with equivalent assertions
|
25
|
+
- Preserving the original test's intent and behavior
|
26
|
+
- Handling RSpec-specific features appropriately
|
27
|
+
- Adding necessary require statements for Minitest and dependencies
|
28
|
+
|
29
|
+
4. Write the complete Minitest file and save it to the appropriate location, replacing `_spec.rb` with `_test.rb` in the filename.
|
30
|
+
|
31
|
+
Your converted Minitest file should maintain at least the same test coverage and intent as the original RSpec test while following Minitest's conventions and patterns.
|
32
|
+
|
33
|
+
IMPORTANT: If you see opportunities to improve the test coverage in the newly written tests, you have my permission to do so. However, note that we should focus on testing behavior, not implementation. Do not test private methods, and never use Ruby tricks to make private methods public. Try to avoid mocking or stubbing anything on the SUT class.
|
@@ -0,0 +1,35 @@
|
|
1
|
+
You are a Ruby testing expert assisting with migrating RSpec tests to Minitest.
|
2
|
+
|
3
|
+
In this final step, you'll use the `cmd` and `coding_agent` tool functions to run the newly created Minitest file and iteratively improve it until all tests pass correctly.
|
4
|
+
|
5
|
+
## Your tasks:
|
6
|
+
|
7
|
+
1. Run the converted Minitest file to see if it passes all tests. Use the `cmd` tool function to execute the test using the following syntax:
|
8
|
+
```ruby
|
9
|
+
ruby -Ilib:test path/to/minitest_file.rb
|
10
|
+
```
|
11
|
+
|
12
|
+
2. Analyze any failures or errors that occur during test execution:
|
13
|
+
- Syntax errors
|
14
|
+
- Missing dependencies
|
15
|
+
- Assertion failures
|
16
|
+
- Test setup/teardown issues
|
17
|
+
- Unexpected behavior
|
18
|
+
|
19
|
+
3. For each issue identified, prompt the `coding_agent` tool to make necessary improvements:
|
20
|
+
- Fix syntax errors
|
21
|
+
- Correct assertion formats
|
22
|
+
- Add missing dependencies
|
23
|
+
- Adjust test setup or teardown
|
24
|
+
- Modify assertions to match expected behavior
|
25
|
+
|
26
|
+
4. Run the test again after each set of improvements.
|
27
|
+
|
28
|
+
5. Continue this iterative process until all tests pass successfully.
|
29
|
+
|
30
|
+
6. Once all tests pass, provide a summary of:
|
31
|
+
- Changes made to fix issues
|
32
|
+
- Any remaining considerations or edge cases
|
33
|
+
- Confirmation of test coverage compared to original RSpec tests
|
34
|
+
|
35
|
+
Again, your goal is to ensure the Minitest version provides at least the same test coverage and reliability as the original RSpec tests, while following Minitest best practices and conventions.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
You are a Ruby testing expert assisting with migrating RSpec tests to Minitest. Note that all of your responses should be in nicely formatted markdown.
|
2
|
+
|
3
|
+
Here is the spec we're going to be migrating today:
|
4
|
+
|
5
|
+
```
|
6
|
+
# <%= file %>
|
7
|
+
<%= File.read(file) %>
|
8
|
+
```
|
9
|
+
|
10
|
+
I have given you powerful tool functions to do this work. The coding_agent is especially powerful and you should delegate complex tasks to it whenever possible.
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# RSpec to Minitest Migration Workflow
|
2
|
+
|
3
|
+
# This workflow demonstrates how to convert RSpec test files to Minitest:
|
4
|
+
# 1. Analyzes the RSpec test to understand its purpose and dependencies
|
5
|
+
# 2. Creates a new Minitest file with equivalent assertions and test structure
|
6
|
+
# 3. Runs the tests and makes improvements until they pass
|
7
|
+
|
8
|
+
name: RSpec to Minitest Migration
|
9
|
+
model: gpt-4.1
|
10
|
+
|
11
|
+
# Target expects a glob pattern matching RSpec files to be converted
|
12
|
+
# target: "**/*_spec.rb"
|
13
|
+
|
14
|
+
tools:
|
15
|
+
- Roast::Tools::ReadFile
|
16
|
+
- Roast::Tools::WriteFile
|
17
|
+
- Roast::Tools::Grep
|
18
|
+
- Roast::Tools::Cmd
|
19
|
+
- Roast::Tools::CodingAgent
|
20
|
+
|
21
|
+
steps:
|
22
|
+
- analyze_spec
|
23
|
+
- create_minitest
|
24
|
+
- run_and_improve
|
25
|
+
- rubocop: $(bundle exec rubocop -A)
|
26
|
+
- Summarize the changes made to the codebase.
|
27
|
+
|
28
|
+
# Configure the steps
|
29
|
+
analyze_spec:
|
30
|
+
print_response: false
|
31
|
+
|
32
|
+
create_minitest:
|
33
|
+
print_response: true
|
34
|
+
|
35
|
+
run_and_improve:
|
36
|
+
print_response: true
|
37
|
+
|
38
|
+
functions:
|
39
|
+
grep:
|
40
|
+
cache: true
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support"
|
4
|
+
require "active_support/isolated_execution_state"
|
3
5
|
require "active_support/cache"
|
4
6
|
require "active_support/notifications"
|
5
7
|
require_relative "logger"
|
@@ -10,17 +12,79 @@ module Roast
|
|
10
12
|
# This module wraps around Raix::FunctionDispatch to provide caching for tool functions
|
11
13
|
module FunctionCachingInterceptor
|
12
14
|
def dispatch_tool_function(function_name, params)
|
13
|
-
|
14
|
-
return super(function_name, params) if configuration.blank?
|
15
|
+
start_time = Time.now
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
ActiveSupport::Notifications.instrument("roast.tool.execute", {
|
18
|
+
function_name: function_name,
|
19
|
+
params: params,
|
20
|
+
})
|
21
|
+
|
22
|
+
# Handle workflows with or without configuration
|
23
|
+
result = if !respond_to?(:configuration) || configuration.nil?
|
22
24
|
super(function_name, params)
|
25
|
+
else
|
26
|
+
function_config = if configuration.respond_to?(:function_config)
|
27
|
+
configuration.function_config(function_name)
|
28
|
+
else
|
29
|
+
{}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if caching is enabled - handle both formats:
|
33
|
+
# 1. cache: true (boolean format)
|
34
|
+
# 2. cache: { enabled: true } (hash format)
|
35
|
+
cache_enabled = if function_config.is_a?(Hash)
|
36
|
+
cache_config = function_config["cache"]
|
37
|
+
if cache_config.is_a?(Hash)
|
38
|
+
cache_config["enabled"]
|
39
|
+
else
|
40
|
+
# Direct boolean value
|
41
|
+
cache_config
|
42
|
+
end
|
43
|
+
else
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
if cache_enabled
|
48
|
+
# Call the original function and pass in the cache
|
49
|
+
super(function_name, params, cache: Roast::Tools::CACHE)
|
50
|
+
else
|
51
|
+
Roast::Helpers::Logger.debug("⚠️ Caching not enabled for #{function_name}")
|
52
|
+
super(function_name, params)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
execution_time = Time.now - start_time
|
57
|
+
|
58
|
+
# Determine if caching was enabled for metrics
|
59
|
+
cache_enabled = if defined?(function_config) && function_config.is_a?(Hash)
|
60
|
+
cache_config = function_config["cache"]
|
61
|
+
if cache_config.is_a?(Hash)
|
62
|
+
cache_config["enabled"]
|
63
|
+
else
|
64
|
+
# Direct boolean value
|
65
|
+
cache_config
|
66
|
+
end
|
67
|
+
else
|
68
|
+
false
|
23
69
|
end
|
70
|
+
|
71
|
+
ActiveSupport::Notifications.instrument("roast.tool.complete", {
|
72
|
+
function_name: function_name,
|
73
|
+
execution_time: execution_time,
|
74
|
+
cache_enabled: cache_enabled,
|
75
|
+
})
|
76
|
+
|
77
|
+
result
|
78
|
+
rescue => e
|
79
|
+
execution_time = Time.now - start_time
|
80
|
+
|
81
|
+
ActiveSupport::Notifications.instrument("roast.tool.error", {
|
82
|
+
function_name: function_name,
|
83
|
+
error: e.class.name,
|
84
|
+
message: e.message,
|
85
|
+
execution_time: execution_time,
|
86
|
+
})
|
87
|
+
raise
|
24
88
|
end
|
25
89
|
end
|
26
90
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Roast
|
8
|
+
module Resources
|
9
|
+
# Resource implementation for API endpoints using Fetch API-style format
|
10
|
+
class ApiResource < BaseResource
|
11
|
+
def process
|
12
|
+
# For API resources, the target might be a JSON configuration or endpoint URL
|
13
|
+
target
|
14
|
+
end
|
15
|
+
|
16
|
+
def config
|
17
|
+
return @config if defined?(@config)
|
18
|
+
|
19
|
+
@config = if target.is_a?(String) && target.match?(/^\s*{/)
|
20
|
+
begin
|
21
|
+
JSON.parse(target)
|
22
|
+
rescue JSON::ParserError
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def api_url
|
29
|
+
if config && config["url"]
|
30
|
+
config["url"]
|
31
|
+
else
|
32
|
+
# Assume direct URL
|
33
|
+
target
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def options
|
38
|
+
return {} unless config
|
39
|
+
|
40
|
+
config["options"] || {}
|
41
|
+
end
|
42
|
+
|
43
|
+
def http_method
|
44
|
+
method_name = (options["method"] || "GET").upcase
|
45
|
+
case method_name
|
46
|
+
when "GET" then Net::HTTP::Get
|
47
|
+
when "POST" then Net::HTTP::Post
|
48
|
+
when "PUT" then Net::HTTP::Put
|
49
|
+
when "DELETE" then Net::HTTP::Delete
|
50
|
+
when "PATCH" then Net::HTTP::Patch
|
51
|
+
when "HEAD" then Net::HTTP::Head
|
52
|
+
else Net::HTTP::Get
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def exists?
|
57
|
+
return false unless target
|
58
|
+
|
59
|
+
uri = URI.parse(api_url)
|
60
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
61
|
+
http.use_ssl = (uri.scheme == "https")
|
62
|
+
|
63
|
+
# Use HEAD request to check existence
|
64
|
+
request = Net::HTTP::Head.new(uri.path.empty? ? "/" : uri.path)
|
65
|
+
|
66
|
+
# Add headers if present in options
|
67
|
+
if options["headers"].is_a?(Hash)
|
68
|
+
options["headers"].each do |key, value|
|
69
|
+
# Process any environment variables in header values
|
70
|
+
processed_value = process_env_vars(value.to_s)
|
71
|
+
request[key] = processed_value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Make the request
|
76
|
+
response = http.request(request)
|
77
|
+
|
78
|
+
# Consider 2xx and 3xx as success
|
79
|
+
response.code.to_i < 400
|
80
|
+
rescue StandardError => e
|
81
|
+
# Log the error but don't crash
|
82
|
+
Roast::Helpers::Logger.error("Error checking API existence: #{e.message}")
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def contents
|
87
|
+
return unless target
|
88
|
+
|
89
|
+
# If it's a configuration, return a prepared request object
|
90
|
+
if config
|
91
|
+
JSON.pretty_generate({
|
92
|
+
"url" => api_url,
|
93
|
+
"method" => (options["method"] || "GET").upcase,
|
94
|
+
"headers" => options["headers"] || {},
|
95
|
+
"body" => options["body"],
|
96
|
+
})
|
97
|
+
else
|
98
|
+
# Assume it's a direct API URL, do a simple GET
|
99
|
+
begin
|
100
|
+
uri = URI.parse(target)
|
101
|
+
Net::HTTP.get(uri)
|
102
|
+
rescue StandardError => e
|
103
|
+
# Log the error but don't crash
|
104
|
+
Roast::Helpers::Logger.error("Error fetching API contents: #{e.message}")
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def name
|
111
|
+
if target
|
112
|
+
if config && config["url"]
|
113
|
+
"API #{config["url"]} (#{(options["method"] || "GET").upcase})"
|
114
|
+
else
|
115
|
+
"API #{target}"
|
116
|
+
end
|
117
|
+
else
|
118
|
+
"Unnamed API"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def type
|
123
|
+
:api
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Replace environment variables in the format ${VAR_NAME}
|
129
|
+
def process_env_vars(text)
|
130
|
+
text.gsub(/\${([^}]+)}/) do |_match|
|
131
|
+
var_name = ::Regexp.last_match(1).strip
|
132
|
+
ENV.fetch(var_name, "${#{var_name}}")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Base class for all resource types
|
6
|
+
# Follows the Strategy pattern to handle different resource types in a polymorphic way
|
7
|
+
class BaseResource
|
8
|
+
attr_reader :target
|
9
|
+
|
10
|
+
# Initialize a resource with a target
|
11
|
+
# @param target [String] The target specified in the workflow, can be nil
|
12
|
+
def initialize(target)
|
13
|
+
@target = target
|
14
|
+
end
|
15
|
+
|
16
|
+
# Process the resource to prepare it for use
|
17
|
+
# @return [String] The processed target
|
18
|
+
def process
|
19
|
+
target
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if the resource exists
|
23
|
+
# @return [Boolean] true if the resource exists
|
24
|
+
def exists?
|
25
|
+
false # Override in subclasses
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the contents of the resource
|
29
|
+
# @return [String] The contents of the resource
|
30
|
+
def contents
|
31
|
+
nil # Override in subclasses
|
32
|
+
end
|
33
|
+
|
34
|
+
# Get a name for the resource to display in logs
|
35
|
+
# @return [String] A descriptive name for the resource
|
36
|
+
def name
|
37
|
+
target || "Unnamed Resource"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the type of this resource as a symbol
|
41
|
+
# @return [Symbol] The resource type
|
42
|
+
def type
|
43
|
+
:unknown
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for directories
|
6
|
+
class DirectoryResource < BaseResource
|
7
|
+
def process
|
8
|
+
if target.include?("*") || target.include?("?")
|
9
|
+
# If it's a glob pattern, return only directories
|
10
|
+
Dir.glob(target).select { |f| Dir.exist?(f) }.map { |d| File.expand_path(d) }.join("\n")
|
11
|
+
else
|
12
|
+
File.expand_path(target)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
Dir.exist?(target)
|
18
|
+
end
|
19
|
+
|
20
|
+
def contents
|
21
|
+
# Return a listing of files in the directory
|
22
|
+
if exists?
|
23
|
+
Dir.entries(target).reject { |f| f == "." || f == ".." }.join("\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
if target
|
29
|
+
File.basename(target) + "/"
|
30
|
+
else
|
31
|
+
"Unnamed Directory"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def type
|
36
|
+
:directory
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for files
|
6
|
+
class FileResource < BaseResource
|
7
|
+
def process
|
8
|
+
# Handle glob patterns in the target
|
9
|
+
if target.include?("*") || target.include?("?")
|
10
|
+
Dir.glob(target).map { |f| File.expand_path(f) unless Dir.exist?(f) }.compact.join("\n")
|
11
|
+
else
|
12
|
+
File.expand_path(target)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def exists?
|
17
|
+
File.exist?(target) && !Dir.exist?(target)
|
18
|
+
end
|
19
|
+
|
20
|
+
def contents
|
21
|
+
File.read(target) if exists?
|
22
|
+
end
|
23
|
+
|
24
|
+
def name
|
25
|
+
File.basename(target) if target
|
26
|
+
end
|
27
|
+
|
28
|
+
def type
|
29
|
+
:file
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Roast
|
4
|
+
module Resources
|
5
|
+
# Resource implementation for workflows with no target (targetless workflows)
|
6
|
+
class NoneResource < BaseResource
|
7
|
+
def process
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def exists?
|
12
|
+
# There's no target to check, so this is always true
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def contents
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def name
|
21
|
+
"Targetless Resource"
|
22
|
+
end
|
23
|
+
|
24
|
+
def type
|
25
|
+
:none
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Roast
|
7
|
+
module Resources
|
8
|
+
# Resource implementation for URLs
|
9
|
+
class UrlResource < BaseResource
|
10
|
+
def process
|
11
|
+
# URLs don't need special processing, just return as is
|
12
|
+
target
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?
|
16
|
+
return false unless target
|
17
|
+
|
18
|
+
begin
|
19
|
+
uri = URI.parse(target)
|
20
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
21
|
+
http.use_ssl = (uri.scheme == "https")
|
22
|
+
|
23
|
+
# Just check the head to see if the resource exists
|
24
|
+
response = http.request_head(uri.path.empty? ? "/" : uri.path)
|
25
|
+
|
26
|
+
# Consider 2xx and 3xx as success
|
27
|
+
response.code.to_i < 400
|
28
|
+
rescue StandardError => e
|
29
|
+
# Log the error but don't crash
|
30
|
+
Roast::Helpers::Logger.error("Error checking URL existence: #{e.message}")
|
31
|
+
false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def contents
|
36
|
+
return unless target
|
37
|
+
|
38
|
+
begin
|
39
|
+
uri = URI.parse(target)
|
40
|
+
Net::HTTP.get(uri)
|
41
|
+
rescue StandardError => e
|
42
|
+
# Log the error but don't crash
|
43
|
+
Roast::Helpers::Logger.error("Error fetching URL contents: #{e.message}")
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def name
|
49
|
+
if target
|
50
|
+
URI.parse(target).host
|
51
|
+
else
|
52
|
+
"Unnamed URL"
|
53
|
+
end
|
54
|
+
rescue URI::InvalidURIError
|
55
|
+
"Invalid URL"
|
56
|
+
end
|
57
|
+
|
58
|
+
def type
|
59
|
+
:url
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|