Skip to content

Commit 5a76e00

Browse files
committed
100% documentation coverage.
1 parent 1c7a877 commit 5a76e00

File tree

11 files changed

+273
-50
lines changed

11 files changed

+273
-50
lines changed

lib/protocol/rack.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,10 @@
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,16 @@
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: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,40 @@
1212
module Protocol
1313
module Rack
1414
module Adapter
15+
# The base adapter class that provides common functionality for all Rack adapters.
16+
# It handles the conversion between {Protocol::HTTP} and Rack environments.
1517
class Generic
18+
# Creates a new adapter instance for the given Rack application.
19+
# Wraps the adapter in a {Rewindable} instance to ensure request body can be read multiple times, which is required for Rack < 3.
20+
#
21+
# @parameter app [Interface(:call)] A Rack application.
22+
# @returns [Rewindable] A rewindable adapter instance.
1623
def self.wrap(app)
17-
self.new(app)
24+
Rewindable.new(self.new(app))
1825
end
1926

27+
# Parses a Rackup file and returns the application.
28+
#
29+
# @parameter path [String] The path to the Rackup file.
30+
# @returns [Interface(:call)] The Rack application.
2031
def self.parse_file(...)
2132
# This is the old interface, which was changed in Rack 3.
2233
::Rack::Builder.parse_file(...).first
2334
end
2435

25-
def self.streaming?
26-
false
27-
end
28-
2936
# Initialize the rack adaptor middleware.
30-
# @parameter app [Object] The rack middleware.
37+
#
38+
# @parameter app [Interface(:call)] The rack middleware.
39+
# @raises [ArgumentError] If the app does not respond to `call`.
3140
def initialize(app)
3241
@app = app
3342

3443
raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
3544
end
3645

46+
# The logger to use for this adapter.
47+
#
48+
# @returns [Console] The console logger.
3749
def logger
3850
Console
3951
end
@@ -112,6 +124,10 @@ def unwrap_request(request, env)
112124
end
113125
end
114126

127+
# Create a base environment hash for the request.
128+
#
129+
# @parameter request [Protocol::HTTP::Request] The incoming request.
130+
# @returns [Hash] The base environment hash.
115131
def make_environment(request)
116132
{
117133
request: request
@@ -121,6 +137,8 @@ def make_environment(request)
121137
# Build a rack `env` from the incoming request and apply it to the rack middleware.
122138
#
123139
# @parameter request [Protocol::HTTP::Request] The incoming request.
140+
# @returns [Protocol::HTTP::Response] The HTTP response.
141+
# @raises [ArgumentError] If the status is not an integer or headers are nil.
124142
def call(request)
125143
env = self.make_environment(request)
126144

@@ -152,12 +170,18 @@ def call(request)
152170
end
153171

154172
# Generate a suitable response for the given exception.
155-
# @parameter exception [Exception]
156-
# @returns [Protocol::HTTP::Response]
173+
#
174+
# @parameter exception [Exception] The exception that occurred.
175+
# @returns [Protocol::HTTP::Response] A response representing the error.
157176
def failure_response(exception)
158177
Protocol::HTTP::Response.for_exception(exception)
159178
end
160179

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

lib/protocol/rack/adapter/rack2.rb

Lines changed: 27 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,6 +82,8 @@ 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

@@ -110,8 +119,11 @@ def call(request)
110119
return failure_response(exception)
111120
end
112121

113-
# Process the rack response headers into into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
114-
# @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.
115127
def wrap_headers(fields)
116128
headers = ::Protocol::HTTP::Headers.new
117129
meta = {}
@@ -133,6 +145,12 @@ def wrap_headers(fields)
133145
return headers, meta
134146
end
135147

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].
136154
def self.make_response(env, response)
137155
# These interfaces should be largely compatible:
138156
headers = response.headers.to_h

lib/protocol/rack/adapter/rack3.rb

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,42 @@
1111
module Protocol
1212
module Rack
1313
module Adapter
14+
# The Rack 3 adapter provides compatibility with Rack 3.x applications.
15+
# It handles the conversion between {Protocol::HTTP} and Rack 3 environments.
16+
# Unlike Rack 2, this adapter supports streaming responses and has a simpler environment setup.
1417
class Rack3 < Generic
18+
# Creates a new adapter instance for the given Rack application.
19+
# Unlike Rack 2, this adapter doesn't require a {Rewindable} wrapper.
20+
#
21+
# @parameter app [Interface(:call)] A Rack application.
22+
# @returns [Rack3] A new adapter instance.
1523
def self.wrap(app)
1624
self.new(app)
1725
end
1826

27+
# Parses a Rackup file and returns the application.
28+
# Uses the Rack 3.x interface for parsing Rackup files.
29+
#
30+
# @parameter path [String] The path to the Rackup file.
31+
# @returns [Interface(:call)] The Rack application.
1932
def self.parse_file(...)
2033
::Rack::Builder.parse_file(...)
2134
end
2235

36+
# Whether this adapter supports streaming responses.
37+
# Rack 3 supports streaming responses by default.
38+
#
39+
# @returns [Boolean] Always true for the Rack 3 adapter.
2340
def self.streaming?
2441
true
2542
end
2643

44+
# Create a Rack 3 environment hash for the request.
45+
# Sets up all required Rack 3 environment variables and processes the request.
46+
# Unlike Rack 2, this adapter doesn't set Rack version or threading flags.
47+
#
48+
# @parameter request [Protocol::HTTP::Request] The incoming request.
49+
# @returns [Hash] The Rack 3 environment hash.
2750
def make_environment(request)
2851
request_path, query_string = request.path.split("?", 2)
2952
server_name, server_port = (request.authority || "").split(":", 2)
@@ -38,13 +61,13 @@ def make_environment(request)
3861
# The response finished callbacks:
3962
RACK_RESPONSE_FINISHED => [],
4063

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

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.
67+
# 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.
4568
CGI::SCRIPT_NAME => "",
4669

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.
70+
# 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.
4871
CGI::PATH_INFO => request_path,
4972
CGI::REQUEST_PATH => request_path,
5073
CGI::REQUEST_URI => request.path,
@@ -72,8 +95,11 @@ def make_environment(request)
7295
return env
7396
end
7497

75-
# Process the rack response headers into into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
76-
# @returns [Tuple(Protocol::HTTP::Headers, Hash)]
98+
# Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
99+
# Unlike Rack 2, this adapter handles array values directly without splitting on newlines.
100+
#
101+
# @parameter fields [Hash] The raw response headers.
102+
# @returns [Tuple(Protocol::HTTP::Headers, Hash)] The processed headers and metadata.
77103
def wrap_headers(fields)
78104
headers = ::Protocol::HTTP::Headers.new
79105
meta = {}
@@ -95,6 +121,13 @@ def wrap_headers(fields)
95121
return headers, meta
96122
end
97123

124+
# Convert a {Protocol::HTTP::Response} into a Rack 3 response tuple.
125+
# Handles protocol upgrades and streaming responses.
126+
# Unlike Rack 2, this adapter forces streaming responses by converting the body to a callable.
127+
#
128+
# @parameter env [Hash] The rack environment.
129+
# @parameter response [Protocol::HTTP::Response] The HTTP response.
130+
# @returns [Tuple(Integer, Hash, Object)] The Rack 3 response tuple [status, headers, body].
98131
def self.make_response(env, response)
99132
# These interfaces should be largely compatible:
100133
headers = response.headers.to_h

lib/protocol/rack/adapter/rack31.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,19 @@
1111
module Protocol
1212
module Rack
1313
module Adapter
14+
# The Rack 3.1 adapter provides compatibility with Rack 3.1.x applications.
15+
# It extends the Rack 3 adapter with improved request body handling and protocol support.
16+
# Key improvements include:
17+
# - Better handling of empty request bodies
18+
# - Direct protocol support via {RACK_PROTOCOL}
19+
# - More efficient body streaming
1420
class Rack31 < Rack3
21+
# Create a Rack 3.1 environment hash for the request.
22+
# Sets up all required Rack 3.1 environment variables and processes the request.
23+
# Unlike Rack 3, this adapter has improved body handling and protocol support.
24+
#
25+
# @parameter request [Protocol::HTTP::Request] The incoming request.
26+
# @returns [Hash] The Rack 3.1 environment hash.
1527
def make_environment(request)
1628
request_path, query_string = request.path.split("?", 2)
1729
server_name, server_port = (request.authority || "").split(":", 2)
@@ -25,13 +37,13 @@ def make_environment(request)
2537
# The response finished callbacks:
2638
RACK_RESPONSE_FINISHED => [],
2739

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

31-
# 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.
43+
# 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.
3244
CGI::SCRIPT_NAME => "",
3345

34-
# 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.
46+
# 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.
3547
CGI::PATH_INFO => request_path,
3648
CGI::REQUEST_PATH => request_path,
3749
CGI::REQUEST_URI => request.path,

lib/protocol/rack/body.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,34 @@
1010

1111
module Protocol
1212
module Rack
13+
# The Body module provides functionality for handling Rack response bodies.
14+
# It includes methods for wrapping different types of response bodies and handling completion callbacks.
1315
module Body
16+
# The `content-length` header key.
1417
CONTENT_LENGTH = "content-length"
1518

19+
# Check if the given status code indicates no content should be returned.
20+
# Status codes 204 (No Content), 205 (Reset Content), and 304 (Not Modified) should not include a response body.
21+
#
22+
# @parameter status [Integer] The HTTP status code.
23+
# @returns [Boolean] True if the status code indicates no content.
1624
def self.no_content?(status)
1725
status == 204 or status == 205 or status == 304
1826
end
1927

28+
# Wrap a Rack response body into a {Protocol::HTTP::Body} instance.
29+
# Handles different types of response bodies:
30+
# - {Protocol::HTTP::Body::Readable} instances are returned as-is.
31+
# - Bodies that respond to `to_path` are wrapped in {Protocol::HTTP::Body::File}.
32+
# - Enumerable bodies are wrapped in {Body::Enumerable}.
33+
# - Other bodies are wrapped in {Body::Streaming}.
34+
#
35+
# @parameter env [Hash] The Rack environment.
36+
# @parameter status [Integer] The HTTP status code.
37+
# @parameter headers [Hash] The response headers.
38+
# @parameter body [Object] The response body to wrap.
39+
# @parameter input [Object] Optional input for streaming bodies.
40+
# @returns [Protocol::HTTP::Body] The wrapped response body.
2041
def self.wrap(env, status, headers, body, input = nil)
2142
# In no circumstance do we want this header propagating out:
2243
if length = headers.delete(CONTENT_LENGTH)
@@ -66,6 +87,14 @@ def self.wrap(env, status, headers, body, input = nil)
6687
return body
6788
end
6889

90+
# Create a completion callback for response finished handlers.
91+
# The callback is called with any error that occurred during response processing.
92+
#
93+
# @parameter response_finished [Array] Array of response finished callbacks.
94+
# @parameter env [Hash] The Rack environment.
95+
# @parameter status [Integer] The HTTP status code.
96+
# @parameter headers [Hash] The response headers.
97+
# @returns [Proc] A callback that calls all response finished handlers.
6998
def self.completion_callback(response_finished, env, status, headers)
7099
proc do |error|
71100
response_finished.each do |callback|

0 commit comments

Comments
 (0)