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 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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
13
+ .env
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.0
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+
6
+ before_install: gem install bundler -v 1.16.4
7
+
8
+ install: bundle install
9
+
10
+ script: bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in google_maps_juice.gemspec
6
+ gemspec
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
+ [![Build Status](https://travis-ci.org/algonauti/google_maps_juice.svg?branch=master)](https://travis-ci.org/algonauti/google_maps_juice)
6
+ [![Coverage Status](https://coveralls.io/repos/github/algonauti/google_maps_juice/badge.svg?branch=master)](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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
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,5 @@
1
+ require 'active_support/configurable'
2
+
3
+ module GoogleMapsJuice
4
+ include ActiveSupport::Configurable
5
+ 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
@@ -0,0 +1,3 @@
1
+ module GoogleMapsJuice
2
+ VERSION = "1.0.0"
3
+ 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: []