diff --git a/lib/http/client.rb b/lib/http/client.rb index dfa05b16..9be350eb 100644 --- a/lib/http/client.rb +++ b/lib/http/client.rb @@ -102,6 +102,26 @@ def close @state = :clean end + # Hash representation of a client + # + # @return [Hash[Symbol, Any]] + def to_h + { + :connection => @connection, + :state => @state + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private def build_response(req, options) diff --git a/lib/http/connection.rb b/lib/http/connection.rb index 2f48e317..989e282b 100644 --- a/lib/http/connection.rb +++ b/lib/http/connection.rb @@ -145,6 +145,35 @@ def expired? !@conn_expires_at || @conn_expires_at < Time.now end + # Hash representation of a connection + # + # @return [Hash[Symbol, Any]] + def to_h + { + :persistent => @persistent, + :keep_alive_timeout => @keep_alive_timeout, + :pending_request => @pending_request, + :pending_response => @pending_response, + :failed_proxy_connect => @failed_proxy_connect, + :buffer => @buffer, + :parser => @parser, + :socket => @socket, + :status_code => status_code, + :http_version => http_version, + :headers => headers + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private # Sets up SSL context and starts TLS if needed. diff --git a/lib/http/content_type.rb b/lib/http/content_type.rb index aa40d3a3..9b8611e2 100644 --- a/lib/http/content_type.rb +++ b/lib/http/content_type.rb @@ -30,5 +30,25 @@ def initialize(mime_type = nil, charset = nil) @mime_type = mime_type @charset = charset end + + # Hash representaiton of ContentType + # + # @return [Hash[Symbol, Any]] + def to_h + { + :mime_type => @mime_type, + :charset => @charset + } + end + + # Pattern matching interface + # + # @param keys [Array[Symbol]] + # Keys to extract + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end end end diff --git a/lib/http/headers.rb b/lib/http/headers.rb index aad91ea3..1563d96b 100644 --- a/lib/http/headers.rb +++ b/lib/http/headers.rb @@ -115,6 +115,21 @@ def to_h end alias to_hash to_h + # Pattern matching interface + # + # @param keys [Array[Symbol]] + # Keys to extract + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + underscored_keys_map = underscored_keys_mapping + + to_h + .map { |k, v| [underscored_keys_map[k], v] } + .to_h + .slice(*keys) + end + # Returns headers key/value pairs. # # @return [Array<[String, String]>] @@ -122,6 +137,9 @@ def to_a @pile.map { |item| item[1..2] } end + # Adds pattern matching interface using `to_a` as a base + alias deconstruct to_a + # Returns human-readable representation of `self` instance. # # @return [String] @@ -219,6 +237,14 @@ def coerce(object) private + # Underscored version of HTTP Header keys for + # Pattern Matching + # + # @return [Hash[String, Symbol]] + def underscored_keys_mapping + Hash[keys.map { |k| [k, k.tr("A-Z-", "a-z_").to_sym] }] + end + # Transforms `name` to canonical HTTP header capitalization # # @param [String] name diff --git a/lib/http/options.rb b/lib/http/options.rb index 753057ab..f149b3af 100644 --- a/lib/http/options.rb +++ b/lib/http/options.rb @@ -169,6 +169,16 @@ def to_hash Hash[*hash_pairs] end + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_hash.slice(*keys) + end + def dup dupped = super yield(dupped) if block_given? diff --git a/lib/http/request.rb b/lib/http/request.rb index 8e99e3ee..9f4881c7 100644 --- a/lib/http/request.rb +++ b/lib/http/request.rb @@ -199,6 +199,32 @@ def inspect "#<#{self.class}/#{@version} #{verb.to_s.upcase} #{uri}>" end + # Hash representation of a request + # + # @return [Hash[Symbol, Any]] + def to_h + { + :verb => @verb, + :uri => @uri, + :scheme => @scheme, + :proxy => @proxy, + :version => @version, + :headers => @headers, + :body => @body, + :port => port + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private # @!attribute [r] host diff --git a/lib/http/request/body.rb b/lib/http/request/body.rb index 14af30b8..57f8862e 100644 --- a/lib/http/request/body.rb +++ b/lib/http/request/body.rb @@ -49,6 +49,26 @@ def ==(other) self.class == other.class && self.source == other.source # rubocop:disable Style/RedundantSelf end + # Hash representation of a + # + # @return [Hash[Symbol, Any]] + def to_h + { + :source => @source, + :size => size + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private def rewind(io) diff --git a/lib/http/request/writer.rb b/lib/http/request/writer.rb index 9f75028b..7a907b4f 100644 --- a/lib/http/request/writer.rb +++ b/lib/http/request/writer.rb @@ -102,6 +102,28 @@ def chunked? @headers[Headers::TRANSFER_ENCODING] == CHUNKED end + # Hash representation of a + # + # @return [Hash[Symbol, Any]] + def to_h + { + :body => @body, + :socket => @socket, + :headers => @headers, + :request_header => @request_header + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private def write(data) diff --git a/lib/http/response.rb b/lib/http/response.rb index 9f0d1f35..7fc9e508 100644 --- a/lib/http/response.rb +++ b/lib/http/response.rb @@ -90,6 +90,33 @@ def to_a [status.to_i, headers.to_h, body.to_s] end + # Adds pattern matching interface using `to_a` as a base + alias deconstruct to_a + + # Returns a Hash of accessible properties + # + # @return [Hash[Symbol, Any]] + def to_h + { + :version => @version, + :request => @request, + :status => @status, + :headers => @headers, + :proxy_headers => @proxy_headers, + :body => @body + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + # Flushes body and returns self-reference # # @return [Response] diff --git a/lib/http/response/body.rb b/lib/http/response/body.rb index a1030043..2ca7bc6a 100644 --- a/lib/http/response/body.rb +++ b/lib/http/response/body.rb @@ -72,6 +72,29 @@ def inspect "#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>" end + # Hash representation of a body + # + # @return [Hash[Symbol, Any]] + def to_h + { + :stream => @stream, + :connection => @connection, + :streaming => @streaming, + :contents => @contents, + :encoding => @encoding + } + end + + # Pattern matching interface + # + # @param keys [Array[Symbol]] + # Keys to extract + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private # Retrieve encoding by name. If encoding cannot be found, default to binary. diff --git a/lib/http/response/inflater.rb b/lib/http/response/inflater.rb index 22ff5bc0..2ef88d3e 100644 --- a/lib/http/response/inflater.rb +++ b/lib/http/response/inflater.rb @@ -22,6 +22,25 @@ def readpartial(*args) chunk end + # Hash representation of an inflater + # + # @return [Hash[Symbol, Any]] + def to_h + { + :connection => connection + } + end + + # Pattern matching interface + # + # @param keys [Array[Symbol]] + # Keys to extract + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + private def zstream diff --git a/lib/http/response/status.rb b/lib/http/response/status.rb index 2682d512..e1e1688c 100644 --- a/lib/http/response/status.rb +++ b/lib/http/response/status.rb @@ -139,6 +139,26 @@ def #{symbol}? # def bad_request? RUBY end + # Hash representation of a + # + # @return [Hash[Symbol, Any]] + def to_h + { + :code => code, + :reason => reason + } + end + + # Pattern matching interface + # + # @param keys [Array] + # Keys to be extracted + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end + def __setobj__(obj) raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i diff --git a/lib/http/uri.rb b/lib/http/uri.rb index f694ed08..f2b98945 100644 --- a/lib/http/uri.rb +++ b/lib/http/uri.rb @@ -146,5 +146,31 @@ def to_s def inspect format("#<%s:0x%014x URI:%s>", self.class.name, object_id << 1, to_s) end + + # Hash representation of a URI + # + # @return [Hash[Symbol, Any]] + def to_h + { + :scheme => scheme, + :user => user, + :password => password, + :host => host, + :port => port, + :path => path, + :query => query, + :fragment => fragment + } + end + + # Pattern matching interface for a URI + # + # @param keys [Array[Symbol]] + # Keys to extract from the URI + # + # @return [Hash[Symbol, Any]] + def deconstruct_keys(keys) + to_h.slice(*keys) + end end end diff --git a/spec/lib/http/client_spec.rb b/spec/lib/http/client_spec.rb index 0aef24d0..cb240cd9 100644 --- a/spec/lib/http/client_spec.rb +++ b/spec/lib/http/client_spec.rb @@ -502,4 +502,37 @@ def on_error(request, error) end end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + let(:client) { described_class.new } + + it "returns a Hash representation of a Client" do + expect(client.to_h).to include( + :connection => nil, + :state => :clean + ) + end + end + + describe "Pattern Matching" do + let(:client) { described_class.new } + + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case client + in state: :clean + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/connection_spec.rb b/spec/lib/http/connection_spec.rb index d7c9c546..52244b25 100644 --- a/spec/lib/http/connection_spec.rb +++ b/spec/lib/http/connection_spec.rb @@ -63,4 +63,42 @@ expect(buffer).to eq "1234567890" end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Connection" do + expect(connection.to_h).to include( + :buffer => "", + :failed_proxy_connect => false, + :headers => a_kind_of(HTTP::Headers), + :http_version => "0.0", + :keep_alive_timeout => 5.0, + :parser => a_kind_of(HTTP::Response::Parser), + :pending_request => false, + :pending_response => false, + :persistent => false, + :socket => socket, + :status_code => 0 + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case connection + in status_code: 0, pending_request: false + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/content_type_spec.rb b/spec/lib/http/content_type_spec.rb index 8175992c..b3c39112 100644 --- a/spec/lib/http/content_type_spec.rb +++ b/spec/lib/http/content_type_spec.rb @@ -44,4 +44,33 @@ its(:charset) { is_expected.to eq "utf-8" } end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Content Type" do + expect(described_class.new("text/plain", "utf-8").to_h).to include( + :charset => "utf-8", + :mime_type => "text/plain" + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case described_class.new('text/plain', 'utf-8') + in mime_type: /text/ + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/headers_spec.rb b/spec/lib/http/headers_spec.rb index abf62269..3c9bf215 100644 --- a/spec/lib/http/headers_spec.rb +++ b/spec/lib/http/headers_spec.rb @@ -523,5 +523,31 @@ it "is aliased as .[]" do expect(described_class.method(:coerce)).to eq described_class.method(:[]) end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "Pattern Matching" do + before do + headers.add :content_type, "application/json" + headers.add :set_cookie, "hoo=ray" + headers.add :set_cookie, "woo=hoo" + end + + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case headers + in content_type: /json/, set_cookie: [/hoo/, *] + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end end diff --git a/spec/lib/http/options_spec.rb b/spec/lib/http/options_spec.rb index 7c81df0c..6de7acd1 100644 --- a/spec/lib/http/options_spec.rb +++ b/spec/lib/http/options_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/BlockLength RSpec.describe HTTP::Options do subject { described_class.new(:response => :body) } @@ -10,4 +11,52 @@ it "coerces to a Hash" do expect(subject.to_hash).to be_a(Hash) end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_hash" do + it "returns a Hash representation of Options" do + expect(subject.to_hash).to include( + :body => nil, + :cookies => {}, + :encoding => nil, + :features => {}, + :follow => nil, + :form => nil, + :headers => an_instance_of(HTTP::Headers), + :json => nil, + :keep_alive_timeout => 5, + :nodelay => false, + :params => nil, + :persistent => nil, + :proxy => {}, + :response => :body, + :socket_class => TCPSocket, + :ssl => {}, + :ssl_context => nil, + :ssl_socket_class => OpenSSL::SSL::SSLSocket, + :timeout_class => HTTP::Timeout::Null, + :timeout_options => {} + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case subject + in keep_alive_timeout: 5..10 + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end +# rubocop:enable Metrics/BlockLength diff --git a/spec/lib/http/request/body_spec.rb b/spec/lib/http/request/body_spec.rb index e609e5ac..8dafb411 100644 --- a/spec/lib/http/request/body_spec.rb +++ b/spec/lib/http/request/body_spec.rb @@ -208,4 +208,33 @@ end end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Body" do + expect(subject.to_h).to include( + :source => "", + :size => 0 + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<~RUBY, __FILE__, __LINE__ + 1 + case subject + in size: 0 + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/request/writer_spec.rb b/spec/lib/http/request/writer_spec.rb index 95c00036..0bda8088 100644 --- a/spec/lib/http/request/writer_spec.rb +++ b/spec/lib/http/request/writer_spec.rb @@ -107,4 +107,35 @@ end end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Writer" do + expect(subject.to_h).to include( + :body => a_kind_of(HTTP::Request::Body), + :headers => a_kind_of(HTTP::Headers), + :request_header => ["GET /test HTTP/1.1"], + :socket => a_kind_of(StringIO) + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case subject + in request_header: [/GET .*/] + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/request_spec.rb b/spec/lib/http/request_spec.rb index da4ba074..74f0ddc0 100644 --- a/spec/lib/http/request_spec.rb +++ b/spec/lib/http/request_spec.rb @@ -231,4 +231,39 @@ it { is_expected.to eq "#" } end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Request" do + expect(request.to_h).to include( + :body => a_kind_of(HTTP::Request::Body), + :headers => a_kind_of(HTTP::Headers), + :port => 80, + :proxy => {}, + :scheme => :http, + :uri => a_kind_of(HTTP::URI), + :verb => :get, + :version => "1.1" + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case request + in port: 80, verb: :get, version: /^1\\.\\d/ + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/response/body_spec.rb b/spec/lib/http/response/body_spec.rb index 663eae43..085088b1 100644 --- a/spec/lib/http/response/body_spec.rb +++ b/spec/lib/http/response/body_spec.rb @@ -82,4 +82,35 @@ end end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Body" do + expect(subject.to_h).to include( + :connection => connection, + :contents => nil, + :encoding => a_kind_of(Encoding), + :streaming => nil + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case subject + in contents: nil + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/response/status_spec.rb b/spec/lib/http/response/status_spec.rb index 6930ab97..9ae51c74 100644 --- a/spec/lib/http/response/status_spec.rb +++ b/spec/lib/http/response/status_spec.rb @@ -250,4 +250,37 @@ expect(described_class.method(:coerce)).to eq described_class.method :[] end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + subject { described_class.new("200.0") } + + it "returns a Hash representation of a Status" do + expect(subject.to_h).to include( + :code => 200, + :reason => "OK" + ) + end + end + + describe "Pattern Matching" do + subject { described_class.new("200.0") } + + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case subject + in code: 200..299 + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/response_spec.rb b/spec/lib/http/response_spec.rb index 398f7413..09191769 100644 --- a/spec/lib/http/response_spec.rb +++ b/spec/lib/http/response_spec.rb @@ -171,4 +171,51 @@ end it { is_expected.not_to be_chunked } end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a Response" do + expect(response.to_h).to include( + :body => "Hello world!", + :headers => a_kind_of(HTTP::Headers), + :proxy_headers => a_kind_of(HTTP::Headers), + :request => a_kind_of(HTTP::Request), + :status => a_kind_of(HTTP::Response::Status), + :version => "1.1" + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case response + in body: /Hello/, version: /^1\\.\\d/ + true + else + false + end + RUBY + + expect(value).to eq(true) + end + + it "can perform an array pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case response + in [200, *] + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end diff --git a/spec/lib/http/uri_spec.rb b/spec/lib/http/uri_spec.rb index 7e91760e..08ee21dd 100644 --- a/spec/lib/http/uri_spec.rb +++ b/spec/lib/http/uri_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Metrics/BlockLength RSpec.describe HTTP::URI do let(:example_http_uri_string) { "http://example.com" } let(:example_https_uri_string) { "https://example.com" } @@ -29,4 +30,40 @@ expect(http_uri.to_s).to eq("http://example.com") end end + + # Pattern Matching only exists in Ruby 2.7+, guard against execution of + # tests otherwise + if RUBY_VERSION >= "2.7" + describe "#to_h" do + it "returns a Hash representation of a URI" do + expect(http_uri.to_h).to include( + :fragment => nil, + :host => "example.com", + :password => nil, + :path => "", + :port => 80, + :query => nil, + :scheme => "http", + :user => nil + ) + end + end + + describe "Pattern Matching" do + it "can perform a pattern match" do + # Cursed hack to ignore syntax errors to test Pattern Matching. + value = instance_eval <<-RUBY, __FILE__, __LINE__ + 1 + case http_uri + in host: /example/, port: 50..100, scheme: 'http' + true + else + false + end + RUBY + + expect(value).to eq(true) + end + end + end end +# rubocop:enable Metrics/BlockLength