protocol-rack 0.12.0 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 253756f1f94918a43327d8f107e1a614414b6c7eee787d876135454697f3f108
4
- data.tar.gz: bfe5453d726e362ac82c06a723d941991da8d6663749a924f8a81c919f6ca798
3
+ metadata.gz: b70faf13a5271971f02ddae3e19e17262c9df2ce74832cce7e0f6312265d76df
4
+ data.tar.gz: e9a4a1dfc0cb419d6395a7e68300f270e26398fadb00661b08093c60b47b7ea2
5
5
  SHA512:
6
- metadata.gz: 990b93f88c80068cdcf57c36ed0ff8bfeeb0e79044dab843668bf93db4eff71ce4117da113b0cac812625d178603c815ca121d8b233945772a0af289690e053c
7
- data.tar.gz: f84b64ec28519ab67db3770d6e2dce6ee86387cbc730a283fe7fa83811da2bb0d6ff831fd05f41ca5c2e8f15b33c92e7864e6ab17ad8335ba965ff3be24505a2
6
+ metadata.gz: 1d8a7f11acb1220d0ed05626c4ddfb87ef23578922741e92d15bf32e0e0294c433ebfa2149e538f10352695b3c0cf30af69667ab34033beead13f799dbbbdad5
7
+ data.tar.gz: 4d59623d81149838f7b2bf5f0ce67a13bdf39a0d5b04e17367ab6d07d57468d008834d6ad439cd6f41e42dcf95e6d314694334b38bfcfe02ba0d96f5a35b5def
checksums.yaml.gz.sig CHANGED
Binary file
@@ -1,35 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "console"
7
7
 
8
8
  require_relative "../constants"
9
9
  require_relative "../input"
10
10
  require_relative "../response"
11
+ require_relative "../rewindable"
11
12
 
12
13
  module Protocol
13
14
  module Rack
14
15
  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.
15
18
  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.
16
24
  def self.wrap(app)
17
- self.new(app)
25
+ Rewindable.new(self.new(app))
18
26
  end
19
27
 
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.
20
32
  def self.parse_file(...)
21
33
  # This is the old interface, which was changed in Rack 3.
22
34
  ::Rack::Builder.parse_file(...).first
23
35
  end
24
36
 
25
37
  # 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`.
27
41
  def initialize(app)
28
42
  @app = app
29
43
 
30
44
  raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
31
45
  end
32
46
 
47
+ # The logger to use for this adapter.
48
+ #
49
+ # @returns [Console] The console logger.
33
50
  def logger
34
51
  Console
35
52
  end
@@ -108,6 +125,10 @@ module Protocol
108
125
  end
109
126
  end
110
127
 
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.
111
132
  def make_environment(request)
112
133
  {
113
134
  request: request
@@ -117,6 +138,8 @@ module Protocol
117
138
  # Build a rack `env` from the incoming request and apply it to the rack middleware.
118
139
  #
119
140
  # @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.
120
143
  def call(request)
121
144
  env = self.make_environment(request)
122
145
 
@@ -148,12 +171,18 @@ module Protocol
148
171
  end
149
172
 
150
173
  # 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.
153
177
  def failure_response(exception)
154
178
  Protocol::HTTP::Response.for_exception(exception)
155
179
  end
156
180
 
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.
157
186
  def self.extract_protocol(env, response, headers)
158
187
  if protocol = response.protocol
159
188
  # This is the newer mechanism for protocol upgrade:
@@ -12,16 +12,23 @@ require_relative "../rewindable"
12
12
  module Protocol
13
13
  module Rack
14
14
  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.
15
17
  class Rack2 < Generic
18
+ # The Rack version constant.
16
19
  RACK_VERSION = "rack.version"
20
+ # Whether the application is multithreaded.
17
21
  RACK_MULTITHREAD = "rack.multithread"
22
+ # Whether the application is multiprocess.
18
23
  RACK_MULTIPROCESS = "rack.multiprocess"
24
+ # Whether the application should run only once.
19
25
  RACK_RUN_ONCE = "rack.run_once"
20
26
 
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.
25
32
  def make_environment(request)
26
33
  request_path, query_string = request.path.split("?", 2)
27
34
  server_name, server_port = (request.authority || "").split(":", 2)
@@ -38,13 +45,13 @@ module Protocol
38
45
  RACK_ERRORS => $stderr,
39
46
  RACK_LOGGER => self.logger,
40
47
 
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.
42
49
  CGI::REQUEST_METHOD => request.method,
43
50
 
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.
45
52
  CGI::SCRIPT_NAME => "",
46
53
 
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.
48
55
  CGI::PATH_INFO => request_path,
49
56
  CGI::REQUEST_PATH => request_path,
50
57
  CGI::REQUEST_URI => request.path,
@@ -75,11 +82,27 @@ module Protocol
75
82
  # Build a rack `env` from the incoming request and apply it to the rack middleware.
76
83
  #
77
84
  # @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.
78
87
  def call(request)
79
88
  env = self.make_environment(request)
80
89
 
81
90
  status, headers, body = @app.call(env)
82
91
 
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
+
83
106
  headers, meta = self.wrap_headers(headers)
84
107
 
85
108
  # Rack 2 spec does not allow only partial hijacking.
@@ -96,8 +119,11 @@ module Protocol
96
119
  return failure_response(exception)
97
120
  end
98
121
 
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.
101
127
  def wrap_headers(fields)
102
128
  headers = ::Protocol::HTTP::Headers.new
103
129
  meta = {}
@@ -119,6 +145,12 @@ module Protocol
119
145
  return headers, meta
120
146
  end
121
147
 
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].
122
154
  def self.make_response(env, response)
123
155
  # These interfaces should be largely compatible:
124
156
  headers = response.headers.to_h
@@ -133,7 +165,11 @@ module Protocol
133
165
  end
134
166
 
135
167
  headers.transform_values! do |value|
136
- value.is_a?(Array) ? value.join("\n") : value
168
+ if value.is_a?(Array)
169
+ value.join("\n")
170
+ else
171
+ value
172
+ end
137
173
  end
138
174
 
139
175
  [response.status, headers, body]
@@ -11,15 +11,34 @@ require_relative "generic"
11
11
  module Protocol
12
12
  module Rack
13
13
  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.
14
17
  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.
15
23
  def self.wrap(app)
16
24
  self.new(app)
17
25
  end
18
26
 
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.
19
32
  def self.parse_file(...)
20
33
  ::Rack::Builder.parse_file(...)
21
34
  end
22
35
 
36
+ # Create a Rack 3 environment hash for the request.
37
+ # Sets up all required Rack 3 environment variables and processes the request.
38
+ # Unlike Rack 2, this adapter doesn't set Rack version or threading flags.
39
+ #
40
+ # @parameter request [Protocol::HTTP::Request] The incoming request.
41
+ # @returns [Hash] The Rack 3 environment hash.
23
42
  def make_environment(request)
24
43
  request_path, query_string = request.path.split("?", 2)
25
44
  server_name, server_port = (request.authority || "").split(":", 2)
@@ -34,13 +53,13 @@ module Protocol
34
53
  # The response finished callbacks:
35
54
  RACK_RESPONSE_FINISHED => [],
36
55
 
37
- # The HTTP request method, such as GET or POST”. This cannot ever be an empty string, and so is always required.
56
+ # The HTTP request method, such as "GET" or "POST". This cannot ever be an empty string, and so is always required.
38
57
  CGI::REQUEST_METHOD => request.method,
39
58
 
40
- # 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.
59
+ # 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.
41
60
  CGI::SCRIPT_NAME => "",
42
61
 
43
- # 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.
62
+ # 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.
44
63
  CGI::PATH_INFO => request_path,
45
64
  CGI::REQUEST_PATH => request_path,
46
65
  CGI::REQUEST_URI => request.path,
@@ -57,7 +76,7 @@ module Protocol
57
76
  # I'm not sure what sane defaults should be here:
58
77
  CGI::SERVER_NAME => server_name,
59
78
  }
60
-
79
+
61
80
  # SERVER_PORT is optional but must not be set if it is not present.
62
81
  if server_port
63
82
  env[CGI::SERVER_PORT] = server_port
@@ -68,8 +87,11 @@ module Protocol
68
87
  return env
69
88
  end
70
89
 
71
- # Process the rack response headers into into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
72
- # @returns [Tuple(Protocol::HTTP::Headers, Hash)]
90
+ # Process the rack response headers into a {Protocol::HTTP::Headers} instance, along with any extra `rack.` metadata.
91
+ # Unlike Rack 2, this adapter handles array values directly without splitting on newlines.
92
+ #
93
+ # @parameter fields [Hash] The raw response headers.
94
+ # @returns [Tuple(Protocol::HTTP::Headers, Hash)] The processed headers and metadata.
73
95
  def wrap_headers(fields)
74
96
  headers = ::Protocol::HTTP::Headers.new
75
97
  meta = {}
@@ -91,6 +113,13 @@ module Protocol
91
113
  return headers, meta
92
114
  end
93
115
 
116
+ # Convert a {Protocol::HTTP::Response} into a Rack 3 response tuple.
117
+ # Handles protocol upgrades and streaming responses.
118
+ # Unlike Rack 2, this adapter forces streaming responses by converting the body to a callable.
119
+ #
120
+ # @parameter env [Hash] The rack environment.
121
+ # @parameter response [Protocol::HTTP::Response] The HTTP response.
122
+ # @returns [Tuple(Integer, Hash, Object)] The Rack 3 response tuple [status, headers, body].
94
123
  def self.make_response(env, response)
95
124
  # These interfaces should be largely compatible:
96
125
  headers = response.headers.to_h
@@ -11,7 +11,19 @@ require_relative "rack3"
11
11
  module Protocol
12
12
  module Rack
13
13
  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
14
20
  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.
15
27
  def make_environment(request)
16
28
  request_path, query_string = request.path.split("?", 2)
17
29
  server_name, server_port = (request.authority || "").split(":", 2)
@@ -25,13 +37,13 @@ module Protocol
25
37
  # The response finished callbacks:
26
38
  RACK_RESPONSE_FINISHED => [],
27
39
 
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.
29
41
  CGI::REQUEST_METHOD => request.method,
30
42
 
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.
32
44
  CGI::SCRIPT_NAME => "",
33
45
 
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.
35
47
  CGI::PATH_INFO => request_path,
36
48
  CGI::REQUEST_PATH => request_path,
37
49
  CGI::REQUEST_URI => request.path,
@@ -48,7 +60,7 @@ module Protocol
48
60
  # I'm not sure what sane defaults should be here:
49
61
  CGI::SERVER_NAME => server_name,
50
62
  }
51
-
63
+
52
64
  # SERVER_PORT is optional but must not be set if it is not present.
53
65
  if server_port
54
66
  env[CGI::SERVER_PORT] = server_port
@@ -1,13 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "rack"
7
7
 
8
8
  module Protocol
9
9
  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
+ # ```
10
18
  module Adapter
19
+ # The version of Rack being used. Can be overridden using the PROTOCOL_RACK_ADAPTER_VERSION environment variable.
11
20
  VERSION = ENV.fetch("PROTOCOL_RACK_ADAPTER_VERSION", ::Rack.release)
12
21
 
13
22
  if VERSION >= "3.1"
@@ -21,14 +30,27 @@ module Protocol
21
30
  IMPLEMENTATION = Rack2
22
31
  end
23
32
 
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
24
37
  def self.new(app)
25
38
  IMPLEMENTATION.wrap(app)
26
39
  end
27
40
 
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
28
46
  def self.make_response(env, response)
29
47
  IMPLEMENTATION.make_response(env, response)
30
48
  end
31
49
 
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
32
54
  def self.parse_file(...)
33
55
  IMPLEMENTATION.parse_file(...)
34
56
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/body/readable"
7
7
  require "protocol/http/body/file"
@@ -9,14 +9,19 @@ require "protocol/http/body/file"
9
9
  module Protocol
10
10
  module Rack
11
11
  module Body
12
- # Wraps the rack response body.
13
- #
14
- # The `rack` body must respond to `each` and must only yield `String` values. If the body responds to `close`, it will be called after iteration.
12
+ # Wraps a Rack response body that responds to `each`.
13
+ # The body must only yield `String` values and may optionally respond to `close`.
14
+ # This class provides both streaming and buffered access to the response body.
15
15
  class Enumerable < ::Protocol::HTTP::Body::Readable
16
+ # The content-length header key.
16
17
  CONTENT_LENGTH = "content-length".freeze
17
18
 
18
- # Wraps an array into a buffered body.
19
- # @parameter body [Object] The `rack` response body.
19
+ # Wraps a Rack response body into an {Enumerable} instance.
20
+ # If the body is an Array, its total size is calculated automatically.
21
+ #
22
+ # @parameter body [Object] The Rack response body that responds to `each`.
23
+ # @parameter length [Integer] Optional content length of the response body.
24
+ # @returns [Enumerable] A new enumerable body instance.
20
25
  def self.wrap(body, length = nil)
21
26
  if body.is_a?(Array)
22
27
  length ||= body.sum(&:bytesize)
@@ -26,9 +31,10 @@ module Protocol
26
31
  end
27
32
  end
28
33
 
29
- # Initialize the output wrapper.
30
- # @parameter body [Object] The rack response body.
31
- # @parameter length [Integer] The rack response length.
34
+ # Initialize the enumerable body wrapper.
35
+ #
36
+ # @parameter body [Object] The Rack response body that responds to `each`.
37
+ # @parameter length [Integer] The content length of the response body.
32
38
  def initialize(body, length)
33
39
  @length = length
34
40
  @body = body
@@ -36,23 +42,32 @@ module Protocol
36
42
  @chunks = nil
37
43
  end
38
44
 
39
- # The rack response body.
45
+ # @attribute [Object] The wrapped Rack response body.
40
46
  attr :body
41
47
 
42
- # The content length of the rack response body.
48
+ # @attribute [Integer] The total size of the response body in bytes.
43
49
  attr :length
44
50
 
45
- # Whether the body is empty.
51
+ # Check if the response body is empty.
52
+ # A body is considered empty if its length is 0 or if it responds to `empty?` and is empty.
53
+ #
54
+ # @returns [Boolean] True if the body is empty.
46
55
  def empty?
47
56
  @length == 0 or (@body.respond_to?(:empty?) and @body.empty?)
48
57
  end
49
58
 
50
- # Whether the body can be read immediately.
59
+ # Check if the response body can be read immediately.
60
+ # A body is ready if it's an Array or responds to `to_ary`.
61
+ #
62
+ # @returns [Boolean] True if the body can be read immediately.
51
63
  def ready?
52
64
  body.is_a?(Array) or body.respond_to?(:to_ary)
53
65
  end
54
66
 
55
67
  # Close the response body.
68
+ # If the body responds to `close`, it will be called.
69
+ #
70
+ # @parameter error [Exception] Optional error that occurred during processing.
56
71
  def close(error = nil)
57
72
  if @body and @body.respond_to?(:close)
58
73
  @body.close
@@ -65,26 +80,43 @@ module Protocol
65
80
  end
66
81
 
67
82
  # Enumerate the response body.
83
+ # Each chunk yielded must be a String.
84
+ # The body is automatically closed after enumeration.
85
+ #
68
86
  # @yields {|chunk| ...}
69
- # @parameter chunk [String]
87
+ # @parameter chunk [String] A chunk of the response body.
70
88
  def each(&block)
71
89
  @body.each(&block)
90
+ rescue => error
91
+ raise
72
92
  ensure
73
- self.close($!)
93
+ self.close(error)
74
94
  end
75
95
 
96
+ # Check if the body is a streaming response.
97
+ # A body is streaming if it doesn't respond to `each`.
98
+ #
99
+ # @returns [Boolean] True if the body is streaming.
76
100
  def stream?
77
101
  !@body.respond_to?(:each)
78
102
  end
79
103
 
104
+ # Stream the response body to the given stream.
105
+ # The body is automatically closed after streaming.
106
+ #
107
+ # @parameter stream [Object] The stream to write the body to.
80
108
  def call(stream)
81
109
  @body.call(stream)
110
+ rescue => error
111
+ raise
82
112
  ensure
83
- self.close($!)
113
+ self.close(error)
84
114
  end
85
115
 
86
116
  # Read the next chunk from the response body.
87
- # @returns [String | Nil]
117
+ # Returns nil when there are no more chunks.
118
+ #
119
+ # @returns [String | Nil] The next chunk or nil if there are no more chunks.
88
120
  def read
89
121
  @chunks ||= @body.to_enum(:each)
90
122
 
@@ -93,6 +125,9 @@ module Protocol
93
125
  return nil
94
126
  end
95
127
 
128
+ # Get a string representation of the body.
129
+ #
130
+ # @returns [String] A string describing the body's class and length.
96
131
  def inspect
97
132
  "\#<#{self.class} length=#{@length.inspect} body=#{@body.class}>"
98
133
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/body/readable"
7
7
  require "protocol/http/body/stream"
@@ -9,10 +9,17 @@ require "protocol/http/body/stream"
9
9
  module Protocol
10
10
  module Rack
11
11
  module Body
12
- # Used for wrapping a generic `rack.input` object into a readable body.
12
+ # Wraps a Rack input object into a readable body.
13
+ # This class provides a consistent interface for reading from Rack input streams,
14
+ # which may be any IO-like object that responds to `read` and `close`.
13
15
  class InputWrapper < Protocol::HTTP::Body::Readable
16
+ # The default block size for reading from the input stream.
14
17
  BLOCK_SIZE = 1024*4
15
18
 
19
+ # Initialize the input wrapper.
20
+ #
21
+ # @parameter io [Object] The input object that responds to `read` and `close`.
22
+ # @parameter block_size [Integer] The size of chunks to read at a time.
16
23
  def initialize(io, block_size: BLOCK_SIZE)
17
24
  @io = io
18
25
  @block_size = block_size
@@ -20,6 +27,10 @@ module Protocol
20
27
  super()
21
28
  end
22
29
 
30
+ # Close the input stream.
31
+ # If the input object responds to `close`, it will be called.
32
+ #
33
+ # @parameter error [Exception] Optional error that occurred during processing.
23
34
  def close(error = nil)
24
35
  if @io
25
36
  @io.close
@@ -27,12 +38,10 @@ module Protocol
27
38
  end
28
39
  end
29
40
 
30
- # def join
31
- # @io.read.tap do |buffer|
32
- # buffer.force_encoding(Encoding::BINARY)
33
- # end
34
- # end
35
-
41
+ # Read the next chunk from the input stream.
42
+ # Returns nil when there is no more data to read.
43
+ #
44
+ # @returns [String | Nil] The next chunk of data or nil if there is no more data.
36
45
  def read
37
46
  @io&.read(@block_size)
38
47
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "body/streaming"
7
7
  require_relative "body/enumerable"
@@ -10,13 +10,34 @@ require "protocol/http/body/completable"
10
10
 
11
11
  module Protocol
12
12
  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.
13
15
  module Body
16
+ # The `content-length` header key.
14
17
  CONTENT_LENGTH = "content-length"
15
18
 
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.
16
24
  def self.no_content?(status)
17
25
  status == 204 or status == 205 or status == 304
18
26
  end
19
27
 
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.
20
41
  def self.wrap(env, status, headers, body, input = nil)
21
42
  # In no circumstance do we want this header propagating out:
22
43
  if length = headers.delete(CONTENT_LENGTH)
@@ -66,6 +87,14 @@ module Protocol
66
87
  return body
67
88
  end
68
89
 
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.
69
98
  def self.completion_callback(response_finished, env, status, headers)
70
99
  proc do |error|
71
100
  response_finished.each do |callback|
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
  # Copyright, 2023, by Genki Takiuchi.
6
6
 
7
7
  require "protocol/http/body/stream"
@@ -64,8 +64,10 @@ module Protocol
64
64
  if @body and @body.respond_to?(:rewind)
65
65
  # If the body is not rewindable, this will fail.
66
66
  @body.rewind
67
+
67
68
  @buffer = nil
68
69
  @finished = false
70
+ @closed = false
69
71
 
70
72
  return true
71
73
  end
@@ -93,14 +95,18 @@ module Protocol
93
95
  # https://github.com/socketry/async-http/issues/183
94
96
  if @body.empty?
95
97
  @body.close
96
- @body = nil
98
+ @closed = true
97
99
  end
98
100
 
99
101
  return chunk
100
102
  else
101
- # So if we are at the end of the stream, we close it automatically:
102
- @body.close
103
- @body = nil
103
+ unless @closed
104
+ # So if we are at the end of the stream, we close it automatically:
105
+ @body.close
106
+ @closed = true
107
+ end
108
+
109
+ return nil
104
110
  end
105
111
  elsif @closed
106
112
  raise IOError, "Stream is not readable, input has been closed!"
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/request"
7
7
  require "protocol/http/headers"
@@ -11,11 +11,22 @@ require_relative "body/input_wrapper"
11
11
 
12
12
  module Protocol
13
13
  module Rack
14
+ # A Rack-compatible HTTP request wrapper.
15
+ # This class provides a bridge between Rack's environment hash and Protocol::HTTP::Request.
16
+ # It handles conversion of Rack environment variables to HTTP request properties.
14
17
  class Request < ::Protocol::HTTP::Request
18
+ # Get or create a Request instance for the given Rack environment.
19
+ # The request is cached in the environment to avoid creating multiple instances.
20
+ #
21
+ # @parameter env [Hash] The Rack environment hash.
22
+ # @returns [Request] A Request instance for the environment.
15
23
  def self.[](env)
16
24
  env["protocol.http.request"] ||= new(env)
17
25
  end
18
26
 
27
+ # Initialize a new Request instance from a Rack environment.
28
+ #
29
+ # @parameter env [Hash] The Rack environment hash.
19
30
  def initialize(env)
20
31
  @env = env
21
32
 
@@ -31,6 +42,11 @@ module Protocol
31
42
  )
32
43
  end
33
44
 
45
+ # Extract the protocol list from the Rack environment.
46
+ # Checks both `rack.protocol` and `HTTP_UPGRADE` headers.
47
+ #
48
+ # @parameter env [Hash] The Rack environment hash.
49
+ # @returns [Array(String) | Nil] The list of protocols or `nil` if none specified.
34
50
  def self.protocol(env)
35
51
  if protocols = env["rack.protocol"]
36
52
  return Array(protocols)
@@ -39,6 +55,11 @@ module Protocol
39
55
  end
40
56
  end
41
57
 
58
+ # Extract HTTP headers from the Rack environment.
59
+ # Converts Rack's `HTTP_*` environment variables to proper HTTP headers.
60
+ #
61
+ # @parameter env [Hash] The Rack environment hash.
62
+ # @returns [Protocol::HTTP::Headers] The extracted HTTP headers.
42
63
  def self.headers(env)
43
64
  headers = ::Protocol::HTTP::Headers.new
44
65
  env.each do |key, value|
@@ -5,10 +5,10 @@
5
5
 
6
6
  require_relative "body"
7
7
  require_relative "constants"
8
- # require 'time'
9
8
 
10
9
  require "protocol/http/response"
11
10
  require "protocol/http/headers"
11
+ require "protocol/http/body/head"
12
12
 
13
13
  module Protocol
14
14
  module Rack
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require "protocol/http/body/rewindable"
7
7
  require "protocol/http/middleware"
@@ -9,8 +9,12 @@ require "protocol/http/middleware"
9
9
  module Protocol
10
10
  module Rack
11
11
  # Content-type driven input buffering, specific to the needs of `rack`.
12
+ # This middleware ensures that request bodies for certain content types
13
+ # can be read multiple times, which is required by Rack's specification.
12
14
  class Rewindable < ::Protocol::HTTP::Middleware
13
15
  # Media types that require buffering.
16
+ # These types typically contain form data or file uploads that may need
17
+ # to be read multiple times by Rack applications.
14
18
  BUFFERED_MEDIA_TYPES = %r{
15
19
  application/x-www-form-urlencoded|
16
20
  multipart/form-data|
@@ -18,17 +22,23 @@ module Protocol
18
22
  multipart/mixed
19
23
  }x
20
24
 
25
+ # The HTTP POST method.
21
26
  POST = "POST"
22
27
 
23
28
  # Initialize the rewindable middleware.
29
+ #
24
30
  # @parameter app [Protocol::HTTP::Middleware] The middleware to wrap.
25
31
  def initialize(app)
26
32
  super(app)
27
33
  end
28
34
 
29
35
  # Determine whether the request needs a rewindable body.
30
- # @parameter request [Protocol::HTTP::Request]
31
- # @returns [Boolean]
36
+ # A request needs a rewindable body if:
37
+ # - It's a POST request with no content type (legacy behavior)
38
+ # - It has a content type that matches BUFFERED_MEDIA_TYPES
39
+ #
40
+ # @parameter request [Protocol::HTTP::Request] The request to check.
41
+ # @returns [Boolean] True if the request body should be rewindable.
32
42
  def needs_rewind?(request)
33
43
  content_type = request.headers["content-type"]
34
44
 
@@ -43,13 +53,20 @@ module Protocol
43
53
  return false
44
54
  end
45
55
 
56
+ # Create a Rack environment from the request.
57
+ # Delegates to the wrapped middleware.
58
+ #
59
+ # @parameter request [Protocol::HTTP::Request] The request to create an environment from.
60
+ # @returns [Hash] The Rack environment hash.
46
61
  def make_environment(request)
47
62
  @delegate.make_environment(request)
48
63
  end
49
64
 
50
65
  # Wrap the request body in a rewindable buffer if required.
51
- # @parameter request [Protocol::HTTP::Request]
52
- # @returns [Protocol::HTTP::Response] the response.
66
+ # If the request needs a rewindable body, wraps it in a {Protocol::HTTP::Body::Rewindable}.
67
+ #
68
+ # @parameter request [Protocol::HTTP::Request] The request to process.
69
+ # @returns [Protocol::HTTP::Response] The response from the wrapped middleware.
53
70
  def call(request)
54
71
  if body = request.body and needs_rewind?(request)
55
72
  request.body = Protocol::HTTP::Body::Rewindable.new(body)
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module Rack
8
- VERSION = "0.12.0"
8
+ VERSION = "0.13.0"
9
9
  end
10
10
  end
data/lib/protocol/rack.rb CHANGED
@@ -1,9 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "rack/version"
7
7
  require_relative "rack/adapter"
8
8
  require_relative "rack/request"
9
9
  require_relative "rack/response"
10
+
11
+ # @namespace
12
+ module Protocol
13
+ # @namespace
14
+ module Rack
15
+ end
16
+ end
data/readme.md CHANGED
@@ -67,6 +67,16 @@ run proc{|env|
67
67
 
68
68
  Please see the [project releases](https://socketry.github.io/protocol-rack/releases/index) for all releases.
69
69
 
70
+ ### v0.13.0
71
+
72
+ - 100% test and documentation coverage.
73
+ - `Protocol::Rack::Input#rewind` now works when the entire input is already read.
74
+ - `Protocol::Rack::Adapter::Rack2` has stricter validation of the application response.
75
+
76
+ ### v0.12.0
77
+
78
+ - Ignore (and close) response bodies for status codes that don't allow them.
79
+
70
80
  ### v0.11.2
71
81
 
72
82
  - Stop setting `env["SERVER_PORT"]` to `nil` if not present.
data/releases.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Releases
2
2
 
3
+ ## v0.13.0
4
+
5
+ - 100% test and documentation coverage.
6
+ - {Protocol::Rack::Input\#rewind} now works when the entire input is already read.
7
+ - {Protocol::Rack::Adapter::Rack2} has stricter validation of the application response.
8
+
9
+ ## v0.12.0
10
+
11
+ - Ignore (and close) response bodies for status codes that don't allow them.
12
+
3
13
  ## v0.11.2
4
14
 
5
15
  - Stop setting `env["SERVER_PORT"]` to `nil` if not present.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -38,7 +38,7 @@ cert_chain:
38
38
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
39
39
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
40
40
  -----END CERTIFICATE-----
41
- date: 2025-04-29 00:00:00.000000000 Z
41
+ date: 2025-05-10 00:00:00.000000000 Z
42
42
  dependencies:
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: protocol-http
metadata.gz.sig CHANGED
Binary file