Skip to content

Commit 1f64cf8

Browse files
authored
Improved code and documentation coverage. (#26)
1 parent 220222f commit 1f64cf8

File tree

26 files changed

+815
-235
lines changed

26 files changed

+815
-235
lines changed

fixtures/disable_console_context.rb

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2022-2025, by Samuel Williams.
5+
6+
require "sus/fixtures/async/http/server_context"
7+
8+
module Protocol
9+
module Rack
10+
module ServerContext
11+
include Sus::Fixtures::Async::HTTP::ServerContext
12+
13+
def app
14+
->(env){[200, {}, ["Hello World!"]]}
15+
end
16+
17+
def middleware
18+
Protocol::Rack::Adapter.new(app)
19+
end
20+
end
21+
end
22+
end

fixtures/server_context.rb

Lines changed: 0 additions & 18 deletions
This file was deleted.

gems.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
gem "sus-fixtures-async"
2525
gem "sus-fixtures-async-http"
26+
gem "sus-fixtures-console"
2627

2728
gem "bake-test"
2829
gem "bake-test-external"

gems/gems.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2023, by Samuel Williams.
4+
# Copyright, 2023-2025, by Samuel Williams.
55

66
source "https://rubygems.org"
77

@@ -10,4 +10,5 @@
1010
gem "bake-test-external"
1111
gem "sus"
1212
gem "sus-fixtures-async-http"
13+
gem "sus-fixtures-console"
1314
gem "covered"

lib/protocol/rack.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2022-2024, by Samuel Williams.
4+
# Copyright, 2022-2025, by Samuel Williams.
55

66
require_relative "rack/version"
77
require_relative "rack/adapter"
88
require_relative "rack/request"
99
require_relative "rack/response"
10+
11+
# @namespace
12+
module Protocol
13+
# @namespace
14+
module Rack
15+
end
16+
end

lib/protocol/rack/adapter.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2022-2024, by Samuel Williams.
4+
# Copyright, 2022-2025, by Samuel Williams.
55

66
require "rack"
77

88
module Protocol
99
module Rack
10+
# The Rack adapter provides a bridge between Protocol::HTTP and Rack applications.
11+
# It automatically selects the appropriate implementation based on the installed Rack version.
12+
#
13+
# ```ruby
14+
# app = ->(env) { [200, {"content-type" => "text/plain"}, ["Hello World"]] }
15+
# adapter = Protocol::Rack::Adapter.new(app)
16+
# response = adapter.call(request)
17+
# ```
1018
module Adapter
19+
# The version of Rack being used. Can be overridden using the PROTOCOL_RACK_ADAPTER_VERSION environment variable.
1120
VERSION = ENV.fetch("PROTOCOL_RACK_ADAPTER_VERSION", ::Rack.release)
1221

1322
if VERSION >= "3.1"
@@ -21,14 +30,27 @@ module Adapter
2130
IMPLEMENTATION = Rack2
2231
end
2332

33+
# Creates a new adapter instance for the given Rack application.
34+
#
35+
# @parameter app [Interface(:call)] A Rack application that responds to #call
36+
# @returns [Protocol::HTTP::Middleware] An adapter that can handle HTTP requests
2437
def self.new(app)
2538
IMPLEMENTATION.wrap(app)
2639
end
2740

41+
# Converts a Rack response into a Protocol::HTTP response.
42+
#
43+
# @parameter env [Hash] The Rack environment
44+
# @parameter response [Array] The Rack response [status, headers, body]
45+
# @returns [Protocol::HTTP::Response] A Protocol::HTTP response
2846
def self.make_response(env, response)
2947
IMPLEMENTATION.make_response(env, response)
3048
end
3149

50+
# Parses a file path from the Rack environment.
51+
#
52+
# @parameter env [Hash] The Rack environment
53+
# @returns [String | Nil] The parsed file path or nil if not found
3254
def self.parse_file(...)
3355
IMPLEMENTATION.parse_file(...)
3456
end

lib/protocol/rack/adapter/generic.rb

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
# frozen_string_literal: true
22

33
# Released under the MIT License.
4-
# Copyright, 2022-2024, by Samuel Williams.
4+
# Copyright, 2022-2025, by Samuel Williams.
55

66
require "console"
77

88
require_relative "../constants"
99
require_relative "../input"
1010
require_relative "../response"
11+
require_relative "../rewindable"
1112

1213
module Protocol
1314
module Rack
1415
module Adapter
16+
# The base adapter class that provides common functionality for all Rack adapters.
17+
# It handles the conversion between {Protocol::HTTP} and Rack environments.
1518
class Generic
19+
# Creates a new adapter instance for the given Rack application.
20+
# Wraps the adapter in a {Rewindable} instance to ensure request body can be read multiple times, which is required for Rack < 3.
21+
#
22+
# @parameter app [Interface(:call)] A Rack application.
23+
# @returns [Rewindable] A rewindable adapter instance.
1624
def self.wrap(app)
17-
self.new(app)
25+
Rewindable.new(self.new(app))
1826
end
1927

28+
# Parses a Rackup file and returns the application.
29+
#
30+
# @parameter path [String] The path to the Rackup file.
31+
# @returns [Interface(:call)] The Rack application.
2032
def self.parse_file(...)
2133
# This is the old interface, which was changed in Rack 3.
2234
::Rack::Builder.parse_file(...).first
2335
end
2436

2537
# Initialize the rack adaptor middleware.
26-
# @parameter app [Object] The rack middleware.
38+
#
39+
# @parameter app [Interface(:call)] The rack middleware.
40+
# @raises [ArgumentError] If the app does not respond to `call`.
2741
def initialize(app)
2842
@app = app
2943

3044
raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
3145
end
3246

47+
# The logger to use for this adapter.
48+
#
49+
# @returns [Console] The console logger.
3350
def logger
3451
Console
3552
end
@@ -108,6 +125,10 @@ def unwrap_request(request, env)
108125
end
109126
end
110127

128+
# Create a base environment hash for the request.
129+
#
130+
# @parameter request [Protocol::HTTP::Request] The incoming request.
131+
# @returns [Hash] The base environment hash.
111132
def make_environment(request)
112133
{
113134
request: request
@@ -117,6 +138,8 @@ def make_environment(request)
117138
# Build a rack `env` from the incoming request and apply it to the rack middleware.
118139
#
119140
# @parameter request [Protocol::HTTP::Request] The incoming request.
141+
# @returns [Protocol::HTTP::Response] The HTTP response.
142+
# @raises [ArgumentError] If the status is not an integer or headers are nil.
120143
def call(request)
121144
env = self.make_environment(request)
122145

@@ -148,12 +171,18 @@ def call(request)
148171
end
149172

150173
# Generate a suitable response for the given exception.
151-
# @parameter exception [Exception]
152-
# @returns [Protocol::HTTP::Response]
174+
#
175+
# @parameter exception [Exception] The exception that occurred.
176+
# @returns [Protocol::HTTP::Response] A response representing the error.
153177
def failure_response(exception)
154178
Protocol::HTTP::Response.for_exception(exception)
155179
end
156180

181+
# Extract protocol information from the environment and response.
182+
#
183+
# @parameter env [Hash] The rack environment.
184+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
185+
# @parameter headers [Hash] The response headers to modify.
157186
def self.extract_protocol(env, response, headers)
158187
if protocol = response.protocol
159188
# This is the newer mechanism for protocol upgrade:

lib/protocol/rack/adapter/rack2.rb

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,23 @@
1212
module Protocol
1313
module Rack
1414
module Adapter
15+
# The Rack 2 adapter provides compatibility with Rack 2.x applications.
16+
# It handles the conversion between {Protocol::HTTP} and Rack 2 environments.
1517
class Rack2 < Generic
18+
# The Rack version constant.
1619
RACK_VERSION = "rack.version"
20+
# Whether the application is multithreaded.
1721
RACK_MULTITHREAD = "rack.multithread"
22+
# Whether the application is multiprocess.
1823
RACK_MULTIPROCESS = "rack.multiprocess"
24+
# Whether the application should run only once.
1925
RACK_RUN_ONCE = "rack.run_once"
2026

21-
def self.wrap(app)
22-
Rewindable.new(self.new(app))
23-
end
24-
27+
# Create a Rack 2 environment hash for the request.
28+
# Sets up all required Rack 2 environment variables and processes the request.
29+
#
30+
# @parameter request [Protocol::HTTP::Request] The incoming request.
31+
# @returns [Hash] The Rack 2 environment hash.
2532
def make_environment(request)
2633
request_path, query_string = request.path.split("?", 2)
2734
server_name, server_port = (request.authority || "").split(":", 2)
@@ -38,13 +45,13 @@ def make_environment(request)
3845
RACK_ERRORS => $stderr,
3946
RACK_LOGGER => self.logger,
4047

41-
# The HTTP request method, such as GET or POST. This cannot ever be an empty string, and so is always required.
48+
# The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required.
4249
CGI::REQUEST_METHOD => request.method,
4350

44-
# The initial portion of the request URL's path that corresponds to the application object, so that the application knows its virtual location. This may be an empty string, if the application corresponds to the root of the server.
51+
# The initial portion of the request URL's "path" that corresponds to the application object, so that the application knows its virtual "location". This may be an empty string, if the application corresponds to the "root" of the server.
4552
CGI::SCRIPT_NAME => "",
4653

47-
# The remainder of the request URL's path, designating the virtual location of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
54+
# The remainder of the request URL's "path", designating the virtual "location" of the request's target within the application. This may be an empty string, if the request URL targets the application root and does not have a trailing slash. This value may be percent-encoded when originating from a URL.
4855
CGI::PATH_INFO => request_path,
4956
CGI::REQUEST_PATH => request_path,
5057
CGI::REQUEST_URI => request.path,
@@ -75,11 +82,27 @@ def make_environment(request)
7582
# Build a rack `env` from the incoming request and apply it to the rack middleware.
7683
#
7784
# @parameter request [Protocol::HTTP::Request] The incoming request.
85+
# @returns [Protocol::HTTP::Response] The HTTP response.
86+
# @raises [ArgumentError] If the status is not an integer or headers are nil.
7887
def call(request)
7988
env = self.make_environment(request)
8089

8190
status, headers, body = @app.call(env)
8291

92+
if status
93+
status = status.to_i
94+
else
95+
raise ArgumentError, "Status must be an integer!"
96+
end
97+
98+
unless headers
99+
raise ArgumentError, "Headers must not be nil!"
100+
end
101+
102+
# unless body.respond_to?(:each)
103+
# raise ArgumentError, "Body must respond to #each!"
104+
# end
105+
83106
headers, meta = self.wrap_headers(headers)
84107

85108
# Rack 2 spec does not allow only partial hijacking.
@@ -96,8 +119,11 @@ def call(request)
96119
return failure_response(exception)
97120
end
98121

99-
# Process the rack response headers into into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
100-
# @returns [Tuple(Protocol::HTTP::Headers, Hash)]
122+
# Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
123+
# Headers with newline-separated values are split into multiple headers.
124+
#
125+
# @parameter fields [Hash] The raw response headers.
126+
# @returns [Tuple(Protocol::HTTP::Headers, Hash)] The processed headers and metadata.
101127
def wrap_headers(fields)
102128
headers = ::Protocol::HTTP::Headers.new
103129
meta = {}
@@ -119,6 +145,12 @@ def wrap_headers(fields)
119145
return headers, meta
120146
end
121147

148+
# Convert a {Protocol::HTTP::Response} into a Rack 2 response tuple.
149+
# Handles protocol upgrades and streaming responses.
150+
#
151+
# @parameter env [Hash] The rack environment.
152+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
153+
# @returns [Tuple(Integer, Hash, Object)] The Rack 2 response tuple [status, headers, body].
122154
def self.make_response(env, response)
123155
# These interfaces should be largely compatible:
124156
headers = response.headers.to_h

0 commit comments

Comments
 (0)