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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/cla.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +28 -0
  5. data/CLAUDE.md +3 -1
  6. data/Gemfile +0 -1
  7. data/Gemfile.lock +3 -4
  8. data/README.md +419 -5
  9. data/Rakefile +1 -6
  10. data/docs/INSTRUMENTATION.md +202 -0
  11. data/examples/api_workflow/README.md +85 -0
  12. data/examples/api_workflow/fetch_api_data/prompt.md +10 -0
  13. data/examples/api_workflow/generate_report/prompt.md +10 -0
  14. data/examples/api_workflow/prompt.md +10 -0
  15. data/examples/api_workflow/transform_data/prompt.md +10 -0
  16. data/examples/api_workflow/workflow.yml +30 -0
  17. data/examples/grading/format_result.rb +25 -9
  18. data/examples/grading/js_test_runner +31 -0
  19. data/examples/grading/rb_test_runner +19 -0
  20. data/examples/grading/read_dependencies/prompt.md +14 -0
  21. data/examples/grading/run_coverage.rb +2 -2
  22. data/examples/grading/workflow.yml +3 -12
  23. data/examples/instrumentation.rb +76 -0
  24. data/examples/rspec_to_minitest/README.md +68 -0
  25. data/examples/rspec_to_minitest/analyze_spec/prompt.md +30 -0
  26. data/examples/rspec_to_minitest/create_minitest/prompt.md +33 -0
  27. data/examples/rspec_to_minitest/run_and_improve/prompt.md +35 -0
  28. data/examples/rspec_to_minitest/workflow.md +10 -0
  29. data/examples/rspec_to_minitest/workflow.yml +40 -0
  30. data/lib/roast/helpers/function_caching_interceptor.rb +72 -8
  31. data/lib/roast/helpers/prompt_loader.rb +2 -0
  32. data/lib/roast/resources/api_resource.rb +137 -0
  33. data/lib/roast/resources/base_resource.rb +47 -0
  34. data/lib/roast/resources/directory_resource.rb +40 -0
  35. data/lib/roast/resources/file_resource.rb +33 -0
  36. data/lib/roast/resources/none_resource.rb +29 -0
  37. data/lib/roast/resources/url_resource.rb +63 -0
  38. data/lib/roast/resources.rb +100 -0
  39. data/lib/roast/tools/coding_agent.rb +69 -0
  40. data/lib/roast/tools.rb +1 -0
  41. data/lib/roast/version.rb +1 -1
  42. data/lib/roast/workflow/base_step.rb +21 -17
  43. data/lib/roast/workflow/base_workflow.rb +69 -17
  44. data/lib/roast/workflow/configuration.rb +83 -8
  45. data/lib/roast/workflow/configuration_parser.rb +218 -3
  46. data/lib/roast/workflow/file_state_repository.rb +156 -0
  47. data/lib/roast/workflow/prompt_step.rb +16 -0
  48. data/lib/roast/workflow/session_manager.rb +82 -0
  49. data/lib/roast/workflow/state_repository.rb +21 -0
  50. data/lib/roast/workflow/workflow_executor.rb +99 -9
  51. data/lib/roast/workflow.rb +4 -0
  52. data/lib/roast.rb +2 -5
  53. data/roast.gemspec +1 -1
  54. data/schema/workflow.json +12 -0
  55. metadata +34 -6
  56. 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
- # legacy workflows don't have a configuration
14
- return super(function_name, params) if configuration.blank?
15
+ start_time = Time.now
15
16
 
16
- function_config = configuration.function_config(function_name)
17
- if function_config&.dig("cache", "enabled")
18
- # Call the original function and pass in the cache
19
- super(function_name, params, cache: Roast::Tools::CACHE)
20
- else
21
- Roast::Helpers::Logger.debug("⚠️ Caching not enabled for #{function_name}")
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
@@ -74,6 +74,8 @@ module Roast
74
74
  end
75
75
 
76
76
  def extract_file_extensions
77
+ return [] if target_file.nil?
78
+
77
79
  file_basename = File.basename(target_file)
78
80
 
79
81
  if file_basename.end_with?(".md") && file_basename.count(".") > 1
@@ -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