diff --git a/examples/example.rb b/examples/example.rb index 0463864..0dbcdbb 100644 --- a/examples/example.rb +++ b/examples/example.rb @@ -88,3 +88,12 @@ response = client.api_keys._(api_key_id).delete puts response.status_code puts response.headers + +# Rate limit information +response = client.version('v3').api_keys._(api_key_id).get +puts response.ratelimit.limit +puts response.ratelimit.remaining +puts response.ratelimit.reset +puts response.ratelimit.exceeded? +# Sleep the current thread until the reset has happened +response.ratelimit.wait! diff --git a/lib/ruby_http_client.rb b/lib/ruby_http_client.rb index 904e6c9..f7fe5cb 100644 --- a/lib/ruby_http_client.rb +++ b/lib/ruby_http_client.rb @@ -6,6 +6,49 @@ module SendGrid # Holds the response from an API call. class Response + # Provide useful functionality around API rate limiting. + class Ratelimit + attr_reader :limit, :remaining, :reset + + # * *Args* : + # - +limit+ -> The total number of requests allowed within a rate limit window + # - +remaining+ -> The number of requests that have been processed within this current rate limit window + # - +reset+ -> The time (in seconds since Unix Epoch) when the rate limit will reset + def initialize(limit, remaining, reset) + @limit = limit.to_i + @remaining = remaining.to_i + @reset = Time.at reset.to_i + end + + def exceeded? + remaining <= 0 + end + + # * *Returns* : + # - The number of requests that have been used out of this + # rate limit window + def used + limit - remaining + end + + # Sleep until the reset time arrives. If given a block, it will + # be called after sleeping is finished. + # + # * *Returns* : + # - The amount of time (in seconds) that the rate limit slept + # for. + def wait! + now = Time.now.utc.to_i + duration = (reset.to_i - now) + 1 + + sleep duration if duration >= 0 + + yield if block_given? + + duration + end + end + # * *Args* : # - +response+ -> A NET::HTTP response object # @@ -21,6 +64,20 @@ def initialize(response) def parsed_body @parsed_body ||= JSON.parse(@body, symbolize_names: true) end + + def ratelimit + return @ratelimit unless @ratelimit.nil? + + limit = headers['X-RateLimit-Limit'] + remaining = headers['X-RateLimit-Remaining'] + reset = headers['X-RateLimit-Reset'] + + # Guard against possibility that one (or probably, all) of the + # needed headers were not returned. + @ratelimit = Ratelimit.new(limit, remaining, reset) if limit && remaining && reset + + @ratelimit + end end # A simple REST client. diff --git a/test/test_ruby_http_client.rb b/test/test_ruby_http_client.rb index e417b7a..6ae8f46 100644 --- a/test/test_ruby_http_client.rb +++ b/test/test_ruby_http_client.rb @@ -12,6 +12,18 @@ def initialize(response) end end +class MockHttpResponse + attr_reader :code, :body, :headers + + def initialize(code, body, headers) + @code = code + @body = body + @headers = headers + end + + alias to_hash headers +end + class MockResponseWithRequestBody < MockResponse attr_reader :request_body @@ -232,6 +244,34 @@ def test__ assert_equal(['test'], url1.url_path) end + def test_ratelimit_core + expiry = Time.now.to_i + 1 + rl = SendGrid::Response::Ratelimit.new(500, 100, expiry) + rl2 = SendGrid::Response::Ratelimit.new(500, 0, expiry) + + refute rl.exceeded? + assert rl2.exceeded? + + assert_equal(rl.used, 400) + assert_equal(rl2.used, 500) + end + + def test_response_ratelimit_parsing + headers = { + 'X-RateLimit-Limit' => '500', + 'X-RateLimit-Remaining' => '300', + 'X-RateLimit-Reset' => Time.now.to_i.to_s + } + + body = '' + code = 204 + http_response = MockHttpResponse.new(code, body, headers) + response = SendGrid::Response.new(http_response) + + refute_nil response.ratelimit + refute response.ratelimit.exceeded? + end + def test_method_missing response = @client.get assert_equal(200, response.status_code)