diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c00cc2..976ec21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) # diff --git a/lib/addressfinder.rb b/lib/addressfinder.rb index eed628f..81fd125 100644 --- a/lib/addressfinder.rb +++ b/lib/addressfinder.rb @@ -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" @@ -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 diff --git a/lib/addressfinder/v1/phone/batch_verification.rb b/lib/addressfinder/v1/phone/batch_verification.rb new file mode 100644 index 0000000..5b1b6fc --- /dev/null +++ b/lib/addressfinder/v1/phone/batch_verification.rb @@ -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] 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 diff --git a/spec/lib/addressfinder/v1/phone/batch_verification_spec.rb b/spec/lib/addressfinder/v1/phone/batch_verification_spec.rb new file mode 100644 index 0000000..e5b56c6 --- /dev/null +++ b/spec/lib/addressfinder/v1/phone/batch_verification_spec.rb @@ -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