Skip to content

Commit f1db3cc

Browse files
committed
Merge branch 'release/v0.3.0'
2 parents b547b44 + 2e224c8 commit f1db3cc

6 files changed

Lines changed: 102 additions & 68 deletions

File tree

google_maps_service.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
1919

2020
spec.add_runtime_dependency 'multi_json', '~> 1.11'
2121
spec.add_runtime_dependency 'hurley', '~> 0.1'
22-
spec.add_runtime_dependency 'retriable', '~> 2.0.2'
22+
spec.add_runtime_dependency 'retriable', '~> 2.0', '>= 2.0.2'
2323
spec.add_runtime_dependency 'ruby-hmac', '~> 0.4.0'
2424
spec.add_development_dependency 'bundler', '~> 1.7'
2525
spec.add_development_dependency 'rake', '~> 10.0'

lib/google_maps_service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module GoogleMapsService
22
class << self
3-
attr_accessor :key, :client_id, :client_secret, :connect_timeout, :read_timeout, :retry_timeout
3+
attr_accessor :key, :client_id, :client_secret, :connect_timeout, :read_timeout, :retry_timeout, :queries_per_second
44

55
def configure
66
yield self

lib/google_maps_service/client.rb

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'hurley'
33
require 'multi_json'
44
require 'retriable'
5+
require 'thread'
56

67
module GoogleMapsService
78
class Client
@@ -43,13 +44,28 @@ class Client
4344
# @return [Integer]
4445
attr_reader :retry_timeout
4546

47+
# Number of queries per second permitted.
48+
# If the rate limit is reached, the client will sleep for
49+
# the appropriate amount of time before it runs the current query.
50+
# @return [Integer]
51+
attr_reader :queries_per_second
52+
4653
def initialize(options={})
4754
@key = options[:key] || GoogleMapsService.key
4855
@client_id = options[:client_id] || GoogleMapsService.client_id
4956
@client_secret = options[:client_secret] || GoogleMapsService.client_secret
5057
@connect_timeout = options[:connect_timeout] || GoogleMapsService.connect_timeout
5158
@read_timeout = options[:read_timeout] || GoogleMapsService.read_timeout
5259
@retry_timeout = options[:retry_timeout] || GoogleMapsService.retry_timeout || 60
60+
@queries_per_second = options[:queries_per_second] || GoogleMapsService.queries_per_second
61+
62+
# Prepare "tickets" for calling API
63+
if @queries_per_second
64+
@sent_times = SizedQueue.new @queries_per_second
65+
@queries_per_second.times do
66+
@sent_times << 0
67+
end
68+
end
5369
end
5470

5571
# Get the current HTTP client
@@ -74,13 +90,21 @@ def new_client
7490
def get(path, params, base_url: DEFAULT_BASE_URL, accepts_client_id: true, custom_response_decoder: nil)
7591
url = base_url + generate_auth_url(path, params, accepts_client_id)
7692

77-
Retriable.retriable timeout: @retry_timeout,
78-
on: RETRIABLE_ERRORS do |try|
79-
response = client.get url
80-
if custom_response_decoder
81-
return custom_response_decoder.call(response)
93+
Retriable.retriable timeout: @retry_timeout, on: RETRIABLE_ERRORS do |try|
94+
# Get/wait the request "ticket" if QPS is configured
95+
# Check for previous request time, it must be more than a second ago before calling new request
96+
if @sent_times
97+
elapsed_since_earliest = Time.now - @sent_times.pop
98+
sleep(1 - elapsed_since_earliest) if elapsed_since_earliest.to_f < 1
8299
end
83-
return decode_response_body(response)
100+
101+
response = client.get url
102+
103+
# Release request "ticket"
104+
@sent_times << Time.now if @sent_times
105+
106+
return custom_response_decoder.call(response) if custom_response_decoder
107+
decode_response_body(response)
84108
end
85109
end
86110

@@ -102,27 +126,17 @@ def decode_response_body(response)
102126

103127
body = MultiJson.load(response.body, :symbolize_keys => true)
104128

105-
api_status = body[:status]
106-
if api_status == "OK" or api_status == "ZERO_RESULTS"
129+
case body[:status]
130+
when 'OK', 'ZERO_RESULTS'
107131
return body
108-
end
109-
110-
if api_status == "OVER_QUERY_LIMIT"
132+
when 'OVER_QUERY_LIMIT'
111133
raise GoogleMapsService::Error::RateLimitError.new(response), body[:error_message]
112-
end
113-
114-
if api_status == "REQUEST_DENIED"
134+
when 'REQUEST_DENIED'
115135
raise GoogleMapsService::Error::RequestDeniedError.new(response), body[:error_message]
116-
end
117-
118-
if api_status == "INVALID_REQUEST"
136+
when 'INVALID_REQUEST'
119137
raise GoogleMapsService::Error::InvalidRequestError.new(response), body[:error_message]
120-
end
121-
122-
if body[:error_message]
123-
raise GoogleMapsService::Error::ApiError.new(response), body[:error_message]
124138
else
125-
raise GoogleMapsService::Error::ApiError.new(response)
139+
raise GoogleMapsService::Error::ApiError.new(response), body[:error_message]
126140
end
127141
end
128142

@@ -131,20 +145,20 @@ def check_response_status_code(response)
131145
when 200..300
132146
# Do-nothing
133147
when 301, 302, 303, 307
134-
message ||= sprintf('Redirect to %s', response.header[:location])
148+
message = sprintf('Redirect to %s', response.header[:location])
135149
raise GoogleMapsService::Error::RedirectError.new(response), message
136150
when 401
137-
message ||= 'Unauthorized'
138-
raise GoogleMapsService::Error::ClientError.new(response)
151+
message = 'Unauthorized'
152+
raise GoogleMapsService::Error::ClientError.new(response), message
139153
when 304, 400, 402...500
140-
message ||= 'Invalid request'
141-
raise GoogleMapsService::Error::ClientError.new(response)
154+
message = 'Invalid request'
155+
raise GoogleMapsService::Error::ClientError.new(response), message
142156
when 500..600
143-
message ||= 'Server error'
144-
raise GoogleMapsService::Error::ServerError.new(response)
157+
message = 'Server error'
158+
raise GoogleMapsService::Error::ServerError.new(response), message
145159
else
146-
message ||= 'Unknown error'
147-
raise GoogleMapsService::Error::TransmissionError.new(response)
160+
message = 'Unknown error'
161+
raise GoogleMapsService::Error::Error.new(response), message
148162
end
149163
end
150164

lib/google_maps_service/roads.rb

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -95,25 +95,18 @@ def extract_roads_body(response)
9595
error = body[:error]
9696
status = error[:status]
9797

98-
if status == 'INVALID_ARGUMENT'
99-
if error[:message] == 'The provided API key is invalid.'
98+
case status
99+
when 'INVALID_ARGUMENT'
100+
if error[:message] == 'The provided API key is invalid.'
101+
raise GoogleMapsService::Error::RequestDeniedError.new(response), error[:message]
102+
end
103+
raise GoogleMapsService::Error::InvalidRequestError.new(response), error[:message]
104+
when 'PERMISSION_DENIED'
100105
raise GoogleMapsService::Error::RequestDeniedError.new(response), error[:message]
101-
end
102-
raise GoogleMapsService::Error::InvalidRequestError.new(response), error[:message]
103-
end
104-
105-
if status == 'PERMISSION_DENIED'
106-
raise GoogleMapsService::Error::RequestDeniedError.new(response), error[:message]
107-
end
108-
109-
if status == 'RESOURCE_EXHAUSTED'
110-
raise GoogleMapsService::Error::RateLimitError.new(response), error[:message]
111-
end
112-
113-
if error.has_key?(:message)
114-
raise GoogleMapsService::Error::ApiError.new(response), error[:message]
115-
else
116-
raise GoogleMapsService::Error::ApiError.new(response)
106+
when 'RESOURCE_EXHAUSTED'
107+
raise GoogleMapsService::Error::RateLimitError.new(response), error[:message]
108+
else
109+
raise GoogleMapsService::Error::ApiError.new(response), error[:message]
117110
end
118111
end
119112

lib/google_maps_service/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module GoogleMapsService
2-
VERSION = '0.2.0'
2+
VERSION = '0.3.0'
33
end

spec/google_maps_service/client_spec.rb

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,49 @@
2525
end
2626
end
2727

28+
# This test assumes that the time to run a mocked query is
29+
# relatively small, eg a few milliseconds. We define a rate of
30+
# 3 queries per second, and run double that, which should take at
31+
# least 1 second but no more than 2.
32+
context 'with total request is double of queries per second' do
33+
let(:queries_per_second) { 3 }
34+
let(:total_request) { queries_per_second * 2 }
35+
let(:client) do
36+
GoogleMapsService::Client.new(key: api_key, queries_per_second: queries_per_second)
37+
end
38+
39+
before(:example) do
40+
stub_request(:get, /https:\/\/maps.googleapis.com\/maps\/api\/.*/).
41+
to_return(:status => 200, headers: { 'Content-Type' => 'application/json' }, :body => '{"status":"OK","results":[]}')
42+
end
43+
44+
it 'should take between 1-2 seconds' do
45+
start_time = Time.now
46+
total_request.times do
47+
client.geocode(address: "Sesame St.")
48+
end
49+
end_time = Time.now
50+
expect(end_time - start_time).to be_between(1, 2).inclusive
51+
end
52+
end
53+
54+
55+
context 'with client id and secret' do
56+
let(:client) do
57+
client = GoogleMapsService::Client.new(client_id: 'foo', client_secret: 'a2V5')
58+
end
59+
60+
before(:example) do
61+
stub_request(:get, /https:\/\/maps.googleapis.com\/maps\/api\/.*/).
62+
to_return(:status => 200, headers: { 'Content-Type' => 'application/json' }, :body => '{"status":"OK","results":[]}')
63+
end
64+
65+
it 'should be signed' do
66+
client.geocode(address: 'Sesame St.')
67+
expect(a_request(:get, 'https://maps.googleapis.com/maps/api/geocode/json?address=Sesame+St.&client=foo&signature=fxbWUIcNPZSekVOhp2ul9LW5TpY=')).to have_been_made
68+
end
69+
end
70+
2871
context 'without api key and client secret pair' do
2972
it 'should raise ArgumentError' do
3073
client = GoogleMapsService::Client.new
@@ -54,22 +97,6 @@
5497
end
5598
end
5699

57-
context 'with client id and secret' do
58-
let(:client) do
59-
client = GoogleMapsService::Client.new(client_id: 'foo', client_secret: 'a2V5')
60-
end
61-
62-
before(:example) do
63-
stub_request(:get, /https:\/\/maps.googleapis.com\/maps\/api\/.*/).
64-
to_return(:status => 200, headers: { 'Content-Type' => 'application/json' }, :body => '{"status":"OK","results":[]}')
65-
end
66-
67-
it 'should be signed' do
68-
client.geocode(address: 'Sesame St.')
69-
expect(a_request(:get, 'https://maps.googleapis.com/maps/api/geocode/json?address=Sesame+St.&client=foo&signature=fxbWUIcNPZSekVOhp2ul9LW5TpY=')).to have_been_made
70-
end
71-
end
72-
73100
context 'with over query limit' do
74101
before(:example) do
75102
stub_request(:get, /https:\/\/maps.googleapis.com\/maps\/api\/geocode\/.*/)

0 commit comments

Comments
 (0)