async-http-capture 0.1.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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "protocol/http/middleware"
7
+ require "protocol/http/body/rewindable"
8
+ require "protocol/http/body/completable"
9
+
10
+ require_relative "interaction_tracker"
11
+
12
+ module Async
13
+ module HTTP
14
+ module Capture
15
+ # Protocol::HTTP::Middleware for recording complete HTTP interactions.
16
+ #
17
+ # This middleware captures both HTTP requests and responses, waiting for both
18
+ # to be fully processed before recording the complete interaction.
19
+ class Middleware < Protocol::HTTP::Middleware
20
+ # Initialize the recording middleware.
21
+ # @parameter app [Protocol::HTTP::Middleware] The next middleware in the chain.
22
+ # @parameter store [Object] An object that responds to #call(interaction) to handle recorded interactions.
23
+ def initialize(app, store:)
24
+ super(app)
25
+ @store = store
26
+ end
27
+
28
+ # Process an HTTP request, capturing both request and response.
29
+ # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
30
+ # @returns [Protocol::HTTP::Response] The response from the next middleware.
31
+ def call(request)
32
+ # Check if we should capture this request:
33
+ unless capture?(request)
34
+ return super(request)
35
+ end
36
+
37
+ # Create completion tracker for this interaction:
38
+ tracker = create_interaction_tracker(request)
39
+
40
+ # Capture request body with completion tracking:
41
+ captured_request = capture_request_with_completion(request, tracker)
42
+
43
+ # Get response from downstream middleware/app:
44
+ response = super(captured_request)
45
+
46
+ # Capture response body with completion tracking:
47
+ capture_response_with_completion(captured_request, response, tracker)
48
+ end
49
+
50
+ # Determine whether to capture the given request.
51
+ # Override this method in subclasses to implement custom filtering logic.
52
+ # @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
53
+ # @returns [Boolean] True if the request should be captured, false otherwise.
54
+ def capture?(request)
55
+ true
56
+ end
57
+
58
+ private
59
+
60
+ # Create a completion tracker for a single interaction.
61
+ # @parameter request [Protocol::HTTP::Request] The original request.
62
+ # @returns [InteractionTracker] A new tracker instance.
63
+ def create_interaction_tracker(request)
64
+ InteractionTracker.new(@store, request)
65
+ end
66
+
67
+ # Capture the request body with completion tracking.
68
+ # @parameter request [Protocol::HTTP::Request] The original request.
69
+ # @parameter tracker [InteractionTracker] The completion tracker.
70
+ # @returns [Protocol::HTTP::Request] A request with rewindable body.
71
+ def capture_request_with_completion(request, tracker)
72
+ return tracker.mark_request_ready(request) unless request.body && !request.body.empty?
73
+
74
+ # Make the request body rewindable:
75
+ rewindable_body = ::Protocol::HTTP::Body::Rewindable.wrap(request)
76
+
77
+ # Wrap with completion callback:
78
+ ::Protocol::HTTP::Body::Completable.wrap(request) do |error|
79
+ if error
80
+ tracker.request_completed(error: error)
81
+ else
82
+ tracker.request_completed(body: rewindable_body.buffered)
83
+ end
84
+ end
85
+
86
+ return request
87
+ end
88
+
89
+ # Capture the response body with completion tracking.
90
+ # @parameter request [Protocol::HTTP::Request] The captured request.
91
+ # @parameter response [Protocol::HTTP::Response] The response to capture.
92
+ # @parameter tracker [InteractionTracker] The completion tracker.
93
+ # @returns [Protocol::HTTP::Response] The wrapped response.
94
+ def capture_response_with_completion(request, response, tracker)
95
+ # Set the response on the tracker:
96
+ tracker.set_response(response)
97
+
98
+ return tracker.mark_response_ready(response) unless response.body && !response.body.empty?
99
+
100
+ # Make the response body rewindable:
101
+ rewindable_body = ::Protocol::HTTP::Body::Rewindable.wrap(response)
102
+
103
+ # Wrap with completion callback:
104
+ ::Protocol::HTTP::Body::Completable.wrap(response) do |error|
105
+ if error
106
+ tracker.response_completed(error: error)
107
+ else
108
+ tracker.response_completed(body: rewindable_body.buffered)
109
+ end
110
+ end
111
+
112
+ return response
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module HTTP
8
+ module Capture
9
+ VERSION = "0.1.0"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "capture/version"
7
+ require_relative "capture/interaction"
8
+ require_relative "capture/cassette"
9
+ require_relative "capture/cassette_store"
10
+ require_relative "capture/console_store"
11
+ require_relative "capture/interaction_tracker"
12
+ require_relative "capture/middleware"
13
+
14
+ # @namespace
15
+ module Async
16
+ # @namespace
17
+ module HTTP
18
+ # @namespace
19
+ module Capture
20
+ end
21
+ end
22
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,133 @@
1
+ # Async::HTTP::Capture
2
+
3
+ A Ruby gem for recording and replaying HTTP requests using `Protocol::HTTP`. Features content-addressed storage, parallel-safe recording, and flexible store backends.
4
+
5
+ [![Development Status](https://github.com/socketry/async-http-capture/workflows/Test/badge.svg)](https://github.com/socketry/async-http-capture/actions?workflow=Test)
6
+
7
+ ## Features
8
+
9
+ - **Pure Protocol::HTTP**: Works directly with Protocol::HTTP objects, no lossy conversions
10
+ - **Content-Addressed Storage**: Each interaction saved as separate JSON file with content hash
11
+ - **Parallel-Safe**: Multiple processes can record simultaneously without conflicts
12
+ - **Flexible Stores**: Pluggable storage backends (files, console logging, etc.)
13
+ - **Complete Headers**: Full round-trip serialization including `fields` and `tail`
14
+ - **Error Handling**: Captures network errors and connection issues
15
+
16
+ ## Usage
17
+
18
+ Please see the [project documentation](https://socketry.github.io/async-http-capture/) for more details.
19
+
20
+ - [Getting Started](https://socketry.github.io/async-http-capture/guides/getting-started/index) - This guide explains how to get started with `async-http-capture`, a Ruby gem for recording and replaying HTTP requests using Protocol::HTTP.
21
+
22
+ ### Basic Recording to Files
23
+
24
+ ``` ruby
25
+ require "async/http/capture"
26
+
27
+ # Create a store that saves to content-addressed files:
28
+ store = Async::HTTP::Capture::CassetteStore.new("interactions")
29
+
30
+ # Create middleware:
31
+ app = ->(request) { Protocol::HTTP::Response[200, {}, ["OK"]] }
32
+ middleware = Async::HTTP::Capture::Middleware.new(app, store: store)
33
+
34
+ # Record interactions:
35
+ request = Protocol::HTTP::Request["GET", "/users"]
36
+ response = middleware.call(request)
37
+ ```
38
+
39
+ ### Recording to Console Log
40
+
41
+ ``` ruby
42
+ # Create a console store for debugging:
43
+ console_store = Async::HTTP::Capture::ConsoleStore.new
44
+ middleware = Async::HTTP::Capture::Middleware.new(app, store: console_store)
45
+
46
+ # This will log interactions to console:
47
+ middleware.call(request)
48
+ # Output: "Recorded: GET /users"
49
+ ```
50
+
51
+ ### Loading and Replaying
52
+
53
+ ``` ruby
54
+ # Load recorded interactions:
55
+ cassette = Async::HTTP::Capture::Cassette.load("interactions")
56
+
57
+ # Replay them:
58
+ cassette.each do |interaction|
59
+ request = interaction.request # Lazy construction
60
+ response = app.call(request) # Send to your app
61
+ end
62
+ ```
63
+
64
+ ### Recording with Responses
65
+
66
+ ``` ruby
67
+ # Record both requests and responses:
68
+ middleware = Async::HTTP::Capture::Middleware.new(
69
+ app,
70
+ store: store,
71
+ record_response: true
72
+ )
73
+
74
+ response = middleware.call(request)
75
+ # Both request and response are now recorded
76
+ ```
77
+
78
+ ## Architecture
79
+
80
+ Middleware -> Store.call(interaction) -> [CassetteStore | ConsoleStore | ...]
81
+
82
+ - **Middleware**: Pure capture logic, creates Interaction objects with Protocol::HTTP data
83
+ - **Store Interface**: Generic `call(interaction)` method for pluggable backends
84
+ - **Stores**: Handle serialization, filtering, persistence, or logging
85
+ - **Interaction**: Simple data container with lazy Protocol::HTTP object construction
86
+
87
+ ## Content-Addressed Storage
88
+
89
+ Each interaction is saved to a file named by its content hash:
90
+
91
+ interactions/
92
+ ├── a1b2c3d4e5f67890.json # GET /users
93
+ ├── f67890a1b2c3d4e5.json # POST /orders
94
+ └── 1234567890abcdef.json # GET /health
95
+
96
+ Benefits:
97
+
98
+ - **Automatic de-duplication**: Identical interactions → same filename
99
+ - **Parallel-safe**: Multiple processes can write without conflicts
100
+ - **Content integrity**: Hash verifies file contents
101
+ - **Git-friendly**: Stable filenames for version control
102
+
103
+ ## Store Implementations
104
+
105
+ ### CassetteStore
106
+
107
+ Saves interactions to content-addressed JSON files in a directory.
108
+
109
+ ### ConsoleStore
110
+
111
+ Logs interactions via the Console gem with different levels based on success/failure.
112
+
113
+ ### Custom Stores
114
+
115
+ Implement the `Store` interface:
116
+
117
+ ``` ruby
118
+ class MyStore
119
+ include Async::HTTP::Capture::Store
120
+
121
+ def call(interaction)
122
+ # Handle the interaction as needed
123
+ end
124
+ end
125
+ ```
126
+
127
+ ## Testing
128
+
129
+ ``` bash
130
+ bundle exec sus
131
+ ```
132
+
133
+ The gem includes comprehensive tests using the Sus testing framework with 41 tests and 101 assertions.
data/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
4
+
5
+ - Initial implementation.
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-http-capture
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: async-http
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.90'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.90'
26
+ executables: []
27
+ extensions: []
28
+ extra_rdoc_files: []
29
+ files:
30
+ - context/getting-started.md
31
+ - context/index.yaml
32
+ - design.md
33
+ - lib/async/http/capture.rb
34
+ - lib/async/http/capture/cassette.rb
35
+ - lib/async/http/capture/cassette_store.rb
36
+ - lib/async/http/capture/console_store.rb
37
+ - lib/async/http/capture/interaction.rb
38
+ - lib/async/http/capture/interaction_tracker.rb
39
+ - lib/async/http/capture/middleware.rb
40
+ - lib/async/http/capture/version.rb
41
+ - license.md
42
+ - readme.md
43
+ - releases.md
44
+ homepage: https://github.com/socketry/async-http-capture
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ documentation_uri: https://socketry.github.io/async-http-capture/
49
+ source_code_uri: https://github.com/socketry/async-http-capture.git
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '3.2'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.6.9
65
+ specification_version: 4
66
+ summary: A HTTP request and response capture.
67
+ test_files: []