address_geocoder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module AddressGeocoder
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,7 @@
1
+ require 'yaml'
2
+ require 'maps_api/google'
3
+
4
+ # Namespace for classes and modules that handling API communication
5
+ # @since 0.0.1
6
+ module MapsApi
7
+ end
@@ -0,0 +1,9 @@
1
+ require 'maps_api/google/client'
2
+ require 'maps_api/google/parser'
3
+ require 'maps_api/google/requester'
4
+ require 'maps_api/google/url_generator'
5
+
6
+ # Namespace for classes that interact with the Google Maps API
7
+ # @since 0.0.1
8
+ module Google
9
+ end
@@ -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