From f6b63b869c28f714842ef5b1f32f06a6baa7bc3f Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 18:22:05 -0800 Subject: [PATCH 1/4] Add pattern matching support to Net::HTTPResponse Implements deconstruct_keys to enable pattern matching on HTTP responses based on status code, content type, and other response attributes. Example: case response in code: '200', content_type: /json/ JSON.parse(response.body) in code: '404' handle_not_found end --- lib/net/http/response.rb | 30 ++++++++++++++++++++++++- test/net/http/test_httpresponse.rb | 35 ++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb index 3aeba2e5..688e739a 100644 --- a/lib/net/http/response.rb +++ b/lib/net/http/response.rb @@ -133,6 +133,9 @@ # there is a protocol error. # class Net::HTTPResponse + # Valid keys for pattern matching via #deconstruct_keys. + PATTERN_MATCHING_KEYS = %i[code message http_version body content_type].freeze + class << self # true if the response has a body. def body_permitted? @@ -232,7 +235,7 @@ def initialize(httpv, code, msg) #:nodoc: internal use only # # - If the given value is an Encoding object, that encoding will be used. # - Otherwise if the value is a string, the value of - # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find] + # {Encoding#find(value)}[rdoc-ref:Encoding.find] # will be used. # - Otherwise an encoding will be deduced from the body itself. # @@ -408,6 +411,31 @@ def body=(value) alias entity body #:nodoc: obsolete + # Returns a hash of response attributes for pattern matching. + # + # Valid keys are: +:code+, +:message+, +:http_version+, +:body+, +:content_type+ + # + # Example: + # + # response = Net::HTTP.get_response(uri) + # case response + # in code: '200', content_type: /json/ + # JSON.parse(response.body) + # in code: '404' + # handle_not_found + # end + # + def deconstruct_keys(keys) + valid_keys = keys ? PATTERN_MATCHING_KEYS & keys : PATTERN_MATCHING_KEYS + valid_keys.to_h do |key| + value = case key + when :body then @body + else public_send(key) + end + [key, value] + end + end + private # :nodoc: diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index 01281063..353401a9 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -744,6 +744,41 @@ def test_inspect_response assert_equal '#', res.inspect end + def test_deconstruct_keys + res = Net::HTTPOK.new('1.1', '200', 'OK') + res.body = 'test body' + res['content-type'] = 'text/plain' + + keys = res.deconstruct_keys(nil) + assert_equal '200', keys[:code] + assert_equal 'OK', keys[:message] + assert_equal '1.1', keys[:http_version] + assert_equal 'test body', keys[:body] + assert_equal 'text/plain', keys[:content_type] + end + + def test_deconstruct_keys_with_specific_keys + res = Net::HTTPOK.new('1.1', '200', 'OK') + res.body = 'test body' + + keys = res.deconstruct_keys([:code, :message]) + assert_equal({code: '200', message: 'OK'}, keys) + end + + def test_pattern_matching + res = Net::HTTPOK.new('1.1', '200', 'OK') + res['content-type'] = 'application/json' + + matched = case res + in code: '200', content_type: /json/ + true + else + false + end + + assert_equal true, matched + end + private def dummy_io(str) From d19272217fe6176807b7b13f735fe11be6e0267f Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 18:30:48 -0800 Subject: [PATCH 2/4] Add Ruby 2.6 compatibility for pattern matching tests --- test/net/http/test_httpresponse.rb | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index 353401a9..0c8e2501 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -769,14 +769,19 @@ def test_pattern_matching res = Net::HTTPOK.new('1.1', '200', 'OK') res['content-type'] = 'application/json' - matched = case res - in code: '200', content_type: /json/ - true - else - false + begin + matched = instance_eval <<~RUBY, __FILE__, __LINE__ + 1 + case res + in code: '200', content_type: /json/ + true + else + false + end + RUBY + assert_equal true, matched + rescue SyntaxError + skip "Pattern matching requires Ruby 2.7+" end - - assert_equal true, matched end private From e3444a3a3beac4d2af7151257c1edf9c9712c661 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 18:32:55 -0800 Subject: [PATCH 3/4] Revert unintended documentation change --- lib/net/http/response.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/net/http/response.rb b/lib/net/http/response.rb index 688e739a..4b524698 100644 --- a/lib/net/http/response.rb +++ b/lib/net/http/response.rb @@ -235,7 +235,7 @@ def initialize(httpv, code, msg) #:nodoc: internal use only # # - If the given value is an Encoding object, that encoding will be used. # - Otherwise if the value is a string, the value of - # {Encoding#find(value)}[rdoc-ref:Encoding.find] + # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find] # will be used. # - Otherwise an encoding will be deduced from the body itself. # From c2892185cf58dfa92a30627b2ec9e6dd9bac88b6 Mon Sep 17 00:00:00 2001 From: Brandon Weaver Date: Sat, 8 Nov 2025 18:37:17 -0800 Subject: [PATCH 4/4] Fix skip to omit for Test::Unit compatibility --- test/net/http/test_httpresponse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb index 0c8e2501..9d901390 100644 --- a/test/net/http/test_httpresponse.rb +++ b/test/net/http/test_httpresponse.rb @@ -780,7 +780,7 @@ def test_pattern_matching RUBY assert_equal true, matched rescue SyntaxError - skip "Pattern matching requires Ruby 2.7+" + omit "Pattern matching requires Ruby 2.7+" end end