address_geocoder 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.rdoc +72 -0
- data/Rakefile +4 -0
- data/address_geocoder.gemspec +26 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/countries.yaml +1000 -0
- data/lib/address_geocoder.rb +11 -0
- data/lib/address_geocoder/client.rb +180 -0
- data/lib/address_geocoder/error.rb +31 -0
- data/lib/address_geocoder/parser.rb +24 -0
- data/lib/address_geocoder/requester.rb +52 -0
- data/lib/address_geocoder/url_generator.rb +43 -0
- data/lib/address_geocoder/version.rb +3 -0
- data/lib/maps_api.rb +7 -0
- data/lib/maps_api/google.rb +9 -0
- data/lib/maps_api/google/client.rb +19 -0
- data/lib/maps_api/google/parser.rb +121 -0
- data/lib/maps_api/google/requester.rb +70 -0
- data/lib/maps_api/google/url_generator.rb +94 -0
- metadata +88 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'maps_api'
|
3
|
+
|
4
|
+
# Namespace for classes and modules directly relating to the gem
|
5
|
+
# @since 0.0.1
|
6
|
+
module AddressGeocoder
|
7
|
+
# The collection of countries supported by this gem
|
8
|
+
COUNTRIES = YAML.load_file('countries.yaml')
|
9
|
+
# The regex used to check the state and city for validity
|
10
|
+
REGEX = /\A[a-zA-Z\ ]*\z/
|
11
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
require 'address_geocoder/error'
|
2
|
+
|
3
|
+
module AddressGeocoder
|
4
|
+
# @abstract Abstract base class for interacting with maps APIs
|
5
|
+
class Client
|
6
|
+
# @!attribute api_key
|
7
|
+
# @return [String] the user's key to the chosen maps API
|
8
|
+
attr_accessor :api_key
|
9
|
+
# @!attribute language
|
10
|
+
# @return [String] the language in which to return the address
|
11
|
+
attr_accessor :language
|
12
|
+
# @!attribute [r] address
|
13
|
+
# @return [Hash] our address object. It contains country, state, city,
|
14
|
+
# postal code, and street.
|
15
|
+
attr_reader :address
|
16
|
+
# @!attribute [r] response
|
17
|
+
# @return [Hash] the response from the maps API
|
18
|
+
attr_reader :response
|
19
|
+
# @!attribute [r] former_address
|
20
|
+
# @return [Hash] the address that was last called from the maps API
|
21
|
+
attr_reader :former_address
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
@address = {}
|
25
|
+
assign_initial(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Determines whether an address is likely to be valid or not
|
29
|
+
# @return [Boolean] true, or false if address is likely to be invalid.
|
30
|
+
# @todo .certain? should be a parser method
|
31
|
+
def valid_address?
|
32
|
+
check_country
|
33
|
+
if values_changed?
|
34
|
+
reset
|
35
|
+
@requester.make_call
|
36
|
+
end
|
37
|
+
@requester.success? && @requester.certain?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Gathers a list of matching addresses from the maps API
|
41
|
+
# @return [Array<Hash>] a list of matching addresses
|
42
|
+
def suggested_addresses
|
43
|
+
check_country
|
44
|
+
if values_changed?
|
45
|
+
reset
|
46
|
+
@requester.make_call
|
47
|
+
end
|
48
|
+
return false unless @requester.success?
|
49
|
+
@requester.array_result.map do |result|
|
50
|
+
@parser.fields = result
|
51
|
+
@parser.parse_response
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @abstract Assigns the entered variables to their proper instance variables
|
56
|
+
# @param args [Hash] arguments to pass to the class
|
57
|
+
# @option args [String] :country a country's alpha2
|
58
|
+
# @option args [String] :api_key the user's key to the chosen maps API
|
59
|
+
# @option args [String] :state the state of the address to be validated
|
60
|
+
# @option args [String] :city the city of the address to be validated
|
61
|
+
# @option args [String] :postal_code the postal code of the address to be
|
62
|
+
# validated
|
63
|
+
# @option args [String] :street the street of the address to be validated
|
64
|
+
# @option args [String] :language (en) the language in which to return the
|
65
|
+
# address
|
66
|
+
# @return [void]
|
67
|
+
def assign_initial(args)
|
68
|
+
raise NeedToOveride, 'assign_initial' unless @requester && @parser
|
69
|
+
Client.instance_methods(false).each do |var|
|
70
|
+
next if var.to_s[/\=/].nil?
|
71
|
+
value = args[var.to_s.tr('=', '').to_sym].to_s
|
72
|
+
next unless value
|
73
|
+
send(var, value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Matches the given alpha2 to a yaml country and assigns it to the address
|
78
|
+
# object
|
79
|
+
# @param str [String] a country's alpha2
|
80
|
+
# @return [Hash, nil] a country object from the yaml, or nil if the provided
|
81
|
+
# alpha2 could not be matched.
|
82
|
+
def country=(str)
|
83
|
+
if COUNTRIES[str]
|
84
|
+
@address[:country] = COUNTRIES[str]
|
85
|
+
@address[:country][:alpha2] = str
|
86
|
+
else
|
87
|
+
@address[:country] = nil
|
88
|
+
end
|
89
|
+
@address[:country]
|
90
|
+
end
|
91
|
+
|
92
|
+
# Assigns the given state to the address object if it passes verification
|
93
|
+
# @param str [String] a state name
|
94
|
+
# @return [String, nil] the entered state, or nil if the provided string
|
95
|
+
# could not be verified.
|
96
|
+
def state=(str)
|
97
|
+
@address[:state] = simple_check_and_assign!(str)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Assigns the given city to the address object if it passes verification
|
101
|
+
# @param str [String] a city name
|
102
|
+
# @return [String, nil] the entered city, or nil if the provided string
|
103
|
+
# could not be verified.
|
104
|
+
def city=(str)
|
105
|
+
@address[:city] = simple_check_and_assign!(str)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Assigns the given postal code to the address object if it passes
|
109
|
+
# verification
|
110
|
+
# @param str [String] a postal code
|
111
|
+
# @return [String, nil] the entered postal code, or nil if the provided
|
112
|
+
# string could not be verified.
|
113
|
+
def postal_code=(str)
|
114
|
+
@address[:postal_code] = pc_check_and_assign!(str)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Assigns the given street to the address object if it passes
|
118
|
+
# verification
|
119
|
+
# @param str [String] a street
|
120
|
+
# @return [String, nil] the entered street, or nil if the provided
|
121
|
+
# string was empty.
|
122
|
+
def street=(str)
|
123
|
+
@address[:street] = nil
|
124
|
+
@address[:street] = str unless str.empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
|
129
|
+
# Determines whether the given alpha2 exists in the countries yaml
|
130
|
+
# @raise [ArgumentError] if the given value is not an alpha2 or does not
|
131
|
+
# match any country in the yaml
|
132
|
+
# @return [void]
|
133
|
+
def check_country
|
134
|
+
raise ArgumentError, 'Invalid country' unless @address[:country]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Resets the former address to new data
|
138
|
+
# @return [void]
|
139
|
+
def reset
|
140
|
+
@former_address = @address
|
141
|
+
@parser.address = @address
|
142
|
+
@requester.address = @address
|
143
|
+
@requester.language = @language
|
144
|
+
@requester.api_key = @api_key
|
145
|
+
end
|
146
|
+
|
147
|
+
# Determines whether the inputted address values have changed in any way
|
148
|
+
# @return [Boolean] true, or false if nothing has been called or the
|
149
|
+
# current address information does not match the information from when the
|
150
|
+
# maps API was last called
|
151
|
+
def values_changed?
|
152
|
+
return true unless @response
|
153
|
+
@address != @former_address
|
154
|
+
end
|
155
|
+
|
156
|
+
# Determines whether the given city/state is valid or not
|
157
|
+
# @param var [String] a city/state name
|
158
|
+
# @return [String, nil] the given city/state, or false if it does not pass
|
159
|
+
# the Regex
|
160
|
+
def simple_check_and_assign!(var)
|
161
|
+
var if var.to_s[REGEX] != ''
|
162
|
+
end
|
163
|
+
|
164
|
+
# Determines whether the given postal code is valid or not
|
165
|
+
# @note validations include checking length, whether or not the given
|
166
|
+
# country has a postal code, and checking to make sure the postal code is
|
167
|
+
# not all one letter or all 0.
|
168
|
+
# @param postal_code [String, Integer] a postal code
|
169
|
+
# @return [String, nil] the given postal code as a string, or nil if it is
|
170
|
+
# not valid.
|
171
|
+
def pc_check_and_assign!(postal_code)
|
172
|
+
pc = postal_code.to_s.tr(' ', '')
|
173
|
+
return nil unless @address.fetch(:country).fetch(:has_postal_code)
|
174
|
+
return nil if pc.length < 3
|
175
|
+
all_one_char = pc.delete(pc[0]) == ''
|
176
|
+
return nil if all_one_char && !(pc[0].to_i.in? Array(1..9))
|
177
|
+
pc
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module AddressGeocoder
|
2
|
+
# @abstract Abstract base class for errors
|
3
|
+
class Error < RuntimeError
|
4
|
+
end
|
5
|
+
|
6
|
+
# Class that defines an error, to be thrown when a method needs to be
|
7
|
+
# overwritten by a child class.
|
8
|
+
class NeedToOveride < Error
|
9
|
+
def initialize(msg = nil)
|
10
|
+
@msg = msg
|
11
|
+
end
|
12
|
+
|
13
|
+
def message
|
14
|
+
msg = 'This Method Needs To Be Overrided'
|
15
|
+
@msg ? "#{msg}: #{@msg}" : msg
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Class that defines an error representing a failure in connection with a
|
20
|
+
# third party Maps API
|
21
|
+
class ConnectionError < Error
|
22
|
+
def initialize(msg = nil)
|
23
|
+
@msg = msg
|
24
|
+
end
|
25
|
+
|
26
|
+
def message
|
27
|
+
msg = 'Failed To Connect'
|
28
|
+
@msg ? "#{msg}: #{@msg}" : msg
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'address_geocoder/error'
|
2
|
+
|
3
|
+
module AddressGeocoder
|
4
|
+
# @abstract Abstract base class for parsing maps API responses
|
5
|
+
class Parser
|
6
|
+
# @!attribute [w] address
|
7
|
+
# @return [Hash] an address object
|
8
|
+
attr_writer :address
|
9
|
+
# @!attribute [w] fields
|
10
|
+
# @return [Hash] a maps API response
|
11
|
+
attr_writer :fields
|
12
|
+
|
13
|
+
def initialize(args = {})
|
14
|
+
@address = args[:address]
|
15
|
+
@fields = args[:fields]
|
16
|
+
end
|
17
|
+
|
18
|
+
# @abstract Abstract base method for parsing maps API responses
|
19
|
+
# @return (see AddressGeocoder::Client#suggested_addresses)
|
20
|
+
def parse_response
|
21
|
+
raise NeedToOveride, 'parse_response'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'address_geocoder/error'
|
3
|
+
|
4
|
+
module AddressGeocoder
|
5
|
+
# @abstract Abstract base class for making requests to maps APIs
|
6
|
+
class Requester
|
7
|
+
# @!attribute [w] parser
|
8
|
+
# @return [Parser] a class instance
|
9
|
+
attr_writer :parser
|
10
|
+
# @!attribute [w] address
|
11
|
+
# @return [Hash] the address to use in the request
|
12
|
+
attr_writer :address
|
13
|
+
# @!attribute [w] language
|
14
|
+
# @return [Hash] the language to return the request in
|
15
|
+
attr_writer :language
|
16
|
+
# @!attribute [w] api_key
|
17
|
+
# @return [Hash] the api_key to use in the request
|
18
|
+
attr_writer :api_key
|
19
|
+
# @!attribute [r] result
|
20
|
+
# @return [Hash] the result of a request to a maps API
|
21
|
+
attr_reader :result
|
22
|
+
|
23
|
+
def initialize(args = {})
|
24
|
+
@parser = args[:parser]
|
25
|
+
end
|
26
|
+
|
27
|
+
# @abstract Abstract base method for initiating a call to a maps API
|
28
|
+
# @return [void]
|
29
|
+
def make_call
|
30
|
+
raise NeedToOveride, 'make_call'
|
31
|
+
end
|
32
|
+
|
33
|
+
# @abstract Abstract base method for checking if a call to a maps API
|
34
|
+
# suceeded
|
35
|
+
# @return [Boolean] true, or false if the request failed.
|
36
|
+
def success?
|
37
|
+
raise NeedToOveride, 'success?'
|
38
|
+
end
|
39
|
+
|
40
|
+
# @abstract Abstract base method for returning a compacted, flattened array
|
41
|
+
# of different address responses.
|
42
|
+
# @return [Array<Hash>] a collection of possible addresses
|
43
|
+
def array_result
|
44
|
+
[@result['results']].flatten
|
45
|
+
end
|
46
|
+
|
47
|
+
# Raise a connection error
|
48
|
+
def connection_error(msg)
|
49
|
+
raise ConnectionError, msg
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'address_geocoder/error'
|
3
|
+
|
4
|
+
module AddressGeocoder
|
5
|
+
# @abstract Abstract base class for generatoring URLs to call maps APIs
|
6
|
+
# @todo If not other apis need this class then maybe this should be a map api
|
7
|
+
# specific class (ie. might not need an abstract base class).
|
8
|
+
class UrlGenerator
|
9
|
+
# @!attribute api_key
|
10
|
+
# @return (see AddressGeocoder::Client#api_key)
|
11
|
+
attr_accessor :api_key
|
12
|
+
|
13
|
+
# @!attribute language
|
14
|
+
# @return (see AddressGeocoder::Client#language)
|
15
|
+
attr_accessor :language
|
16
|
+
|
17
|
+
# @!attribute address
|
18
|
+
# @return [Hash]
|
19
|
+
attr_accessor :address
|
20
|
+
|
21
|
+
def initialize(args = {})
|
22
|
+
@api_key = args[:api_key]
|
23
|
+
@language = args[:language]
|
24
|
+
@address = args[:address]
|
25
|
+
end
|
26
|
+
|
27
|
+
# @abstract Abstract base method for generating a URL with which to call a
|
28
|
+
# maps API
|
29
|
+
# @return [String] a URL to use in calling a maps API
|
30
|
+
def generate_url
|
31
|
+
raise NeedToOveride, 'generate_url'
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Translate a hash into a query string
|
37
|
+
# @param hash [Hash] the object to be transformed
|
38
|
+
# @return [String] a URL query
|
39
|
+
def hash_to_query(hash)
|
40
|
+
URI.encode_www_form(hash)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/maps_api.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'address_geocoder/client'
|
2
|
+
require 'maps_api/google/parser'
|
3
|
+
require 'maps_api/google/requester'
|
4
|
+
|
5
|
+
module MapsApi
|
6
|
+
module Google
|
7
|
+
# Class for interacting with Google Maps API
|
8
|
+
class Client < ::AddressGeocoder::Client
|
9
|
+
# Assigns the entered variables to their proper instance variables
|
10
|
+
# @param (see AddressGeocoder::Client#assign_initial)
|
11
|
+
# @return (see AddressGeocoder::Client#assign_initial)
|
12
|
+
def assign_initial(args)
|
13
|
+
@parser = Parser.new
|
14
|
+
@requester = Requester.new(parser: @parser)
|
15
|
+
super args
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'address_geocoder/parser'
|
2
|
+
|
3
|
+
module MapsApi
|
4
|
+
module Google
|
5
|
+
# Class for parsing Google Maps API responses
|
6
|
+
class Parser < ::AddressGeocoder::Parser
|
7
|
+
# List of Google's attribute title for streets
|
8
|
+
STREET_TYPES = { values: %w(route), name: 'street' }.freeze
|
9
|
+
# List of Google's attribute title for cities
|
10
|
+
CITY_TYPES = { values: %w(neighborhood locality sublocality),
|
11
|
+
name: 'city' }.freeze
|
12
|
+
# List of Google's attribute titles for states
|
13
|
+
STATE_TYPES = { values: %w(administrative_area_level_4
|
14
|
+
administrative_area_level_3
|
15
|
+
administrative_area_level_2
|
16
|
+
administrative_area_level_1),
|
17
|
+
name: 'state' }.freeze
|
18
|
+
# List of Google's attribute titles for postal codes
|
19
|
+
POSTAL_TYPES = { values: %w(postal_code postal_code_prefix),
|
20
|
+
name: 'postal_code' }.freeze
|
21
|
+
|
22
|
+
# Convert Google Maps' response into our format with the goal of finding
|
23
|
+
# several matching addresses
|
24
|
+
# @return (see AddressGeocoder::Parser#parse_response)
|
25
|
+
def parse_response
|
26
|
+
@fields['address_components'].each do |field|
|
27
|
+
parse_field(field)
|
28
|
+
end
|
29
|
+
define_address
|
30
|
+
end
|
31
|
+
|
32
|
+
# Takes a specific field and converts it into our format
|
33
|
+
# @param field [Hash] one particular field from Google Maps' response
|
34
|
+
# @return [void]
|
35
|
+
def parse_field(field)
|
36
|
+
[STREET_TYPES, CITY_TYPES, STATE_TYPES, POSTAL_TYPES].each do |type|
|
37
|
+
if similar?(field['types'], type[:values])
|
38
|
+
send("add_#{type[:name]}", field)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Check to see if the city should have been used in the call, and if so,
|
44
|
+
# was it?
|
45
|
+
# @return [Boolean] true, or false if the city should have been used but
|
46
|
+
# wasn't or vice versa
|
47
|
+
def city_present?(level)
|
48
|
+
[3, 4, 7].include?(level) && @address[:city]
|
49
|
+
end
|
50
|
+
|
51
|
+
# Check to see if the state should have been used in the call, and if so,
|
52
|
+
# was it?
|
53
|
+
# @return [Boolean] true, or false if the state should have been used but
|
54
|
+
# wasn't or vice versa
|
55
|
+
def state_present?(level)
|
56
|
+
4 == level && @address[:state]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check to see if the postal code should have been used in the call, and
|
60
|
+
# if so, was it?
|
61
|
+
# @return [Boolean] true, or false if the postal code should have been
|
62
|
+
# used but wasn't or vice versa
|
63
|
+
def pc_present?(level)
|
64
|
+
[5, 6, 7].include?(level) && @address[:postal_code]
|
65
|
+
end
|
66
|
+
|
67
|
+
def just_country?(google_response)
|
68
|
+
google_response['results'][0]['address_components'].count == 1
|
69
|
+
end
|
70
|
+
|
71
|
+
def not_correct_country?(google_response)
|
72
|
+
components = google_response['results']
|
73
|
+
components = components[0]['address_components']
|
74
|
+
!(components.select do |x|
|
75
|
+
x['short_name'] == @address[:country][:alpha2]
|
76
|
+
end).any?
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def define_address
|
82
|
+
{ country: @address[:country], city: @city, state: @state,
|
83
|
+
postal_code: @postal_code, street: @street }
|
84
|
+
end
|
85
|
+
|
86
|
+
def similar?(array1, array2)
|
87
|
+
(array1 & array2).any?
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_street(field)
|
91
|
+
@street = field['long_name']
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_city(field)
|
95
|
+
if @city
|
96
|
+
@state = field['long_name']
|
97
|
+
@switch = true
|
98
|
+
else
|
99
|
+
@city = field['long_name']
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def add_state(field)
|
104
|
+
str = if field['types'].include? 'administrative_area_level_1'
|
105
|
+
'short_name'
|
106
|
+
else
|
107
|
+
'long_name'
|
108
|
+
end
|
109
|
+
if @switch
|
110
|
+
@city = @state
|
111
|
+
@switch = false
|
112
|
+
end
|
113
|
+
@state = field[str]
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_postal_code(field)
|
117
|
+
@postal_code = field['long_name']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|