diff --git a/app/services/arcgis_api/geocoder.rb b/app/services/arcgis_api/geocoder.rb index 7b14b5775a3..dfc80c56a9c 100644 --- a/app/services/arcgis_api/geocoder.rb +++ b/app/services/arcgis_api/geocoder.rb @@ -1,8 +1,23 @@ module ArcgisApi class Geocoder Suggestion = Struct.new(:text, :magic_key, keyword_init: true) + AddressCandidate = Struct.new( + :address, :location, :street_address, :city, :state, :zip_code, + keyword_init: true + ) + Location = Struct.new(:latitude, :longitude, keyword_init: true) - # Makes HTTP request to get potential address matches + # These are option URL params that tend to apply to multiple endpoints + # https://developers.arcgis.com/rest/geocode/api-reference/geocoding-find-address-candidates.htm#ESRI_SECTION2_38613C3FCB12462CAADD55B2905140BF + COMMON_DEFAULT_PARAMETERS = { + f: 'json', + countryCode: 'USA', + category: 'address', + }.freeze + + # Makes an HTTP request to quickly find potential address matches. Each match that is found + # will include an associated magic_key value which can later be used to get more details about + # the address using the #find_address_candidates method # Requests text input and will only match possible addresses # A maximum of 5 suggestions are included in the suggestions array. # @param text [String] @@ -11,9 +26,7 @@ def suggest(text) url = "#{root_url}/suggest" params = { text: text, - category: 'address', - countryCode: 'USA', - f: 'json', + **COMMON_DEFAULT_PARAMETERS, } parse_suggestions( @@ -23,6 +36,24 @@ def suggest(text) ) end + # Makes HTTP request to find an exact address using magic_key + # @param magic_key [String] a magic key value from a previous call to the #suggest method + # @return [Array] AddressCandidates + def find_address_candidates(magic_key) + url = "#{root_url}/findAddressCandidates" + params = { + magicKey: magic_key, + outFields: 'StAddr,City,RegionAbbr,Postal', + **COMMON_DEFAULT_PARAMETERS, + } + + parse_address_candidates( + faraday.get(url, params) do |req| + req.options.context = { service_name: 'arcgis_geocoder_find_address_candidates' } + end.body, + ) + end + private def root_url @@ -48,6 +79,39 @@ def request_headers end def parse_suggestions(response_body) + handle_api_errors(response_body) + + response_body['suggestions'].map do |suggestion| + Suggestion.new( + text: suggestion['text'], + magic_key: suggestion['magicKey'], + ) + end + end + + def parse_address_candidates(response_body) + handle_api_errors(response_body) + + response_body['candidates'].map do |candidate| + AddressCandidate.new( + address: candidate['address'], + location: Location.new( + longitude: candidate.dig('location', 'x'), + latitude: candidate.dig( + 'location', 'y' + ), + ), + street_address: candidate.dig('attributes', 'StAddr'), + city: candidate.dig('attributes', 'City'), + state: candidate.dig('attributes', 'RegionAbbr'), + zip_code: candidate.dig('attributes', 'Postal'), + ) + end + end + + # handles API error state when returned as a status of 200 + # @param response_body [Hash] + def handle_api_errors(response_body) if response_body['error'] error_code = response_body.dig('error', 'code') @@ -56,13 +120,6 @@ def parse_suggestions(response_body) response_body, ) end - - response_body['suggestions'].map do |suggestion| - Suggestion.new( - text: suggestion['text'], - magic_key: suggestion['magicKey'], - ) - end end end end diff --git a/spec/fixtures/arcgis_api_responses/request_candidates_error.json b/spec/fixtures/arcgis_api_responses/request_candidates_error.json new file mode 100644 index 00000000000..04a5a47d0c7 --- /dev/null +++ b/spec/fixtures/arcgis_api_responses/request_candidates_error.json @@ -0,0 +1,10 @@ +{ + "error": { + "code": 400, + "extendedCode": -2147467259, + "message": "Unable to complete operation.", + "details": [ + "Something went wrong" + ] + } +} diff --git a/spec/fixtures/arcgis_api_responses/request_candidates_response.json b/spec/fixtures/arcgis_api_responses/request_candidates_response.json new file mode 100644 index 00000000000..8d71536f9fd --- /dev/null +++ b/spec/fixtures/arcgis_api_responses/request_candidates_response.json @@ -0,0 +1,28 @@ +{ + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + }, + "candidates": [ + { + "address": "100 Main Ave, La Grande, Oregon, 97850", + "location": { + "x": -118.10754025791812, + "y": 45.328271485226445 + }, + "score": 100, + "attributes": { + "StAddr": "100 Main Ave", + "City": "La Grande", + "RegionAbbr": "OR", + "Postal": "97850" + }, + "extent": { + "xmin": -118.10854025791812, + "ymin": 45.327271485226447, + "xmax": -118.10654025791811, + "ymax": 45.329271485226442 + } + } + ] +} diff --git a/spec/fixtures/arcgis_api_responses/request_candidates_response_empty.json b/spec/fixtures/arcgis_api_responses/request_candidates_response_empty.json new file mode 100644 index 00000000000..465d033f5fe --- /dev/null +++ b/spec/fixtures/arcgis_api_responses/request_candidates_response_empty.json @@ -0,0 +1,8 @@ +{ + "spatialReference": { + "wkid": 4326, + "latestWkid": 4326 + }, + "candidates": [ + ] +} diff --git a/spec/services/arcgis_api/geocoder_spec.rb b/spec/services/arcgis_api/geocoder_spec.rb index 258ea113dd0..9b90402563d 100644 --- a/spec/services/arcgis_api/geocoder_spec.rb +++ b/spec/services/arcgis_api/geocoder_spec.rb @@ -33,4 +33,42 @@ ) end end + + describe '#find_address_candidates' do + it 'returns candidates from magic_key' do + stub_request_candidates_response + + suggestions = subject.find_address_candidates('abc123') + + expect(suggestions.first.as_json).to eq( + { + 'address' => '100 Main Ave, La Grande, Oregon, 97850', + 'location' => { 'longitude' => -118.10754025791812, 'latitude' => 45.328271485226445 }, + 'street_address' => '100 Main Ave', + 'city' => 'La Grande', + 'state' => 'OR', + 'zip_code' => '97850', + }, + ) + end + + # https://developers.arcgis.com/rest/geocode/api-reference/geocoding-service-output.htm#ESRI_SECTION3_619341BEAA3A4F488FC66FAE8E479563 + it 'handles no results' do + stub_request_candidates_empty_response + + suggestions = subject.find_address_candidates('abc123') + + expect(suggestions).to be_empty + end + + it 'returns an error response body but with Status coded as 200' do + stub_request_candidates_error + + expect { subject.find_address_candidates('abc123') }.to raise_error do |error| + expect(error).to be_instance_of(Faraday::ClientError) + expect(error.message).to eq('received error code 400') + expect(error.response).to be_kind_of(Hash) + end + end + end end diff --git a/spec/support/arcgis_api_fixtures.rb b/spec/support/arcgis_api_fixtures.rb index ea58a24bc3d..83caed6519c 100644 --- a/spec/support/arcgis_api_fixtures.rb +++ b/spec/support/arcgis_api_fixtures.rb @@ -13,6 +13,18 @@ def self.request_suggestions_error_html load_response_fixture('request_suggestions_error.html') end + def self.request_candidates_response + load_response_fixture('request_candidates_response.json') + end + + def self.request_candidates_empty_response + load_response_fixture('request_candidates_response_empty.json') + end + + def self.request_candidates_error + load_response_fixture('request_candidates_error.json') + end + def self.load_response_fixture(filename) path = File.join( File.dirname(__FILE__), diff --git a/spec/support/arcgis_api_helper.rb b/spec/support/arcgis_api_helper.rb index 431454dbb11..44859656c7a 100644 --- a/spec/support/arcgis_api_helper.rb +++ b/spec/support/arcgis_api_helper.rb @@ -18,4 +18,25 @@ def stub_request_suggestions_error_html status: 400, body: ArcgisApi::Mock::Fixtures.request_suggestions_error_html, ) end + + def stub_request_candidates_response + stub_request(:get, %r{/findAddressCandidates}).to_return( + status: 200, body: ArcgisApi::Mock::Fixtures.request_candidates_response, + headers: { content_type: 'application/json;charset=UTF-8' } + ) + end + + def stub_request_candidates_empty_response + stub_request(:get, %r{/findAddressCandidates}).to_return( + status: 200, body: ArcgisApi::Mock::Fixtures.request_candidates_empty_response, + headers: { content_type: 'application/json;charset=UTF-8' } + ) + end + + def stub_request_candidates_error + stub_request(:get, %r{/findAddressCandidates}).to_return( + status: 200, body: ArcgisApi::Mock::Fixtures.request_candidates_error, + headers: { content_type: 'application/json;charset=UTF-8' } + ) + end end