address_geocoder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|