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.
@@ -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