Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# Unreleased (September 2025) #

* Add CGI as dependency for Ruby 3.5+
* Add batch capability to Phone Numbers verification

# Addressfinder 1.15.0 (March 2025) #

* Automatically skip empty strings within Batch verification
* Mark unverified addressses as false within Batch verification
* Mark unverified addresses as false within Batch verification

# Addressfinder 1.14.0 (February 2025) #

Expand Down
5 changes: 5 additions & 0 deletions lib/addressfinder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require "addressfinder/v1/email/verification"
require "addressfinder/v1/email/batch_verification"
require "addressfinder/v1/phone/verification"
require "addressfinder/v1/phone/batch_verification"
require "addressfinder/errors"
require "addressfinder/util"
require "addressfinder/http"
Expand Down Expand Up @@ -96,6 +97,10 @@ def phone_verification(args = {})
AddressFinder::V1::Phone::Verification.new(**args.merge(http: AddressFinder::HTTP.new(configuration))).perform.result
end

def phone_verification_batch(args = {})
AddressFinder::V1::Phone::BatchVerification.new(**args.merge(http: AddressFinder::HTTP.new(configuration))).perform.results
end

def bulk(&block)
AddressFinder::Bulk.new(
http: AddressFinder::HTTP.new(configuration), verification_version: configuration.verification_version, default_country: configuration.default_country, &block
Expand Down
72 changes: 72 additions & 0 deletions lib/addressfinder/v1/phone/batch_verification.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module AddressFinder
module V1
module Phone
class BatchVerification
attr_reader :phone_numbers, :results

# Verifies an array of phone numbers using concurrency to reduce the execution time.
# The results of the verification are stored in the `results` attribute, in the same order
# in which they were supplied.
#
# @param [Array<String>] phone_numbers
# @param [String] default_country_code
# @param [AddressFinder::HTTP] http HTTP connection helper
# @param [Integer] concurrency How many threads to use for verification
# @param [Hash] args Any additional arguments that will be passed onto the EV API
def initialize(phone_numbers:, default_country_code:, http:, concurrency: 5, **args)
@phone_numbers = phone_numbers
@concurrency = concurrency
@default_country_code = default_country_code
@http = http
@args = args
end

def perform
confirm_concurrency_level
verify_each_phone_number_concurrently

self
end

private

attr_reader :args, :concurrency, :http, :default_country_code

MAX_CONCURRENCY_LEVEL = 10

def confirm_concurrency_level
return unless @concurrency > MAX_CONCURRENCY_LEVEL

warn "WARNING: Concurrency level of #{@concurrency} is higher than the maximum of #{MAX_CONCURRENCY_LEVEL}. Using #{MAX_CONCURRENCY_LEVEL}."
@concurrency = MAX_CONCURRENCY_LEVEL
end

def verify_each_phone_number_concurrently
@results = Concurrent::Array.new(phone_numbers.length)

pool = Concurrent::FixedThreadPool.new(concurrency)

@phone_numbers.each_with_index do |phone_number, index_of_phone_number|
# Start a new thread for each task
pool.post do
@results[index_of_phone_number] = verify_phone_number(phone_number)
end
end

## Shutdown the pool and wait for all tasks to complete
pool.shutdown
pool.wait_for_termination
end

# Verifies a single phone number, and writes the result into @results
def verify_phone_number(phone_number)
return if phone_number.empty?

AddressFinder::V1::Phone::Verification.new(phone_number: phone_number, default_country_code: default_country_code, http: http.clone, **args).perform.result
rescue AddressFinder::RequestRejectedError => e
OpenStruct.new(success: false, body: e.body, status: e.status)
end
end
end
end
end
71 changes: 71 additions & 0 deletions spec/lib/addressfinder/v1/phone/batch_verification_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
require "spec_helper"
require "cgi"

RSpec.describe AddressFinder::V1::Phone::BatchVerification do
let(:http) {
AddressFinder::HTTP.new(AddressFinder.configuration)
}

let(:phone_numbers) { ["0424980072", "02 9098 8273", "+61414421799"] }
before do
AddressFinder.configure do |af|
af.api_key = "XXX"
af.api_secret = "YYY"
af.timeout = 5
af.retries = 3
end

stub_request(:get, /\Ahttps:\/\/api\.addressfinder\.io\/api\/phone\/v1\/verification/)
.to_return do |request|
uri = URI.parse(request.uri)
params = CGI.parse(uri.query)
phone_number = params["phone_number"].first

# returns a JSON string with the requested phone number embedded
{
body: verified_response(phone_number), status: 200
}
end
end

describe "when operating concurrently" do
subject(:results) do
AddressFinder::V1::Phone::BatchVerification.new(phone_numbers: phone_numbers, default_country_code: "AU", concurrency: 3, http: http).perform.results
end

it "has 3 results" do
expect(results.size).to eq(3)
end

it "contains the results in the expected order" do
expect(results.collect(&:raw_national)).to eq(phone_numbers)
end

it "returns records of type Result" do
expect(results.collect(&:class).uniq).to eq([AddressFinder::V1::Base::Result])
end
end

describe "with an excessive concurrency level" do
it "writes a warning message" do
verifier = AddressFinder::V1::Phone::BatchVerification.new(phone_numbers: phone_numbers, default_country_code: "AU", concurrency: 100, http: http)
expect(verifier).to receive(:warn).with("WARNING: Concurrency level of 100 is higher than the maximum of 10. Using 10.")
verifier.perform
end
end

def verified_response(phone_number)
%({
"is_verified": true,
"line_type": "mobile",
"line_status": "connected",
"line_status_reason": null,
"country_code": "AU",
"calling_code": "61",
"raw_national": "#{phone_number}",
"not_verified_code": null,
"not_verified_reason": null,
"success": true
})
end
end