lib/protocol/http/request.rb



# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2019-2024, 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, _headers = nil, _body = nil, scheme: nil, authority: nil, headers: _headers, body: _body, protocol: nil, interim_response: nil)
				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