google_maps_juice 1.0.0
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 +13 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +6 -0
- data/bin/console +21 -0
- data/bin/setup +8 -0
- data/env-example +1 -0
- data/google_maps_juice.gemspec +35 -0
- data/lib/google_maps_juice.rb +21 -0
- data/lib/google_maps_juice/client.rb +39 -0
- data/lib/google_maps_juice/configuration.rb +5 -0
- data/lib/google_maps_juice/endpoint.rb +86 -0
- data/lib/google_maps_juice/geocoding.rb +99 -0
- data/lib/google_maps_juice/geocoding/response.rb +70 -0
- data/lib/google_maps_juice/timezone.rb +82 -0
- data/lib/google_maps_juice/version.rb +3 -0
- metadata +191 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 81dd523f6c0a801f8b8e15d0589525cd683359f4b85d0427f9b697d1ade77f5d
|
4
|
+
data.tar.gz: 2fb1b05f6a92b3770771a5ae74de3ccd09f411f66dabde16d38160f151f48d66
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cdc4a09d044ecd8549b5a7851c1fda06be8a1ff4677df0e3173dfdabadfbb9b791789a3fe4a4a8d1dbc1d353f2382812ddc6dbb912915b679598502dd0254212
|
7
|
+
data.tar.gz: 646b96bb8275a1230eb16a19d6472e56bd0f31218e0580976f1e9a4c88f738604885a156d92927fcb5060e54ab0ea077bc66c77e4f351eb12384ef63b3ac5260
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Algonauti srl
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
# GoogleMapsJuice
|
2
|
+
|
3
|
+
**Drink Google Maps Services with ease!** :tropical_drink: :earth_americas:
|
4
|
+
|
5
|
+
[](https://travis-ci.org/algonauti/google_maps_juice)
|
6
|
+
[](https://coveralls.io/github/algonauti/google_maps_juice?branch=master)
|
7
|
+
|
8
|
+
This gem aims at progressively covering a fair amount of those widely-used services that are part of the Google Maps Platform, such as: [Geocoding](https://developers.google.com/maps/documentation/geocoding/intro), [Time Zone](https://developers.google.com/maps/documentation/timezone/intro), [Directions](https://developers.google.com/maps/documentation/directions/intro), etc. with some key ideas:
|
9
|
+
|
10
|
+
1. Allowing "standard" requests, meaning: sending the same params documented by Google.
|
11
|
+
2. Allowing "smart" requests, meaning: with more "developer-friendly" params, and/or improved error handling.
|
12
|
+
3. Return full Google responses, but also provide methods to easily inspect the most relevant info.
|
13
|
+
4. Provide error handling.
|
14
|
+
|
15
|
+
|
16
|
+
`GoogleMapsJuice` currently covers:
|
17
|
+
|
18
|
+
* Geocoding
|
19
|
+
* Time Zone
|
20
|
+
|
21
|
+
Contributors are welcome!
|
22
|
+
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'google_maps_juice'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle
|
35
|
+
|
36
|
+
Or install it yourself as:
|
37
|
+
|
38
|
+
$ gem install google_maps_juice
|
39
|
+
|
40
|
+
|
41
|
+
## Google API Key
|
42
|
+
|
43
|
+
You can set your Google API key with the following one-liner:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
GoogleMapsJuice.configure { |c| c.api_key = 'my-api-key' }`
|
47
|
+
```
|
48
|
+
|
49
|
+
In a Rails application, that would typically go in an initializer.
|
50
|
+
|
51
|
+
|
52
|
+
### Multiple API keys
|
53
|
+
|
54
|
+
If you need to use multiple API keys, you have two options:
|
55
|
+
|
56
|
+
* a) pass an `api_key` named param to the endpoint class method, e.g.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
GoogleMapsJuice::Geocoding.geocode(params, api_key: 'my-api-key')
|
60
|
+
```
|
61
|
+
|
62
|
+
* b) create your own `GoogleMapsJuice::Client` instance(s) and use it to create your endpoint object(s), e.g.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
client = GoogleMapsJuice::Client.new(api_key: 'my-api-key')
|
66
|
+
geocoding = GoogleMapsJuice::Geocoding.new(client)
|
67
|
+
response = geocoding.geocode(params)
|
68
|
+
```
|
69
|
+
|
70
|
+
This is especially useful in some "hybrid" scenario, where an API key is shared by a group of requests, but another group uses a different key: a `client` object would then be instantiated and reused for each group.
|
71
|
+
|
72
|
+
|
73
|
+
## Error Handling
|
74
|
+
|
75
|
+
If Google servers respond with a non-successful HTTP status code, i.e. `4xx` or `5xx`, a `GoogleMapsJuice::Error` is raised with a message of the form `'HTTP 503 - Error details as returned by the server'`.
|
76
|
+
|
77
|
+
API errors are also handled, based on the `status` attribute of Google's JSON response, and the optional `error_message` attribute.
|
78
|
+
|
79
|
+
* `GoogleMapsJuice::ZeroResults` is raised when `status` is `'ZERO_RESULTS'`
|
80
|
+
* `GoogleMapsJuice::ApiLimitError` is raised when `status` is `'OVER_DAILY_LIMIT'` or `'OVER_QUERY_LIMIT'`
|
81
|
+
* `GoogleMapsJuice::Error` is raised when `status` is not `OK` with a message of the form `API <status> - <error_message>`
|
82
|
+
|
83
|
+
|
84
|
+
## Geocoding
|
85
|
+
|
86
|
+
### Standard Geocoding
|
87
|
+
|
88
|
+
The simplest geocoding requests accept an address:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
response = GoogleMapsJuice::Geocoding.geocode(address: '8955 Lantana Rd, Lake Worth, FL 33467, USA')
|
92
|
+
```
|
93
|
+
|
94
|
+
Supported params are the ones accepted by Google's endpoint: `address`, `components`, `bounds`, `language`, `region`; at least one between `address` and `components` is required. Learn more [here](https://developers.google.com/maps/documentation/geocoding/intro#geocoding). `GoogleMapsJuice` will raise an `ArgumentError` if some unsupported param is passed, or when none of the required params are passed.
|
95
|
+
|
96
|
+
|
97
|
+
### Smart Geocoding
|
98
|
+
|
99
|
+
**Motivation**
|
100
|
+
|
101
|
+
For best geocoding results, the `address` param should be formatted according to the local language. This is often a hard task for an application that needs to geocode addresses stored as separate fields. Luckily, Google offers the `components` param which accepts individual address fields; however, it's annoying to build it and it's not fault tolerant. For example, an error on `postal_code` makes geocoding of a whole address fail.
|
102
|
+
|
103
|
+
Purpose of `i_geocode` method is twofold:
|
104
|
+
|
105
|
+
1. Providing a simpler method interface for leveraging Google's `components` param
|
106
|
+
2. Providing an approximate geocoding result when some address component is wrong
|
107
|
+
|
108
|
+
Here an example call with all supported params:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
response = GoogleMapsJuice::Geocoding.i_geocode(
|
112
|
+
{
|
113
|
+
address: '8955 Lantana Rd',
|
114
|
+
locality: 'Lake Worth',
|
115
|
+
administrative_area: 'FL',
|
116
|
+
postal_code: '33467',
|
117
|
+
country: 'US'
|
118
|
+
}, sleep_before_retry: 0.15
|
119
|
+
)
|
120
|
+
```
|
121
|
+
|
122
|
+
**Accepted params:**
|
123
|
+
|
124
|
+
* At least one between `address` and `country` is required
|
125
|
+
* `locality`, `administrative_area`, `postal_code` and `country` expect the same content as described in [Component Filtering](https://developers.google.com/maps/documentation/geocoding/intro#ComponentFiltering)
|
126
|
+
* `address` can also include more info than street number and name, as long as they do not contrast with other params passed
|
127
|
+
* An optional `sleep_before_retry` param sets seconds between geocoding attempts (see below); defaults to zero.
|
128
|
+
* `GoogleMapsJuice` will raise an `ArgumentError` if some unsupported param is passed, or when none of the required params are passed.
|
129
|
+
|
130
|
+
**How it works**
|
131
|
+
|
132
|
+
On its 1st attempt, `i_geocode` sends all received params to Google's endpoint, properly formatted. If a `GoogleMapsJuice::ZeroResults` is raised, it removes a param and retries until no error is raised. Params are removed in the following order:
|
133
|
+
|
134
|
+
* `postal_code`
|
135
|
+
* `address`
|
136
|
+
* `locality`
|
137
|
+
* `administrative_area`
|
138
|
+
|
139
|
+
As a consequence:
|
140
|
+
|
141
|
+
* In the best case, `i_geocode` will send 1 request to Google API
|
142
|
+
* In the worst case, `i_geocode` will send 4 requests to Google API
|
143
|
+
|
144
|
+
|
145
|
+
### Geocoding Response
|
146
|
+
|
147
|
+
Both `geocode` and `i_geocode` methods return a `GoogleMapsJuice::Geocoding::Response`. It's a `Hash` representation of Google's JSON response. However, it also provides many useful methods:
|
148
|
+
|
149
|
+
* `latitude`, `longitude`: geographic coordinates as `float` numbers
|
150
|
+
|
151
|
+
* `street_number`, `route`, `locality`, `postal_code`, `administrative_area_level_1`, `country`: all of these methods return a `Hash` with 2 keys: `'short_name'` and `'long_name'`
|
152
|
+
|
153
|
+
* `partial_match?`: boolean, `true` if some param (of the last geocoding attempt) partially matched
|
154
|
+
|
155
|
+
* `precision`: can be one of: `'street_number'`, `'route'`, `'locality'`, `'postal_code'`, `'administrative_area_level_1'`, `'country'` and represents the most-specific matching component
|
156
|
+
|
157
|
+
|
158
|
+
## Time Zone
|
159
|
+
|
160
|
+
[Google's Time Zone API](https://developers.google.com/maps/documentation/timezone/intro#Requests) returns the time zone of a given geographic location; it also accepts a timestamp, in order to determine whether DST should be applied or not.
|
161
|
+
|
162
|
+
GoogleMapsJuice provides the `GoogleMapsJuice::Timezone.by_location` method. Compared to Google's raw API request, it provides simpler params and some validations, in order to avoid sending requests when they would fail for sure (and then save money!) - to learn more see `spec/unit/timezone_spec.rb`.
|
163
|
+
|
164
|
+
**Accepted params:**
|
165
|
+
|
166
|
+
* Both `latitude` and `longitude` are mandatory
|
167
|
+
* `timestamp` is optional and defaults to `Time.now`
|
168
|
+
* `language` is optional
|
169
|
+
|
170
|
+
|
171
|
+
### Time Zone Response
|
172
|
+
|
173
|
+
The `by_location` method returns a `GoogleMapsJuice::Timezone::Response`. It's a `Hash` representation of Google's JSON response. However, it also provides a few useful methods:
|
174
|
+
|
175
|
+
* `timezone_id`: unique name as defined in [IANA Time Zone Database](https://www.iana.org/time-zones)
|
176
|
+
|
177
|
+
* `timezone_name`: the long form name of the time zone
|
178
|
+
|
179
|
+
* `raw_offset`: the offset from UTC in seconds
|
180
|
+
|
181
|
+
* `dst_offset`: the offset for daylight-savings time in seconds
|
182
|
+
|
183
|
+
|
184
|
+
## Development
|
185
|
+
|
186
|
+
After checking out the repo, run `bin/setup` to install dependencies. Create a `.env` file and save your Google API key there; if you want to use a different key for testing, put it in `.env.test` and it will override the one in `.env`.
|
187
|
+
|
188
|
+
Run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
189
|
+
|
190
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
191
|
+
|
192
|
+
|
193
|
+
### Implementing a new endpoint
|
194
|
+
|
195
|
+
All endpoints must be subclasses of `GoogleMapsJuice::Endpoint`; methods that implement "standard" Google API calls have a common structure, described by the `invoke` method in the `SomeEndpoint` test class in `spec/unit/endpoint_spec.rb`.
|
196
|
+
|
197
|
+
All new endpoints' methods must return subclasses of `GoogleMapsJuice::Endpoint::Response` as their response objects, since it contains methods needed for error handling.
|
198
|
+
|
199
|
+
|
200
|
+
## Contributing
|
201
|
+
|
202
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/algonauti/google_maps_juice.
|
203
|
+
|
204
|
+
|
205
|
+
## License
|
206
|
+
|
207
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "google_maps_juice"
|
5
|
+
require 'dotenv/load'
|
6
|
+
|
7
|
+
Dotenv.overload('.env', '.env.development')
|
8
|
+
|
9
|
+
GoogleMapsJuice.configure do |config|
|
10
|
+
config.api_key = ENV.fetch('API_KEY')
|
11
|
+
end
|
12
|
+
|
13
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
14
|
+
# with your gem easier. You can also use a different console, if you like.
|
15
|
+
|
16
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
17
|
+
# require "pry"
|
18
|
+
# Pry.start
|
19
|
+
|
20
|
+
require "irb"
|
21
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/env-example
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
API_KEY=
|
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "google_maps_juice/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "google_maps_juice"
|
8
|
+
spec.version = GoogleMapsJuice::VERSION
|
9
|
+
spec.authors = ["Algonauti srl"]
|
10
|
+
spec.email = ["info@algonauti.com"]
|
11
|
+
|
12
|
+
spec.summary = "Client for popular Google Maps API Services: Geocoding, Time Zones"
|
13
|
+
spec.description = "Put Google Maps APIs in a spin-dryer and drink their juice: Geocoding, Time Zones, ...and more upcoming!"
|
14
|
+
spec.homepage = "https://github.com/algonauti/google_maps_juice"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency 'activesupport', '~> 5.2'
|
26
|
+
spec.add_dependency 'excon'
|
27
|
+
|
28
|
+
spec.add_development_dependency 'bundler', '~> 1.16'
|
29
|
+
spec.add_development_dependency 'rake', '~> 12.3'
|
30
|
+
spec.add_development_dependency 'rspec', '~> 3.5'
|
31
|
+
spec.add_development_dependency 'webmock', '~> 3.4'
|
32
|
+
spec.add_development_dependency 'dotenv', '~> 2.5'
|
33
|
+
spec.add_development_dependency 'vcr', '~> 4.0'
|
34
|
+
spec.add_development_dependency 'coveralls', '~> 0.8'
|
35
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_support/core_ext/object'
|
2
|
+
require 'active_support/dependencies/autoload'
|
3
|
+
|
4
|
+
require 'excon'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
require 'google_maps_juice/configuration'
|
8
|
+
require 'google_maps_juice/version'
|
9
|
+
|
10
|
+
module GoogleMapsJuice
|
11
|
+
extend ActiveSupport::Autoload
|
12
|
+
|
13
|
+
autoload :Client, 'google_maps_juice/client'
|
14
|
+
autoload :Endpoint, 'google_maps_juice/endpoint'
|
15
|
+
autoload :Geocoding, 'google_maps_juice/geocoding'
|
16
|
+
autoload :Timezone, 'google_maps_juice/timezone'
|
17
|
+
|
18
|
+
class Error < Exception; end
|
19
|
+
class ApiLimitError < Exception; end
|
20
|
+
class ZeroResults < Exception; end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module GoogleMapsJuice
|
2
|
+
class Client
|
3
|
+
attr_reader :api_key, :connection
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def get(endpoint, params, api_key: GoogleMapsJuice.config.api_key)
|
7
|
+
self.new(api_key: api_key).get(endpoint, params)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
API_HOST = 'https://maps.googleapis.com'
|
12
|
+
API_PATH = '/maps/api'
|
13
|
+
|
14
|
+
def initialize(api_key: GoogleMapsJuice.config.api_key)
|
15
|
+
@api_key = api_key
|
16
|
+
@connection = Excon.new(API_HOST)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(endpoint, params)
|
20
|
+
full_params = (params || Hash.new).merge({ key: api_key })
|
21
|
+
response = connection.get(
|
22
|
+
path: "#{API_PATH}#{endpoint}",
|
23
|
+
query: full_params
|
24
|
+
)
|
25
|
+
if healthy_http_status?(response.status)
|
26
|
+
response.body
|
27
|
+
else
|
28
|
+
msg = "HTTP #{response.status}"
|
29
|
+
msg += " - #{response.body}" if response.body.present?
|
30
|
+
raise GoogleMapsJuice::Error, msg
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def healthy_http_status?(status)
|
35
|
+
/^[123]\d{2}$/.match?("#{status}")
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module GoogleMapsJuice
|
2
|
+
class Endpoint
|
3
|
+
|
4
|
+
def initialize(client)
|
5
|
+
@client = client
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def detect_errors(response)
|
11
|
+
raise ArgumentError, 'GoogleMapsJuice::Endpoint::Response argument expected' unless response.is_a?(Response)
|
12
|
+
if response.zero_results?
|
13
|
+
raise GoogleMapsJuice::ZeroResults
|
14
|
+
elsif response.limit_error?
|
15
|
+
raise GoogleMapsJuice::ApiLimitError, build_error_message(response)
|
16
|
+
elsif response.error?
|
17
|
+
raise GoogleMapsJuice::Error, "API #{build_error_message(response)}"
|
18
|
+
else
|
19
|
+
response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_error_message(response)
|
24
|
+
msg = response.status
|
25
|
+
msg += " - #{response.error_message}" if response.error_message.present?
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate_supported_params(params, supported_keys)
|
29
|
+
unsupported_params = params.keys.select do |key|
|
30
|
+
!supported_keys.include?(key.to_s)
|
31
|
+
end
|
32
|
+
if unsupported_params.present?
|
33
|
+
raise ArgumentError, "The following params are not supported: #{unsupported_params.join(', ')}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_required_params(params, required_keys, check_mode)
|
38
|
+
required_params_present = required_keys.send("#{check_mode}?") do |key|
|
39
|
+
params.keys.map(&:to_s).include?(key)
|
40
|
+
end
|
41
|
+
unless required_params_present
|
42
|
+
verb = check_mode == 'one' ? 'is' : 'are'
|
43
|
+
raise ArgumentError, "#{check_mode.capitalize} of the following params #{verb} required: #{required_keys.join(', ')}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
%w( any one all ).each do |check_mode|
|
49
|
+
|
50
|
+
define_method "validate_#{check_mode}_required_params" do |params, required_keys|
|
51
|
+
validate_required_params(params, required_keys, check_mode)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class Response < Hash
|
58
|
+
|
59
|
+
def status
|
60
|
+
self['status']
|
61
|
+
end
|
62
|
+
|
63
|
+
def error?
|
64
|
+
status.upcase != 'OK'
|
65
|
+
end
|
66
|
+
|
67
|
+
def zero_results?
|
68
|
+
status.upcase == 'ZERO_RESULTS'
|
69
|
+
end
|
70
|
+
|
71
|
+
def limit_error?
|
72
|
+
%w( OVER_DAILY_LIMIT OVER_QUERY_LIMIT ).include?(status.upcase)
|
73
|
+
end
|
74
|
+
|
75
|
+
def error_message
|
76
|
+
self['error_message']
|
77
|
+
end
|
78
|
+
|
79
|
+
def results
|
80
|
+
self['results']
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module GoogleMapsJuice
|
2
|
+
class Geocoding < Endpoint
|
3
|
+
|
4
|
+
ENDPOINT = '/geocode'
|
5
|
+
|
6
|
+
autoload :Response, 'google_maps_juice/geocoding/response'
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def geocode(params, api_key: GoogleMapsJuice.config.api_key)
|
10
|
+
client = GoogleMapsJuice::Client.new(api_key: api_key)
|
11
|
+
self.new(client).geocode(params)
|
12
|
+
end
|
13
|
+
|
14
|
+
def i_geocode(params, sleep_before_retry: 0, api_key: GoogleMapsJuice.config.api_key)
|
15
|
+
client = GoogleMapsJuice::Client.new(api_key: api_key)
|
16
|
+
self.new(client).i_geocode(params, sleep_before_retry: sleep_before_retry)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def geocode(params)
|
22
|
+
validate_geocode_params(params)
|
23
|
+
response_text = @client.get("#{ENDPOINT}/json", params)
|
24
|
+
response = JSON.parse(response_text, object_class: Response)
|
25
|
+
detect_errors(response)
|
26
|
+
end
|
27
|
+
|
28
|
+
def i_geocode(params, sleep_before_retry: 0)
|
29
|
+
validate_i_geocode_params(params)
|
30
|
+
response = nil
|
31
|
+
removable_keys = [:administrative_area, :locality, :address, :postal_code]
|
32
|
+
begin
|
33
|
+
request_params = build_request_params(params)
|
34
|
+
response = geocode(request_params)
|
35
|
+
rescue ZeroResults => e
|
36
|
+
deleted_param = nil
|
37
|
+
while removable_keys.present? && deleted_param.nil?
|
38
|
+
key = removable_keys.pop
|
39
|
+
deleted_param = params.delete(key)
|
40
|
+
end
|
41
|
+
if deleted_param.present?
|
42
|
+
sleep sleep_before_retry
|
43
|
+
retry
|
44
|
+
else
|
45
|
+
raise e
|
46
|
+
end
|
47
|
+
end
|
48
|
+
response
|
49
|
+
end
|
50
|
+
|
51
|
+
def validate_geocode_params(params)
|
52
|
+
raise ArgumentError, 'Hash argument expected' unless params.is_a?(Hash)
|
53
|
+
|
54
|
+
supported_keys = %w( address components bounds language region )
|
55
|
+
validate_supported_params(params, supported_keys)
|
56
|
+
|
57
|
+
required_keys = %w( address components )
|
58
|
+
validate_any_required_params(params, required_keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def validate_i_geocode_params(params)
|
62
|
+
raise ArgumentError, 'Hash argument expected' unless params.is_a?(Hash)
|
63
|
+
|
64
|
+
supported_keys = %w( address locality postal_code administrative_area country language)
|
65
|
+
validate_supported_params(params, supported_keys)
|
66
|
+
|
67
|
+
required_keys = %w( address country )
|
68
|
+
validate_any_required_params(params, required_keys)
|
69
|
+
end
|
70
|
+
|
71
|
+
def build_request_params(i_geocode_params)
|
72
|
+
req_params = Hash.new
|
73
|
+
|
74
|
+
[:address, :language].each do |key|
|
75
|
+
if i_geocode_params[key].present?
|
76
|
+
req_params[key] = i_geocode_params[key]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
components = build_components_param(i_geocode_params, keys:
|
81
|
+
[:locality, :postal_code, :administrative_area, :country]
|
82
|
+
)
|
83
|
+
if components.present?
|
84
|
+
req_params[:components] = components
|
85
|
+
end
|
86
|
+
|
87
|
+
req_params
|
88
|
+
end
|
89
|
+
|
90
|
+
def build_components_param(params, keys: [])
|
91
|
+
keys.map do |key|
|
92
|
+
if params[key].present?
|
93
|
+
"#{key}:#{params[key]}"
|
94
|
+
end
|
95
|
+
end.compact.join('|')
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'active_support/core_ext/hash/slice'
|
2
|
+
|
3
|
+
module GoogleMapsJuice
|
4
|
+
class Geocoding::Response < GoogleMapsJuice::Endpoint::Response
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def precisions
|
8
|
+
%w( street_number route postal_code locality administrative_area_level_1
|
9
|
+
administrative_area_level_2 administrative_area_level_3 country )
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def latitude
|
14
|
+
location['lat']
|
15
|
+
end
|
16
|
+
|
17
|
+
def longitude
|
18
|
+
location['lng']
|
19
|
+
end
|
20
|
+
|
21
|
+
def location
|
22
|
+
result.dig('geometry', 'location')
|
23
|
+
end
|
24
|
+
|
25
|
+
def partial_match?
|
26
|
+
result['partial_match'] == true
|
27
|
+
end
|
28
|
+
|
29
|
+
def result
|
30
|
+
results.first
|
31
|
+
end
|
32
|
+
|
33
|
+
def address_components
|
34
|
+
result['address_components']
|
35
|
+
end
|
36
|
+
|
37
|
+
def precision
|
38
|
+
self.class.precisions.find do |type|
|
39
|
+
address_components.any? do |comp|
|
40
|
+
comp['types'].include?(type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
%w( street_number
|
47
|
+
route
|
48
|
+
locality
|
49
|
+
sublocality
|
50
|
+
postal_code
|
51
|
+
administrative_area_level_1
|
52
|
+
administrative_area_level_2
|
53
|
+
administrative_area_level_3
|
54
|
+
country ).each do |type|
|
55
|
+
|
56
|
+
define_method(type) do
|
57
|
+
addr_component_by(type: type)&.slice('long_name', 'short_name')
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def addr_component_by(type: '')
|
66
|
+
address_components.find { |ac| ac['types'].include?(type) }
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module GoogleMapsJuice
|
2
|
+
class Timezone < Endpoint
|
3
|
+
|
4
|
+
ENDPOINT = '/timezone'
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def by_location(params, api_key: GoogleMapsJuice.config.api_key)
|
8
|
+
client = GoogleMapsJuice::Client.new(api_key: api_key)
|
9
|
+
self.new(client).by_location(params)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def by_location(params)
|
14
|
+
validate_params(params)
|
15
|
+
response_text = @client.get("#{ENDPOINT}/json", build_request_params(params))
|
16
|
+
response = JSON.parse(response_text, object_class: Response)
|
17
|
+
detect_errors(response)
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate_params(params)
|
21
|
+
raise ArgumentError, 'Hash argument expected' unless params.is_a?(Hash)
|
22
|
+
|
23
|
+
supported_keys = %w( latitude longitude timestamp language )
|
24
|
+
validate_supported_params(params, supported_keys)
|
25
|
+
|
26
|
+
required_keys = %w( latitude longitude )
|
27
|
+
validate_all_required_params(params, required_keys)
|
28
|
+
|
29
|
+
validate_timestamp_param(params)
|
30
|
+
validate_location_params(params)
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_timestamp_param(params)
|
34
|
+
if params.has_key?(:timestamp)
|
35
|
+
raise ArgumentError, 'Timestamp must be a Time instance' unless params[:timestamp].is_a?(Time)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_location_params(params)
|
40
|
+
if params[:latitude].abs > 90
|
41
|
+
raise ArgumentError, 'Wrong latitude value'
|
42
|
+
end
|
43
|
+
if params[:longitude].abs > 180
|
44
|
+
raise ArgumentError, 'Wrong longitude value'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_request_params(params)
|
49
|
+
seconds_since_epoch = (params[:timestamp] || Time.now).to_i
|
50
|
+
{
|
51
|
+
location: "#{params[:latitude]},#{params[:longitude]}",
|
52
|
+
timestamp: seconds_since_epoch
|
53
|
+
}.tap do |req_params|
|
54
|
+
if params[:language]
|
55
|
+
req_params[:language] = params[:language]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
class Response < GoogleMapsJuice::Endpoint::Response
|
62
|
+
|
63
|
+
def timezone_id
|
64
|
+
self['timeZoneId']
|
65
|
+
end
|
66
|
+
|
67
|
+
def timezone_name
|
68
|
+
self['timeZoneName']
|
69
|
+
end
|
70
|
+
|
71
|
+
def raw_offset
|
72
|
+
self['rawOffset']
|
73
|
+
end
|
74
|
+
|
75
|
+
def dst_offset
|
76
|
+
self['dstOffset']
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: google_maps_juice
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Algonauti srl
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: excon
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.16'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.16'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '12.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '12.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.5'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.4'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.4'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: dotenv
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: vcr
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '4.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: coveralls
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.8'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.8'
|
139
|
+
description: 'Put Google Maps APIs in a spin-dryer and drink their juice: Geocoding,
|
140
|
+
Time Zones, ...and more upcoming!'
|
141
|
+
email:
|
142
|
+
- info@algonauti.com
|
143
|
+
executables: []
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".gitignore"
|
148
|
+
- ".rspec"
|
149
|
+
- ".ruby-version"
|
150
|
+
- ".travis.yml"
|
151
|
+
- Gemfile
|
152
|
+
- LICENSE.txt
|
153
|
+
- README.md
|
154
|
+
- Rakefile
|
155
|
+
- bin/console
|
156
|
+
- bin/setup
|
157
|
+
- env-example
|
158
|
+
- google_maps_juice.gemspec
|
159
|
+
- lib/google_maps_juice.rb
|
160
|
+
- lib/google_maps_juice/client.rb
|
161
|
+
- lib/google_maps_juice/configuration.rb
|
162
|
+
- lib/google_maps_juice/endpoint.rb
|
163
|
+
- lib/google_maps_juice/geocoding.rb
|
164
|
+
- lib/google_maps_juice/geocoding/response.rb
|
165
|
+
- lib/google_maps_juice/timezone.rb
|
166
|
+
- lib/google_maps_juice/version.rb
|
167
|
+
homepage: https://github.com/algonauti/google_maps_juice
|
168
|
+
licenses:
|
169
|
+
- MIT
|
170
|
+
metadata: {}
|
171
|
+
post_install_message:
|
172
|
+
rdoc_options: []
|
173
|
+
require_paths:
|
174
|
+
- lib
|
175
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '0'
|
180
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
181
|
+
requirements:
|
182
|
+
- - ">="
|
183
|
+
- !ruby/object:Gem::Version
|
184
|
+
version: '0'
|
185
|
+
requirements: []
|
186
|
+
rubyforge_project:
|
187
|
+
rubygems_version: 2.7.3
|
188
|
+
signing_key:
|
189
|
+
specification_version: 4
|
190
|
+
summary: 'Client for popular Google Maps API Services: Geocoding, Time Zones'
|
191
|
+
test_files: []
|