diff --git a/lib/protocol/http.rb b/lib/protocol/http.rb index 8bf912e..15eb446 100644 --- a/lib/protocol/http.rb +++ b/lib/protocol/http.rb @@ -4,3 +4,8 @@ # Copyright, 2018-2023, by Samuel Williams. require_relative "http/version" + +require_relative 'http/headers' +require_relative 'http/request' +require_relative 'http/response' +require_relative 'http/middleware' diff --git a/lib/protocol/http/body/buffered.rb b/lib/protocol/http/body/buffered.rb index 5fd387b..e0926ad 100644 --- a/lib/protocol/http/body/buffered.rb +++ b/lib/protocol/http/body/buffered.rb @@ -12,7 +12,11 @@ module Body # A body which buffers all it's contents. class Buffered < Readable # Wraps an array into a buffered body. - # @return [Readable, nil] the wrapped body or nil if nil was given. + # + # For compatibility, also accepts anything that behaves like an `Array(String)`. + # + # @parameter body [String | Array(String) | Readable | nil] the body to wrap. + # @returns [Readable | nil] the wrapped body or nil if nil was given. def self.wrap(body) if body.is_a?(Readable) return body diff --git a/lib/protocol/http/request.rb b/lib/protocol/http/request.rb index 86cb897..88855e0 100644 --- a/lib/protocol/http/request.rb +++ b/lib/protocol/http/request.rb @@ -11,6 +11,17 @@ 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 @@ -25,28 +36,28 @@ def initialize(scheme = nil, authority = nil, method = nil, path = nil, version @protocol = protocol end - # The request scheme, usually one of "http" or "https". + # @attribute [String] the request scheme, usually `"http"` or `"https"`. attr_accessor :scheme - - # The request authority, usually a hostname and port number. + + # @attribute [String] the request authority, usually a hostname and port number, e.g. `"example.com:80"`. attr_accessor :authority - - # The request method, usually one of "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT" or "OPTIONS". + + # @attribute [String] the request method, usually one of `"GET"`, `"HEAD"`, `"POST"`, `"PUT"`, `"DELETE"`, `"CONNECT"` or `"OPTIONS"`, etc. attr_accessor :method - - # The request path, usually a path and query string. + + # @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 - - # The request version, usually "http/1.0", "http/1.1", "h2", or "h3". + + # @attribute [String] the request version, usually `"http/1.0"`, `"http/1.1"`, `"h2"`, or `"h3"`. attr_accessor :version - - # The request headers, contains metadata associated with the request such as the user agent, accept (content type), accept-language, etc. + + # @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 - - # The request body, an instance of Protocol::HTTP::Body::Readable or similar. + + # @attribute [Body::Readable] the request body. It should only be read once (it may not be idempotent). attr_accessor :body - # The request protocol, usually empty, but occasionally "websocket" or "webtransport", can be either single value `String` or multi-value `Array` of `String` instances. 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. + # @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 # Send the request to the given connection. @@ -54,14 +65,22 @@ def call(connection) connection.call(self) 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) body = Body::Buffered.wrap(body) headers = ::Protocol::HTTP::Headers[headers] @@ -69,6 +88,7 @@ def self.[](method, path, headers = nil, body = nil) self.new(nil, nil, method, path, nil, headers, body) end + # Whether the request can be replayed without side-effects. def idempotent? @method != Methods::POST && (@body.nil? || @body.empty?) end diff --git a/lib/protocol/http/response.rb b/lib/protocol/http/response.rb index 84fc45d..17985a0 100644 --- a/lib/protocol/http/response.rb +++ b/lib/protocol/http/response.rb @@ -8,9 +8,27 @@ module Protocol module HTTP + # Represents an HTTP response which can be used both server and client-side. + # + # ~~~ ruby + # require 'protocol/http' + # + # # Long form: + # Protocol::HTTP::Response.new("http/1.1", 200, Protocol::HTTP::Headers[["content-type", "text/html"]], Protocol::HTTP::Body::Buffered.wrap("Hello, World!")) + # + # # Short form: + # Protocol::HTTP::Response[200, {"content-type" => "text/html"}, ["Hello, World!"]] + # ~~~ class Response prepend Body::Reader + # Create a new response. + # + # @parameter version [String | Nil] The HTTP version, e.g. `"HTTP/1.1"`. If `nil`, the version may be provided by the server sending the response. + # @parameter status [Integer] The HTTP status code, e.g. `200`, `404`, etc. + # @parameter headers [Hash] The headers, e.g. `{"content-type" => "text/html"}`, etc. + # @parameter body [Body::Readable] The body, e.g. `"Hello, World!"`, etc. + # @parameter protocol [String | Array(String)] The protocol, e.g. `"websocket"`, etc. def initialize(version = nil, status = 200, headers = Headers.new, body = nil, protocol = nil) @version = version @status = status @@ -19,12 +37,22 @@ def initialize(version = nil, status = 200, headers = Headers.new, body = nil, p @protocol = protocol end + # @attribute [String | Nil] The HTTP version, usually one of `"HTTP/1.1"`, `"HTTP/2"`, etc. attr_accessor :version + + # @attribute [Integer] The HTTP status code, e.g. `200`, `404`, etc. attr_accessor :status + + # @attribute [Hash] The headers, e.g. `{"content-type" => "text/html"}`, etc. attr_accessor :headers + + # @attribute [Body::Readable] The body, e.g. `"Hello, World!"`, etc. attr_accessor :body + + # @attribute [String | Array(String) | Nil] The protocol, e.g. `"websocket"`, etc. attr_accessor :protocol + # Whether the response is considered a hijack: the connection has been taken over by the application and the server should not send any more data. def hijack? false end @@ -93,6 +121,15 @@ def internal_server_error? # @deprecated Use {#internal_server_error?} instead. alias server_failure? internal_server_error? + # A short-cut method which exposes the main response variables that you'd typically care about. It follows the same order as the `Rack` response tuple, but also includes the protocol. + # + # ~~~ ruby + # Response[200, {"content-type" => "text/html"}, ["Hello, World!"]] + # ~~~ + # + # @parameter status [Integer] The HTTP status code, e.g. `200`, `404`, etc. + # @parameter headers [Hash] The headers, e.g. `{"content-type" => "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.[](status, headers = nil, body = nil, protocol = nil) body = Body::Buffered.wrap(body) headers = ::Protocol::HTTP::Headers[headers] @@ -100,6 +137,9 @@ def self.[](status, headers = nil, body = nil, protocol = nil) self.new(nil, status, headers, body, protocol) end + # Create a response for the given exception. + # + # @parameter exception [Exception] The exception to generate the response for. def self.for_exception(exception) Response[500, Headers['content-type' => 'text/plain'], ["#{exception.class}: #{exception.message}"]] end