Skip to content

Commit

Permalink
✨ Add #extract_responses method
Browse files Browse the repository at this point in the history
This is similar to ActiveSupport's Array#extract!.

Yields all of the unhandled #responses for a single response type.
Removes and returns the responses for which the block returns a true
value.

Extracting responses is synchronized with other threads.  The lock is
released before returning.
  • Loading branch information
nevans committed Sep 25, 2024
1 parent e8b1193 commit 2a7be9e
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 4 deletions.
30 changes: 28 additions & 2 deletions lib/net/imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ module Net
# pre-authenticated connection.
# - #responses: Yields unhandled UntaggedResponse#data and <em>non-+nil+</em>
# ResponseCode#data.
# - #extract_responses: Removes and returns the responses for which the block
# returns a true value.
# - #clear_responses: Deletes unhandled data from #responses and returns it.
# - #add_response_handler: Add a block to be called inside the receiver thread
# with every server response.
Expand Down Expand Up @@ -2534,7 +2536,7 @@ def idle_done
# return the TaggedResponse directly, #add_response_handler must be used to
# handle all response codes.
#
# Related: #clear_responses, #response_handlers, #greeting
# Related: #extract_responses, #clear_responses, #response_handlers, #greeting
def responses(type = nil)
if block_given?
synchronize { yield(type ? @responses[type.to_s.upcase] : @responses) }
Expand Down Expand Up @@ -2562,7 +2564,7 @@ def responses(type = nil)
# Clearing responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #response_handlers
# Related: #extract_responses, #responses, #response_handlers
def clear_responses(type = nil)
synchronize {
if type
Expand All @@ -2576,6 +2578,30 @@ def clear_responses(type = nil)
.freeze
end

# :call-seq:
# extract_responses(type) {|response| ... } -> array
#
# Yields all of the unhandled #responses for a single response +type+.
# Removes and returns the responses for which the block returns a true
# value.
#
# Extracting responses is synchronized with other threads. The lock is
# released before returning.
#
# Related: #responses, #clear_responses
def extract_responses(type)
type = String.try_convert(type) or
raise ArgumentError, "type must be a string"
raise ArgumentError, "must provide a block" unless block_given?
extracted = []
responses(type) do |all|
all.reject! do |response|
extracted << response if yield response
end
end
extracted
end

# Returns all response handlers, including those that are added internally
# by commands. Each response handler will be called with every new
# UntaggedResponse, TaggedResponse, and ContinuationRequest.
Expand Down
47 changes: 45 additions & 2 deletions test/net/imap/test_imap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1110,7 +1110,7 @@ def test_enable
end
end

def test_responses
test "#responses" do
with_fake_server do |server, imap|
# responses available before SELECT/EXAMINE
assert_equal(%w[IMAP4REV1 NAMESPACE MOVE IDLE UTF8=ACCEPT],
Expand Down Expand Up @@ -1144,7 +1144,7 @@ def test_responses
end
end

def test_clear_responses
test "#clear_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
Expand All @@ -1168,6 +1168,49 @@ def test_clear_responses
end
end

test "#extract_responses" do
with_fake_server do |server, imap|
resp = imap.select "INBOX"
assert_equal([Net::IMAP::TaggedResponse, "RUBY0001", "OK"],
[resp.class, resp.tag, resp.name])
# Need to send a string type and a block
assert_raise(ArgumentError) do imap.extract_responses { true } end
assert_raise(ArgumentError) do imap.extract_responses(nil) { true } end
assert_raise(ArgumentError) do imap.extract_responses("OK") end
# matching nothing
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([], imap.extract_responses("EXISTS") { String === _1 })
assert_equal([172], imap.responses("EXISTS", &:dup))
# matching everything
assert_equal([172], imap.responses("EXISTS", &:dup))
assert_equal([172], imap.extract_responses("EXISTS", &:even?))
assert_equal([], imap.responses("EXISTS", &:dup))
# matching some
server.unsolicited("101 FETCH (UID 1111 FLAGS (\\Seen))")
server.unsolicited("102 FETCH (UID 2222 FLAGS (\\Seen \\Flagged))")
server.unsolicited("103 FETCH (UID 3333 FLAGS (\\Deleted))")
wait_for_response_count(imap, type: "FETCH", count: 3)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Flagged) }
assert_equal(
[
Net::IMAP::FetchData.new(
102, {"UID" => 2222, "FLAGS" => [:Seen, :Flagged]}
),
],
result,
)
assert_equal 2, imap.responses("FETCH", &:count)

result = imap.extract_responses("FETCH") { _1.flags.include?(:Deleted) }
assert_equal(
[Net::IMAP::FetchData.new(103, {"UID" => 3333, "FLAGS" => [:Deleted]})],
result
)
assert_equal 1, imap.responses("FETCH", &:count)
end
end

test "#select with condstore" do
with_fake_server do |server, imap|
imap.select "inbox", condstore: true
Expand Down

0 comments on commit 2a7be9e

Please sign in to comment.