File: request.rb

package info (click to toggle)
ruby-protocol-http 0.55.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 840 kB
  • sloc: ruby: 6,904; makefile: 4
file content (171 lines) | stat: -rw-r--r-- 6,975 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2025, by Samuel Williams.

require_relative "body/buffered"
require_relative "body/reader"

require_relative "headers"
require_relative "methods"

module Protocol
	module HTTP
		# Represents an HTTP request which can be used both server and client-side.
		#
		# ~~~ ruby
		# require 'protocol/http'
		# 
		# # Long form:
		# Protocol::HTTP::Request.new("http", "example.com", "GET", "/index.html", "HTTP/1.1", Protocol::HTTP::Headers[["accept", "text/html"]])
		# 
		# # Short form:
		# Protocol::HTTP::Request["GET", "/index.html", {"accept" => "text/html"}]
		# ~~~
		class Request
			prepend Body::Reader
			
			# Initialize the request.
			#
			# @parameter scheme [String | Nil] The request scheme, usually `"http"` or `"https"`.
			# @parameter authority [String | Nil] The request authority, usually a hostname and port number, e.g. `"example.com:80"`.
			# @parameter method [String | Nil] The request method, usually one of `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"DELETE"`, `"CONNECT"` or `"OPTIONS"`, etc.
			# @parameter path [String | Nil] The request path, usually a path and query string, e.g. `"/index.html"`, `"/search?q=hello"`, etc.
			# @parameter version [String | Nil] The request version, usually `"http/1.0"`, `"http/1.1"`, `"h2"`, or `"h3"`.
			# @parameter headers [Headers] The request headers, usually containing metadata associated with the request such as the `"user-agent"`, `"accept"` (content type), `"accept-language"`, etc.
			# @parameter body [Body::Readable] The request body.
			# @parameter protocol [String | Array(String) | Nil] The request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`.
			# @parameter interim_response [Proc] A callback which is called when an interim response is received.
			def initialize(scheme = nil, authority = nil, method = nil, path = nil, version = nil, headers = Headers.new, body = nil, protocol = nil, interim_response = nil)
				@scheme = scheme
				@authority = authority
				@method = method
				@path = path
				@version = version
				@headers = headers
				@body = body
				@protocol = protocol
				@interim_response = interim_response
			end
			
			# @attribute [String] the request scheme, usually `"http"` or `"https"`.
			attr_accessor :scheme
			
			# @attribute [String] the request authority, usually a hostname and port number, e.g. `"example.com:80"`.
			attr_accessor :authority
			
			# @attribute [String] the request method, usually one of `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"DELETE"`, `"CONNECT"` or `"OPTIONS"`, etc.
			attr_accessor :method
			
			# @attribute [String] the request path, usually a path and query string, e.g. `"/index.html"`, `"/search?q=hello"`, however it can be any [valid request target](https://www.rfc-editor.org/rfc/rfc9110#target.resource).
			attr_accessor :path
			
			# @attribute [String] the request version, usually `"http/1.0"`, `"http/1.1"`, `"h2"`, or `"h3"`.
			attr_accessor :version
			
			# @attribute [Headers] the request headers, usually containing metadata associated with the request such as the `"user-agent"`, `"accept"` (content type), `"accept-language"`, etc.
			attr_accessor :headers
			
			# @attribute [Body::Readable] the request body. It should only be read once (it may not be idempotent).
			attr_accessor :body
			
			# @attribute [String | Array(String) | Nil] the request protocol, usually empty, but occasionally `"websocket"` or `"webtransport"`. In HTTP/1, it is used to request a connection upgrade, and in HTTP/2 it is used to indicate a specfic protocol for the stream.
			attr_accessor :protocol
			
			# @attribute [Proc] a callback which is called when an interim response is received.
			attr_accessor :interim_response
			
			# A request that is generated by a server, may choose to include the peer (address) associated with the request. It should be implemented by a sub-class.
			#
			# @returns [Peer | Nil] The peer (address) associated with the request.
			def peer
				nil
			end
			
			# Send the request to the given connection.
			def call(connection)
				connection.call(self)
			end
			
			# Send an interim response back to the origin of this request, if possible.
			def send_interim_response(status, headers)
				@interim_response&.call(status, headers)
			end
			
			# Register a callback to be called when an interim response is received.
			#
			# @yields {|status, headers| ...} The callback to be called when an interim response is received.
			# 	@parameter status [Integer] The HTTP status code, e.g. `100`, `101`, etc.
			# 	@parameter headers [Hash] The headers, e.g. `{"link" => "</style.css>; rel=stylesheet"}`, etc.
			def on_interim_response(&block)
				if interim_response = @interim_response
					@interim_response = ->(status, headers) do
						block.call(status, headers)
						interim_response.call(status, headers)
					end
				else
					@interim_response = block
				end
			end
			
			# Whether this is a HEAD request: no body is expected in the response.
			def head?
				@method == Methods::HEAD
			end
			
			# Whether this is a CONNECT request: typically used to establish a tunnel.
			def connect?
				@method == Methods::CONNECT
			end
			
			# A short-cut method which exposes the main request variables that you'd typically care about.
			#
			# @parameter method [String] The HTTP method, e.g. `"GET"`, `"POST"`, etc.
			# @parameter path [String] The path, e.g. `"/index.html"`, `"/search?q=hello"`, etc.
			# @parameter headers [Hash] The headers, e.g. `{"accept" => "text/html"}`, etc.
			# @parameter body [String | Array(String) | Body::Readable] The body, e.g. `"Hello, World!"`, etc. See {Body::Buffered.wrap} for more information about .
			def self.[](method, path = nil, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil)
				path = path&.to_s
				body = Body::Buffered.wrap(body)
				headers = Headers[headers]
				
				self.new(scheme, authority, method, path, nil, headers, body, protocol, interim_response)
			end
			
			# Whether the request can be replayed without side-effects.
			def idempotent?
				@method != Methods::POST && (@body.nil? || @body.empty?)
			end
			
			# Convert the request to a hash, suitable for serialization.
			#
			# @returns [Hash] The request as a hash.
			def as_json(...)
				{
					scheme: @scheme,
					authority: @authority,
					method: @method,
					path: @path,
					version: @version,
					headers: @headers&.as_json,
					body: @body&.as_json,
					protocol: @protocol
				}
			end
			
			# Convert the request to JSON.
			#
			# @returns [String] The request as JSON.
			def to_json(...)
				as_json.to_json(...)
			end
			
			# Summarize the request as a string.
			#
			# @returns [String] The request as a string.
			def to_s
				"#{@scheme}://#{@authority}: #{@method} #{@path} #{@version}"
			end
		end
	end
end