From 4488a49292e16f6409ee896489a37acd2753f949 Mon Sep 17 00:00:00 2001 From: lineoffligbot <4628864+lineoffligbot@users.noreply.github.com> Date: Sun, 16 Aug 2026 15:25:59 +0200 Subject: [PATCH] Add coerce method to Status for integer comparison Integer#<=> requires the numeric coercion protocol (#coerce) to compare with custom numeric types. Status lacked #coerce, so `500 <=> status` returned nil and prevented Range#=== from matching, causing pattern matching like `case response in status: 500..599` to fail silently. This adds #coerce returning [other, code]. Co-authored-by: Hakan Ensari --- CHANGELOG.md | 6 ++++++ lib/http/response/status.rb | 16 ++++++++++++++++ sig/http.rbs | 1 + test/http/response/status_test.rb | 19 +++++++++++++++++++ test/http/response_test.rb | 12 ++++++++++++ 5 files changed, 54 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954aa1ae..202e574b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 JVM could garbage-collect llhttp's native callback trampolines while a parser was still in use, after which `llhttp_execute` succeeded without invoking any callbacks. Callback procs are now retained for the lifetime of the parser. +- `Response::Status` now implements the numeric coercion protocol (instance + `#coerce`), so range patterns (`case response in status: 500..599`) and + Integer-first comparisons (`500 <=> status`) work. Previously `Integer#<=>` + returned nil for `Status` operands, so range patterns silently never + matched. ([#847]) ## [6.0.4] - 2026-07-14 @@ -330,6 +335,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#785]: https://github.com/httprb/http/issues/785 [#826]: https://github.com/httprb/http/issues/826 [#841]: https://github.com/httprb/http/pull/841 +[#847]: https://github.com/httprb/http/pull/847 [unreleased]: https://github.com/httprb/http/compare/v6.0.4...HEAD [6.0.4]: https://github.com/httprb/http/compare/v6.0.3...v6.0.4 [6.0.3]: https://github.com/httprb/http/compare/v6.0.2...v6.0.3 diff --git a/lib/http/response/status.rb b/lib/http/response/status.rb index 441102f5..370d5165 100644 --- a/lib/http/response/status.rb +++ b/lib/http/response/status.rb @@ -120,6 +120,22 @@ def <=>(other) code <=> other.to_i end + # Numeric coercion protocol, letting Integer compare against Status + # + # Makes `Integer#<=>` (and thus `Range#===`) work with a Status, + # so range patterns like `in status: 500..599` match. + # + # @example + # 500 <=> Status.new(503) # => -1 + # (500..599) === Status.new(503) # => true + # + # @param [Numeric] other + # @return [Array(Numeric, Integer)] + # @api public + def coerce(other) + [other, code] + end + # Hash value based on status code # # @example diff --git a/sig/http.rbs b/sig/http.rbs index 9b1c4a69..ed26af4e 100644 --- a/sig/http.rbs +++ b/sig/http.rbs @@ -1007,6 +1007,7 @@ module HTTP def to_i: () -> Integer def to_int: () -> Integer def <=>: (Numeric | Status other) -> Integer? + def coerce: (Numeric other) -> [Numeric, Integer] def hash: () -> Integer def reason: () -> String? def to_s: () -> String diff --git a/test/http/response/status_test.rb b/test/http/response/status_test.rb index 2f6b9009..57eec84a 100644 --- a/test/http/response/status_test.rb +++ b/test/http/response/status_test.rb @@ -179,6 +179,25 @@ def test_spaceship_compares_with_objects_that_respond_to_to_i_but_not_to_int assert_equal 1, HTTP::Response::Status.new(200) <=> "abc" end + # --------------------------------------------------------------------------- + # #coerce + # --------------------------------------------------------------------------- + def test_coerce_returns_pair_comparable_with_integers + assert_equal [500, 503], HTTP::Response::Status.new(503).coerce(500) + end + + def test_integer_spaceship_compares_with_status + assert_equal(-1, 500 <=> HTTP::Response::Status.new(503)) + end + + def test_range_case_equality_matches_status_within_range + assert_operator 500..599, :===, HTTP::Response::Status.new(503) + end + + def test_range_case_equality_does_not_match_status_outside_range + refute_operator 500..599, :===, HTTP::Response::Status.new(404) + end + # --------------------------------------------------------------------------- # #== # --------------------------------------------------------------------------- diff --git a/test/http/response_test.rb b/test/http/response_test.rb index 14f72c7c..adfb5d4c 100644 --- a/test/http/response_test.rb +++ b/test/http/response_test.rb @@ -107,6 +107,18 @@ def test_deconstruct_keys_supports_hash_pattern_matching assert matched end + def test_deconstruct_keys_supports_range_pattern_on_status + response = build_response(status: 503) + matched = case response + in { status: 500..599 } + true + else + false + end + + assert matched + end + # --------------------------------------------------------------------------- # #deconstruct # ---------------------------------------------------------------------------