From 14ad942ea8454cf98d892c245d0c6642a69fc64b Mon Sep 17 00:00:00 2001 From: Haacked Date: Fri, 18 Oct 2013 14:49:00 -0700 Subject: [PATCH] RateLimit class encapsulates rate limit headers Added a RateLimit class to encapsulate pulling rate limit information from the headers. This is now exposed by ApiInfo as well as the RateLimitExceeededException. That way these two classes are not grabbing the same information in different ways which we were doing before. --- .../Clients/MiscellaneousClientTests.cs | 4 +- .../RateLimitExceededExceptionTests.cs | 15 ++- Octokit.Tests/Http/ApiConnectionTests.cs | 2 +- Octokit.Tests/Http/ApiInfoParserTests.cs | 15 +-- Octokit.Tests/Http/RateLimitTests.cs | 114 ++++++++++++++++++ .../Models/ReadOnlyPagedCollectionTests.cs | 2 +- Octokit.Tests/Octokit.Tests.csproj | 1 + Octokit.Tests/OctokitRT.Tests.csproj | 1 + .../ObservableRepositoriesClientTests.cs | 21 ++-- .../Exceptions/RateLimitExceededException.cs | 54 ++++----- Octokit/Http/ApiInfo.cs | 19 +-- Octokit/Http/ApiInfoParser.cs | 51 ++++---- Octokit/Http/Connection.cs | 4 +- Octokit/Http/RateLimit.cs | 75 ++++++++++++ Octokit/Octokit.csproj | 1 + Octokit/OctokitRT.csproj | 1 + 16 files changed, 281 insertions(+), 99 deletions(-) create mode 100644 Octokit.Tests/Http/RateLimitTests.cs create mode 100644 Octokit/Http/RateLimit.cs diff --git a/Octokit.Tests/Clients/MiscellaneousClientTests.cs b/Octokit.Tests/Clients/MiscellaneousClientTests.cs index c2cd62b0..ede1479d 100644 --- a/Octokit.Tests/Clients/MiscellaneousClientTests.cs +++ b/Octokit.Tests/Clients/MiscellaneousClientTests.cs @@ -18,7 +18,7 @@ namespace Octokit.Tests.Clients var scopes = new List(); IResponse response = new ApiResponse { - ApiInfo = new ApiInfo(links, scopes, scopes, "", 1, 1), + ApiInfo = new ApiInfo(links, scopes, scopes, "", new RateLimit(new Dictionary())), Body = "Test" }; var connection = Substitute.For(); @@ -46,7 +46,7 @@ namespace Octokit.Tests.Clients var scopes = new List(); IResponse> response = new ApiResponse> { - ApiInfo = new ApiInfo(links, scopes, scopes, "", 1, 1), + ApiInfo = new ApiInfo(links, scopes, scopes, "", new RateLimit(new Dictionary())), BodyAsObject = new Dictionary { { "foo", "http://example.com/foo.gif" }, diff --git a/Octokit.Tests/Exceptions/RateLimitExceededExceptionTests.cs b/Octokit.Tests/Exceptions/RateLimitExceededExceptionTests.cs index b62479d5..00388580 100644 --- a/Octokit.Tests/Exceptions/RateLimitExceededExceptionTests.cs +++ b/Octokit.Tests/Exceptions/RateLimitExceededExceptionTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; @@ -19,6 +20,7 @@ namespace Octokit.Tests.Exceptions response.Headers.Add("X-RateLimit-Limit", "100"); response.Headers.Add("X-RateLimit-Remaining", "42"); response.Headers.Add("X-RateLimit-Reset", "1372700873"); + response.ApiInfo = CreateApiInfo(response); var exception = new RateLimitExceededException(response); Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode); @@ -40,6 +42,7 @@ namespace Octokit.Tests.Exceptions response.Headers.Add("X-RateLimit-Limit", "XXX"); response.Headers.Add("X-RateLimit-Remaining", "XXXX"); response.Headers.Add("X-RateLimit-Reset", "XXXX"); + response.ApiInfo = CreateApiInfo(response); var exception = new RateLimitExceededException(response); Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode); @@ -55,7 +58,11 @@ namespace Octokit.Tests.Exceptions [Fact] public void HandlesMissingHeaderValues() { - var response = new ApiResponse { StatusCode = HttpStatusCode.Forbidden }; + var response = new ApiResponse + { + StatusCode = HttpStatusCode.Forbidden + }; + response.ApiInfo = CreateApiInfo(response); var exception = new RateLimitExceededException(response); Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode); @@ -76,6 +83,7 @@ namespace Octokit.Tests.Exceptions response.Headers.Add("X-RateLimit-Limit", "100"); response.Headers.Add("X-RateLimit-Remaining", "42"); response.Headers.Add("X-RateLimit-Reset", "1372700873"); + response.ApiInfo = CreateApiInfo(response); var exception = new RateLimitExceededException(response); @@ -98,5 +106,10 @@ namespace Octokit.Tests.Exceptions } #endif } + + static ApiInfo CreateApiInfo(IResponse response) + { + return new ApiInfo(new Dictionary(), new List(), new List(), "etag", new RateLimit(response.Headers) ); + } } } diff --git a/Octokit.Tests/Http/ApiConnectionTests.cs b/Octokit.Tests/Http/ApiConnectionTests.cs index e0df6362..e240cec3 100644 --- a/Octokit.Tests/Http/ApiConnectionTests.cs +++ b/Octokit.Tests/Http/ApiConnectionTests.cs @@ -71,7 +71,7 @@ namespace Octokit.Tests.Http var scopes = new List(); IResponse> response = new ApiResponse> { - ApiInfo = new ApiInfo(links, scopes, scopes, "etag", 1, 1), + ApiInfo = new ApiInfo(links, scopes, scopes, "etag", new RateLimit(new Dictionary())), BodyAsObject = new List {new object(), new object()} }; var connection = Substitute.For(); diff --git a/Octokit.Tests/Http/ApiInfoParserTests.cs b/Octokit.Tests/Http/ApiInfoParserTests.cs index 8b5d7f48..2119854a 100644 --- a/Octokit.Tests/Http/ApiInfoParserTests.cs +++ b/Octokit.Tests/Http/ApiInfoParserTests.cs @@ -25,16 +25,15 @@ namespace Octokit.Tests { "ETag", "5634b0b187fd2e91e3126a75006cc4fa" } } }; - var parser = new ApiInfoParser(); - parser.ParseApiHttpHeaders(response); + ApiInfoParser.ParseApiHttpHeaders(response); var apiInfo = response.ApiInfo; Assert.NotNull(apiInfo); Assert.Equal(new[] { "user" }, apiInfo.AcceptedOauthScopes.ToArray()); Assert.Equal(new[] { "user", "public_repo", "repo", "gist" }, apiInfo.OauthScopes.ToArray()); - Assert.Equal(5000, apiInfo.RateLimit); - Assert.Equal(4997, apiInfo.RateLimitRemaining); + Assert.Equal(5000, apiInfo.RateLimit.Limit); + Assert.Equal(4997, apiInfo.RateLimit.Remaining); Assert.Equal("5634b0b187fd2e91e3126a75006cc4fa", apiInfo.Etag); } @@ -52,9 +51,8 @@ namespace Octokit.Tests } } }; - var parser = new ApiInfoParser(); - parser.ParseApiHttpHeaders(response); + ApiInfoParser.ParseApiHttpHeaders(response); var apiInfo = response.ApiInfo; Assert.NotNull(apiInfo); @@ -77,9 +75,8 @@ namespace Octokit.Tests } } }; - var parser = new ApiInfoParser(); - parser.ParseApiHttpHeaders(response); + ApiInfoParser.ParseApiHttpHeaders(response); var apiInfo = response.ApiInfo; Assert.NotNull(apiInfo); @@ -138,7 +135,7 @@ namespace Octokit.Tests static ApiInfo BuildApiInfo(IDictionary links) { - return new ApiInfo(links, new List(), new List(), "etag", 0, 0); + return new ApiInfo(links, new List(), new List(), "etag", new RateLimit(new Dictionary())); } } } diff --git a/Octokit.Tests/Http/RateLimitTests.cs b/Octokit.Tests/Http/RateLimitTests.cs new file mode 100644 index 00000000..3b0db944 --- /dev/null +++ b/Octokit.Tests/Http/RateLimitTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using Xunit; + +namespace Octokit.Tests.Http +{ + public class RateLimitTests + { + public class TheConstructor + { + public void Foo() + { + Console.WriteLine(new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks); + } + + [Fact] + public void ParsesRateLimitsFromHeaders() + { + var headers = new Dictionary + { + { "X-RateLimit-Limit", "100" }, + { "X-RateLimit-Remaining", "42" }, + { "X-RateLimit-Reset", "1372700873" } + }; + + var rateLimit = new RateLimit(headers); + + Assert.Equal(100, rateLimit.Limit); + Assert.Equal(42, rateLimit.Remaining); + var expectedReset = DateTimeOffset.ParseExact( + "Mon 01 Jul 2013 5:47:53 PM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, rateLimit.Reset); + } + + [Fact] + public void HandlesInvalidHeaderValues() + { + var headers = new Dictionary + { + { "X-RateLimit-Limit", "1234scoobysnacks1234" }, + { "X-RateLimit-Remaining", "xanadu" }, + { "X-RateLimit-Reset", "garbage" } + }; + + var rateLimit = new RateLimit(headers); + + Assert.Equal(0, rateLimit.Limit); + Assert.Equal(0, rateLimit.Remaining); + var expectedReset = DateTimeOffset.ParseExact( + "Thu 01 Jan 1970 0:00:00 AM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, rateLimit.Reset); + } + + [Fact] + public void HandlesMissingHeaderValues() + { + var headers = new Dictionary(); + + var rateLimit = new RateLimit(headers); + + Assert.Equal(0, rateLimit.Limit); + Assert.Equal(0, rateLimit.Remaining); + var expectedReset = DateTimeOffset.ParseExact( + "Thu 01 Jan 1970 0:00:00 AM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, rateLimit.Reset); + } + +#if !NETFX_CORE + [Fact] + public void CanPopulateObjectFromSerializedData() + { + var headers = new Dictionary + { + { "X-RateLimit-Limit", "100" }, + { "X-RateLimit-Remaining", "42" }, + { "X-RateLimit-Reset", "1372700873" } + }; + + var rateLimit = new RateLimit(headers); + + using (var stream = new MemoryStream()) + { + var formatter = new BinaryFormatter(); + formatter.Serialize(stream, rateLimit); + stream.Position = 0; + var deserialized = (RateLimit) formatter.Deserialize(stream); + + Assert.Equal(100, deserialized.Limit); + Assert.Equal(42, deserialized.Remaining); + var expectedReset = DateTimeOffset.ParseExact( + "Mon 01 Jul 2013 5:47:53 PM -00:00", + "ddd dd MMM yyyy h:mm:ss tt zzz", + CultureInfo.InvariantCulture); + Assert.Equal(expectedReset, deserialized.Reset); + } + } +#endif + [Fact] + public void EnsuresHeadersNotNull() + { + Assert.Throws(() => new RateLimit(null)); + } + } + } +} \ No newline at end of file diff --git a/Octokit.Tests/Models/ReadOnlyPagedCollectionTests.cs b/Octokit.Tests/Models/ReadOnlyPagedCollectionTests.cs index 9a7e4de1..b454f827 100644 --- a/Octokit.Tests/Models/ReadOnlyPagedCollectionTests.cs +++ b/Octokit.Tests/Models/ReadOnlyPagedCollectionTests.cs @@ -23,7 +23,7 @@ namespace Octokit.Tests.Models var response = new ApiResponse> { BodyAsObject = new List(), - ApiInfo = new ApiInfo(links, scopes, scopes, "etag", 100, 100) + ApiInfo = new ApiInfo(links, scopes, scopes, "etag", new RateLimit(new Dictionary())) }; var connection = Substitute.For(); connection.GetAsync>(nextPageUrl, null, null).Returns(nextPageResponse); diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index ee908dd7..e5f5608c 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -87,6 +87,7 @@ + diff --git a/Octokit.Tests/OctokitRT.Tests.csproj b/Octokit.Tests/OctokitRT.Tests.csproj index f54cdd7d..e084bcc3 100644 --- a/Octokit.Tests/OctokitRT.Tests.csproj +++ b/Octokit.Tests/OctokitRT.Tests.csproj @@ -78,6 +78,7 @@ + diff --git a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs index e0689d3f..983ea7f8 100644 --- a/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs +++ b/Octokit.Tests/Reactive/ObservableRepositoriesClientTests.cs @@ -48,7 +48,6 @@ namespace Octokit.Tests.Reactive var firstPageUrl = new Uri("user/repos", UriKind.Relative); var secondPageUrl = new Uri("https://example.com/page/2"); var firstPageLinks = new Dictionary {{"next", secondPageUrl}}; - var scopes = new List(); var firstPageResponse = new ApiResponse> { BodyAsObject = new List @@ -57,7 +56,7 @@ namespace Octokit.Tests.Reactive new Repository {Id = 2}, new Repository {Id = 3} }, - ApiInfo = new ApiInfo(firstPageLinks, scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(firstPageLinks) }; var thirdPageUrl = new Uri("https://example.com/page/3"); var secondPageLinks = new Dictionary {{"next", thirdPageUrl}}; @@ -69,7 +68,7 @@ namespace Octokit.Tests.Reactive new Repository {Id = 5}, new Repository {Id = 6} }, - ApiInfo = new ApiInfo(secondPageLinks, scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(secondPageLinks) }; var lastPageResponse = new ApiResponse> { @@ -77,7 +76,7 @@ namespace Octokit.Tests.Reactive { new Repository {Id = 7} }, - ApiInfo = new ApiInfo(new Dictionary(), scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(new Dictionary()) }; var gitHubClient = Substitute.For(); gitHubClient.Connection.GetAsync>(firstPageUrl) @@ -102,7 +101,6 @@ namespace Octokit.Tests.Reactive var firstPageUrl = new Uri("user/repos", UriKind.Relative); var secondPageUrl = new Uri("https://example.com/page/2"); var firstPageLinks = new Dictionary { { "next", secondPageUrl } }; - var scopes = new List(); var firstPageResponse = new ApiResponse> { BodyAsObject = new List @@ -111,7 +109,7 @@ namespace Octokit.Tests.Reactive new Repository {Id = 2}, new Repository {Id = 3} }, - ApiInfo = new ApiInfo(firstPageLinks, scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(firstPageLinks) }; var thirdPageUrl = new Uri("https://example.com/page/3"); var secondPageLinks = new Dictionary { { "next", thirdPageUrl } }; @@ -123,7 +121,7 @@ namespace Octokit.Tests.Reactive new Repository {Id = 5}, new Repository {Id = 6} }, - ApiInfo = new ApiInfo(secondPageLinks, scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(secondPageLinks) }; var fourthPageUrl = new Uri("https://example.com/page/4"); var thirdPageLinks = new Dictionary { { "next", fourthPageUrl } }; @@ -133,7 +131,7 @@ namespace Octokit.Tests.Reactive { new Repository {Id = 7} }, - ApiInfo = new ApiInfo(thirdPageLinks, scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(thirdPageLinks) }; var lastPageResponse = new ApiResponse> { @@ -141,7 +139,7 @@ namespace Octokit.Tests.Reactive { new Repository {Id = 8} }, - ApiInfo = new ApiInfo(new Dictionary(), scopes, scopes, "etag", 100, 100) + ApiInfo = CreateApiInfo(new Dictionary()) }; var gitHubClient = Substitute.For(); gitHubClient.Connection.GetAsync>(firstPageUrl) @@ -163,5 +161,10 @@ namespace Octokit.Tests.Reactive gitHubClient.Connection.Received(0).GetAsync>(fourthPageUrl, null, null); } } + + static ApiInfo CreateApiInfo(IDictionary links) + { + return new ApiInfo(links, new List(), new List(), "etag", new RateLimit(new Dictionary())); + } } } diff --git a/Octokit/Exceptions/RateLimitExceededException.cs b/Octokit/Exceptions/RateLimitExceededException.cs index a9d98327..54a75580 100644 --- a/Octokit/Exceptions/RateLimitExceededException.cs +++ b/Octokit/Exceptions/RateLimitExceededException.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; +using Octokit.Internal; namespace Octokit { @@ -21,7 +23,7 @@ namespace Octokit Justification = "These exceptions are specific to the GitHub API and not general purpose exceptions")] public class RateLimitExceededException : ForbiddenException { - readonly int _resetUnixEpochSeconds; + readonly RateLimit _rateLimit; public RateLimitExceededException(IResponse response) : this(response, null) { @@ -30,27 +32,33 @@ namespace Octokit public RateLimitExceededException(IResponse response, Exception innerException) : base(response, innerException) { Ensure.ArgumentNotNull(response, "response"); - - Limit = ToInt32Safe(response, "X-RateLimit-Limit"); - Remaining = ToInt32Safe(response, "X-RateLimit-Remaining"); - _resetUnixEpochSeconds = ToInt32Safe(response, "X-RateLimit-Reset"); - Reset = FromUnixTime(_resetUnixEpochSeconds); + + _rateLimit = response.ApiInfo.RateLimit; } /// /// The maximum number of requests that the consumer is permitted to make per hour. /// - public int Limit { get; private set; } + public int Limit + { + get { return _rateLimit.Limit; } + } /// /// The number of requests remaining in the current rate limit window. /// - public int Remaining { get; private set; } + public int Remaining + { + get { return _rateLimit.Remaining; } + } /// - /// The time at which the current rate limit window resets + /// The date and time at which the current rate limit window resets /// - public DateTimeOffset Reset { get; private set; } + public DateTimeOffset Reset + { + get { return _rateLimit.Reset; } + } // TODO: Might be nice to have this provide a more detailed message such as what the limit is, // how many are remaining, and when it will reset. I'm too lazy to do it now. @@ -63,34 +71,16 @@ namespace Octokit protected RateLimitExceededException(SerializationInfo info, StreamingContext context) : base(info, context) { - Limit = info.GetInt32("Limit"); - Remaining = info.GetInt32("Remaining"); - _resetUnixEpochSeconds = info.GetInt32("Reset"); - Reset = FromUnixTime(_resetUnixEpochSeconds); + _rateLimit = info.GetValue("RateLimit", typeof(RateLimit)) as RateLimit + ?? new RateLimit(new Dictionary()); } public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); - info.AddValue("Limit", Limit); - info.AddValue("Remaining", Remaining); - info.AddValue("Reset", _resetUnixEpochSeconds); + + info.AddValue("RateLimit", _rateLimit); } #endif - - static DateTimeOffset FromUnixTime(long unixTime) - { - var epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - return epoch.AddSeconds(unixTime); - } - - static int ToInt32Safe(IResponse response, string key) - { - string value; - int result; - return !response.Headers.TryGetValue(key, out value) || value == null || !int.TryParse(value, out result) - ? 0 - : result; - } } } diff --git a/Octokit/Http/ApiInfo.cs b/Octokit/Http/ApiInfo.cs index 64b27ff1..8050a448 100644 --- a/Octokit/Http/ApiInfo.cs +++ b/Octokit/Http/ApiInfo.cs @@ -15,8 +15,7 @@ namespace Octokit IList oauthScopes, IList acceptedOauthScopes, string etag, - int rateLimit, - int rateLimitRemaining) + RateLimit rateLimit) { Ensure.ArgumentNotNull(links, "links"); Ensure.ArgumentNotNull(oauthScopes, "ouathScopes"); @@ -26,7 +25,6 @@ namespace Octokit AcceptedOauthScopes = new ReadOnlyCollection(acceptedOauthScopes); Etag = etag; RateLimit = rateLimit; - RateLimitRemaining = rateLimitRemaining; } /// @@ -44,19 +42,14 @@ namespace Octokit /// public string Etag { get; private set; } - /// - /// Rate limit in requests/hr. - /// - public int RateLimit { get; private set; } - - /// - /// Number of calls remaining before hitting the rate limit. - /// - public int RateLimitRemaining { get; private set; } - /// /// Links to things like next/previous pages /// public IReadOnlyDictionary Links { get; private set; } + + /// + /// Information about the API rate limit + /// + public RateLimit RateLimit { get; private set; } } } diff --git a/Octokit/Http/ApiInfoParser.cs b/Octokit/Http/ApiInfoParser.cs index 3ba08517..be87292f 100644 --- a/Octokit/Http/ApiInfoParser.cs +++ b/Octokit/Http/ApiInfoParser.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Text.RegularExpressions; namespace Octokit.Internal { - public class ApiInfoParser + internal static class ApiInfoParser { const RegexOptions regexOptions = #if NETFX_CORE @@ -15,58 +14,54 @@ namespace Octokit.Internal RegexOptions.Compiled | RegexOptions.IgnoreCase; #endif + static readonly Regex _linkRelRegex = new Regex("rel=\"(next|prev|first|last)\"", regexOptions); + static readonly Regex _linkUriRegex = new Regex("<(.+)>", regexOptions); - readonly Regex _linkRelRegex = new Regex("rel=\"(next|prev|first|last)\"", regexOptions); - readonly Regex _linkUriRegex = new Regex("<(.+)>", regexOptions); - - public void ParseApiHttpHeaders(IResponse response) + public static void ParseApiHttpHeaders(IResponse response) { Ensure.ArgumentNotNull(response, "response"); response.ApiInfo = ParseHeaders(response); } - ApiInfo ParseHeaders(IResponse response) + static ApiInfo ParseHeaders(IResponse response) { + Ensure.ArgumentNotNull(response, "response"); + + return ParseResponseHeaders(response.Headers); + } + + static ApiInfo ParseResponseHeaders(IDictionary responseHeaders) + { + Ensure.ArgumentNotNull(responseHeaders, "responseHeaders"); + var httpLinks = new Dictionary(); var oauthScopes = new List(); var acceptedOauthScopes = new List(); - int rateLimit = 0; - int rateLimitRemaining = 0; string etag = null; - if (response.Headers.ContainsKey("X-Accepted-OAuth-Scopes")) + if (responseHeaders.ContainsKey("X-Accepted-OAuth-Scopes")) { - acceptedOauthScopes.AddRange(response.Headers["X-Accepted-OAuth-Scopes"] + acceptedOauthScopes.AddRange(responseHeaders["X-Accepted-OAuth-Scopes"] .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim())); } - if (response.Headers.ContainsKey("X-OAuth-Scopes")) + if (responseHeaders.ContainsKey("X-OAuth-Scopes")) { - oauthScopes.AddRange(response.Headers["X-OAuth-Scopes"] + oauthScopes.AddRange(responseHeaders["X-OAuth-Scopes"] .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(x => x.Trim())); } - if (response.Headers.ContainsKey("X-RateLimit-Limit")) + if (responseHeaders.ContainsKey("ETag")) { - rateLimit = Convert.ToInt32(response.Headers["X-RateLimit-Limit"], CultureInfo.InvariantCulture); + etag = responseHeaders["ETag"]; } - if (response.Headers.ContainsKey("X-RateLimit-Remaining")) + if (responseHeaders.ContainsKey("Link")) { - rateLimitRemaining = Convert.ToInt32(response.Headers["X-RateLimit-Remaining"], CultureInfo.InvariantCulture); - } - - if (response.Headers.ContainsKey("ETag")) - { - etag = response.Headers["ETag"]; - } - - if (response.Headers.ContainsKey("Link")) - { - var links = response.Headers["Link"].Split(','); + var links = responseHeaders["Link"].Split(','); foreach (var link in links) { var relMatch = _linkRelRegex.Match(link); @@ -79,7 +74,7 @@ namespace Octokit.Internal } } - return new ApiInfo(httpLinks, oauthScopes, acceptedOauthScopes, etag, rateLimit, rateLimitRemaining); + return new ApiInfo(httpLinks, oauthScopes, acceptedOauthScopes, etag, new RateLimit(responseHeaders)); } } } diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index 1a88410d..3bb0fc64 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -19,7 +19,6 @@ namespace Octokit readonly Authenticator _authenticator; readonly IHttpClient _httpClient; readonly JsonHttpPipeline _jsonPipeline; - readonly ApiInfoParser _apiInfoParser; public Connection(string userAgent) : this(userAgent, _defaultGitHubApiUrl, _anonymousCredentials) { @@ -67,7 +66,6 @@ namespace Octokit _authenticator = new Authenticator(credentialStore); _httpClient = httpClient; _jsonPipeline = new JsonHttpPipeline(); - _apiInfoParser = new ApiInfoParser(); } public async Task> GetAsync(Uri uri, IDictionary parameters, string accepts) @@ -224,7 +222,7 @@ namespace Octokit request.Headers.Add("User-Agent", UserAgent); await _authenticator.Apply(request); var response = await _httpClient.Send(request); - _apiInfoParser.ParseApiHttpHeaders(response); + ApiInfoParser.ParseApiHttpHeaders(response); HandleErrors(response); return response; } diff --git a/Octokit/Http/RateLimit.cs b/Octokit/Http/RateLimit.cs new file mode 100644 index 00000000..28830ef7 --- /dev/null +++ b/Octokit/Http/RateLimit.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Octokit +{ +#if !NETFX_CORE + [Serializable] +#endif + public class RateLimit +#if !NETFX_CORE + : ISerializable +#endif + { + const long _unixEpochTicks = 621355968000000000; // Unix Epoch is January 1, 1970 00:00 -0:00 + + public RateLimit(IDictionary responseHeaders) + { + Ensure.ArgumentNotNull(responseHeaders, "responseHeaders"); + + Limit = (int) GetHeaderValueAsInt32Safe(responseHeaders, "X-RateLimit-Limit"); + Remaining = (int) GetHeaderValueAsInt32Safe(responseHeaders, "X-RateLimit-Remaining"); + Reset = FromUnixTime(GetHeaderValueAsInt32Safe(responseHeaders, "X-RateLimit-Reset")); + } + + /// + /// The maximum number of requests that the consumer is permitted to make per hour. + /// + public int Limit { get; private set; } + + /// + /// The number of requests remaining in the current rate limit window. + /// + public int Remaining { get; private set; } + + /// + /// The date and time at which the current rate limit window resets + /// + public DateTimeOffset Reset { get; private set; } + + static long GetHeaderValueAsInt32Safe(IDictionary responseHeaders, string key) + { + string value; + long result; + return !responseHeaders.TryGetValue(key, out value) || value == null || !long.TryParse(value, out result) + ? 0 + : result; + } + + static DateTimeOffset FromUnixTime(long unixTime) + { + return new DateTimeOffset(unixTime*TimeSpan.TicksPerSecond + _unixEpochTicks, TimeSpan.Zero); + } + +#if !NETFX_CORE + protected RateLimit(SerializationInfo info, StreamingContext context) + { + Ensure.ArgumentNotNull(info, "info"); + + Limit = info.GetInt32("Limit"); + Remaining = info.GetInt32("Remaining"); + Reset = new DateTimeOffset(info.GetInt64("Reset"), TimeSpan.Zero); + } + + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + Ensure.ArgumentNotNull(info, "info"); + + info.AddValue("Limit", Limit); + info.AddValue("Remaining", Remaining); + info.AddValue("Reset", Reset.Ticks); + } +#endif + } +} diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index eaa98466..8caab489 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -83,6 +83,7 @@ + diff --git a/Octokit/OctokitRT.csproj b/Octokit/OctokitRT.csproj index 0579744a..41f99411 100644 --- a/Octokit/OctokitRT.csproj +++ b/Octokit/OctokitRT.csproj @@ -137,6 +137,7 @@ +