diff --git a/build/analyzers.ruleset b/build/analyzers.ruleset index c3d0f539a2..79627376f3 100644 --- a/build/analyzers.ruleset +++ b/build/analyzers.ruleset @@ -1071,7 +1071,7 @@ - + @@ -1091,7 +1091,7 @@ - + @@ -1130,7 +1130,7 @@ - + diff --git a/src/Tgstation.Server.Api/Models/Instance.cs b/src/Tgstation.Server.Api/Models/Instance.cs index 0349ce9fce..e7afce2eaa 100644 --- a/src/Tgstation.Server.Api/Models/Instance.cs +++ b/src/Tgstation.Server.Api/Models/Instance.cs @@ -50,7 +50,10 @@ namespace Tgstation.Server.Api.Models [NotMapped] public Job MoveJob { get; set; } - /// + /// + /// Create a clone of the essential metadata + /// + /// A clone of the essential metadata public Instance CloneMetadata() => new Instance { Id = Id, diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs index b5d3b5f9a3..51d8025f73 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs @@ -13,7 +13,7 @@ /// /// Gets the associated with the /// - /// + /// The associated with the public abstract override string ToString(); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs index b0bbb0db23..76dbe54ce2 100644 --- a/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs +++ b/src/Tgstation.Server.Api/Models/Internal/TestMergeBase.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// Construct a from a /// - /// + /// The to copy data from protected TestMergeBase(TestMergeBase copy) { if (copy == null) diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index 452bcf1e23..3d01a67871 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -49,6 +49,79 @@ namespace Tgstation.Server.Client /// ApiHeaders headers; + static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new[] { new VersionConverter() } + }; + + static void HandleBadResponse(HttpResponseMessage response, string json) + { + ErrorMessage errorMessage = null; + try + { + // check if json serializes to an error message + errorMessage = JsonConvert.DeserializeObject(json, GetSerializerSettings()); + } + catch (JsonException) { } + + const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!"; + +#pragma warning disable IDE0010 // Add missing cases + switch (response.StatusCode) +#pragma warning restore IDE0010 // Add missing cases + { + case HttpStatusCode.UpgradeRequired: + throw new ApiMismatchException(errorMessage ?? new ErrorMessage + { + Message = "API Mismatch but no current API version provided!" + BadSpecExtension, + SeverApiVersion = null + }); + case HttpStatusCode.Unauthorized: + throw new UnauthorizedException(); + case HttpStatusCode.RequestTimeout: + throw new RequestTimeoutException(); + case HttpStatusCode.Forbidden: + throw new InsufficientPermissionsException(); + case HttpStatusCode.ServiceUnavailable: + throw new ServiceUnavailableException(); + case HttpStatusCode.Gone: + errorMessage = errorMessage ?? new ErrorMessage + { + Message = "The requested resource could not be found!", + SeverApiVersion = null + }; + goto case HttpStatusCode.Conflict; + case HttpStatusCode.NotFound: + // our fault somehow + errorMessage = errorMessage ?? new ErrorMessage + { + Message = "This is not a valid route!" + BadSpecExtension, + SeverApiVersion = null + }; + goto case HttpStatusCode.Conflict; + case HttpStatusCode.Conflict: + throw new ConflictException(errorMessage ?? new ErrorMessage + { + Message = "An undescribed conflict occurred!" + BadSpecExtension, + SeverApiVersion = null + }, response.StatusCode); + case HttpStatusCode.NotImplemented: + // unprocessable entity + case (HttpStatusCode)422: + throw new MethodNotSupportedException(); + case HttpStatusCode.InternalServerError: + // response json is html + throw new ServerErrorException(json); + case (HttpStatusCode)429: + // rate limited + response.Headers.TryGetValues("Retry-After", out var values); + throw new RateLimitException(values?.FirstOrDefault()); + default: + throw new ApiConflictException(errorMessage, response.StatusCode); + } + } + /// /// Construct an /// @@ -60,7 +133,7 @@ namespace Tgstation.Server.Client this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); Url = url ?? throw new ArgumentNullException(nameof(url)); headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders)); - + requestLoggers = new List(); } @@ -70,10 +143,11 @@ namespace Tgstation.Server.Client /// /// Main request method /// + /// The resulting POCO type /// The route to run /// The body of the request /// The method of the request - /// The optional for the request + /// The optional for the request /// The for the operation /// A resulting in the response on success async Task RunRequest(string route, object body, HttpMethod method, long? instanceId, CancellationToken cancellationToken) @@ -85,102 +159,42 @@ namespace Tgstation.Server.Client if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put)) throw new InvalidOperationException("Body cannot be null for POST or PUT!"); + HttpResponseMessage response; var fullUri = new Uri(Url, route); - - var message = new HttpRequestMessage(method, fullUri); - - var serializerSettings = new JsonSerializerSettings + var serializerSettings = GetSerializerSettings(); + using (var request = new HttpRequestMessage(method, fullUri)) { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Converters = new[] { new VersionConverter() } - }; + if (body != null) + request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson); - if (body != null) - message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson); + headers.SetRequestHeaders(request.Headers, instanceId); - headers.SetRequestHeaders(message.Headers, instanceId); + await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false); - await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(message, cancellationToken))).ConfigureAwait(false); + response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + } - var response = await httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false); - - await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false); - - var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - if (!response.IsSuccessStatusCode) + using (response) { - ErrorMessage errorMessage = null; + await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false); + + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + HandleBadResponse(response, json); + + if (String.IsNullOrWhiteSpace(json)) + json = JsonConvert.SerializeObject(new object()); + try { - //check if json serializes to an error message - errorMessage = JsonConvert.DeserializeObject(json, serializerSettings); + return JsonConvert.DeserializeObject(json, serializerSettings); } - catch (JsonException) { } - - const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!"; - - switch (response.StatusCode) + catch (JsonException) { - case HttpStatusCode.UpgradeRequired: - throw new ApiMismatchException(errorMessage ?? new ErrorMessage - { - Message = "API Mismatch but no current API version provided!" + BadSpecExtension, - SeverApiVersion = null - }); - case HttpStatusCode.Unauthorized: - throw new UnauthorizedException(); - case HttpStatusCode.RequestTimeout: - throw new RequestTimeoutException(); - case HttpStatusCode.Forbidden: - throw new InsufficientPermissionsException(); - case HttpStatusCode.ServiceUnavailable: - throw new ServiceUnavailableException(); - case HttpStatusCode.Gone: - errorMessage = errorMessage ?? new ErrorMessage - { - Message = "The requested resource could not be found!", - SeverApiVersion = null - }; - goto case HttpStatusCode.Conflict; - case HttpStatusCode.NotFound: //our fault somehow - errorMessage = errorMessage ?? new ErrorMessage - { - Message = "This is not a valid route!" + BadSpecExtension, - SeverApiVersion = null - }; - goto case HttpStatusCode.Conflict; - case HttpStatusCode.Conflict: - throw new ConflictException(errorMessage ?? new ErrorMessage - { - Message = "An undescribed conflict occurred!" + BadSpecExtension, - SeverApiVersion = null - }, response.StatusCode); - case HttpStatusCode.NotImplemented: - case (HttpStatusCode)422: //unprocessable entity - throw new MethodNotSupportedException(); - case HttpStatusCode.InternalServerError: - //response - throw new ServerErrorException(json); //json is html - case (HttpStatusCode)429: //rate limited - response.Headers.TryGetValues("Retry-After", out var values); - throw new RateLimitException(values?.FirstOrDefault()); - default: - throw new ApiConflictException(errorMessage, response.StatusCode); + throw new UnrecognizedResponseException(json, response.StatusCode); } } - - if (String.IsNullOrWhiteSpace(json)) - json = JsonConvert.SerializeObject(new object()); - - try - { - return JsonConvert.DeserializeObject(json, serializerSettings); - } - catch (JsonException) - { - throw new UnrecognizedResponseException(json, response.StatusCode); - } } /// diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index 92c2f8aba5..f3675dc265 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs index 5c303f5f77..f7890f1f22 100644 --- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs @@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index 1f1af74469..54dc6acf39 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// diff --git a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs index e077ea142d..6c2d5869d2 100644 --- a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs @@ -13,6 +13,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// diff --git a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs index c3ceeca108..440340b6c4 100644 --- a/src/Tgstation.Server.Client/Components/InstanceUserClient.cs +++ b/src/Tgstation.Server.Client/Components/InstanceUserClient.cs @@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// @@ -33,6 +34,7 @@ namespace Tgstation.Server.Client.Components /// public Task Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken); + /// public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceUser, instanceUser.UserId.Value), instance.Id, cancellationToken); /// diff --git a/src/Tgstation.Server.Client/Components/JobsClient.cs b/src/Tgstation.Server.Client/Components/JobsClient.cs index 59cdbc22ba..d6c421e65d 100644 --- a/src/Tgstation.Server.Client/Components/JobsClient.cs +++ b/src/Tgstation.Server.Client/Components/JobsClient.cs @@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// diff --git a/src/Tgstation.Server.Client/Components/RepositoryClient.cs b/src/Tgstation.Server.Client/Components/RepositoryClient.cs index 18e332fd09..97d4cdfec5 100644 --- a/src/Tgstation.Server.Client/Components/RepositoryClient.cs +++ b/src/Tgstation.Server.Client/Components/RepositoryClient.cs @@ -13,6 +13,7 @@ namespace Tgstation.Server.Client.Components /// The for the /// readonly IApiClient apiClient; + /// /// The for the /// @@ -22,7 +23,7 @@ namespace Tgstation.Server.Client.Components /// Construct a /// /// The value of - /// + /// The value of public RepositoryClient(IApiClient apiClient, Instance instance) { this.apiClient = apiClient; diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index 7eb8538156..ab833b1731 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -10,10 +10,19 @@ namespace Tgstation.Server.Client /// interface IApiClient : IDisposable { + /// + /// The the uses + /// ApiHeaders Headers { get; set; } + /// + /// The pointing the tgstation-server + /// Uri Url { get; } + /// + /// The request timeout + /// TimeSpan Timeout { get; set; } /// @@ -22,21 +31,155 @@ namespace Tgstation.Server.Client /// The to add void AddRequestLogger(IRequestLogger requestLogger); + /// + /// Run an HTTP PUT request + /// + /// The type to of the request body + /// The type of the response body + /// The server route to make the request to + /// The request body + /// The for the operation + /// A resulting in the response body as a Task Create(string route, TBody body, CancellationToken cancellationToken); + + /// + /// Run an HTTP PUT request + /// + /// The type of the response body + /// The server route to make the request to + /// The for the operation + /// A resulting in the response body as a Task Create(string route, CancellationToken cancellationToken); + + /// + /// Run an HTTP GET request + /// + /// The type of the response body + /// The server route to make the request to + /// The for the operation + /// A resulting in the response body as a Task Read(string route, CancellationToken cancellationToken); + + /// + /// Run an HTTP POST request + /// + /// The type to of the request body + /// The type of the response body + /// The server route to make the request to + /// The request body + /// The for the operation + /// A resulting in the response body as a Task Update(string route, TBody body, CancellationToken cancellationToken); + + /// + /// Run an HTTP POST request + /// + /// The type of the response body + /// The server route to make the request to + /// The for the operation + /// A resulting in the response body as a Task Update(string route, CancellationToken cancellationToken); + + /// + /// Run an HTTP POST request + /// + /// The type to of the request body + /// The server route to make the request to + /// The request body + /// The for the operation + /// A representing the running operation Task Update(string route, TBody body, CancellationToken cancellationToken); + + /// + /// Run an HTTP DELETE request + /// + /// The server route to make the request to + /// The for the operation + /// A representing the running operation Task Delete(string route, CancellationToken cancellationToken); + /// + /// Run an HTTP PUT request + /// + /// The type to of the request body + /// The type of the response body + /// The server route to make the request to + /// The request body + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP PUT request + /// + /// The type of the response body + /// The server route to make the request to + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Create(string route, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP PATCH request + /// + /// The type of the response body + /// The server route to make the request to + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Patch(string route, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP GET request + /// + /// The type of the response body + /// The server route to make the request to + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Read(string route, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP POST request + /// + /// The type to of the request body + /// The type of the response body + /// The server route to make the request to + /// The request body + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP DELETE request + /// + /// The server route to make the request to + /// The to make the request to + /// The for the operation + /// A representing the running operation Task Delete(string route, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP DELETE request + /// + /// The type to of the request body + /// The server route to make the request to + /// The request body + /// The to make the request to + /// The for the operation + /// A representing the running operation Task Delete(string route, TBody body, long instanceId, CancellationToken cancellationToken); + + /// + /// Run an HTTP DELETE request + /// + /// The type of the response body + /// The server route to make the request to + /// The to make the request to + /// The for the operation + /// A resulting in the response body as a Task Delete(string route, long instanceId, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/IServerClient.cs b/src/Tgstation.Server.Client/IServerClient.cs index 9432ea9afd..685606df7d 100644 --- a/src/Tgstation.Server.Client/IServerClient.cs +++ b/src/Tgstation.Server.Client/IServerClient.cs @@ -43,6 +43,8 @@ namespace Tgstation.Server.Client /// /// The of the /// + /// The for the operation + /// A resulting in the of the target server Task Version(CancellationToken cancellationToken); /// diff --git a/src/Tgstation.Server.Client/IServerClientFactory.cs b/src/Tgstation.Server.Client/IServerClientFactory.cs index df0427efef..32816de376 100644 --- a/src/Tgstation.Server.Client/IServerClientFactory.cs +++ b/src/Tgstation.Server.Client/IServerClientFactory.cs @@ -16,8 +16,8 @@ namespace Tgstation.Server.Client /// The URL to access TGS /// The username to for the /// The password for the - /// The for the operation /// The representing timeout for the connection + /// The for the operation /// A resulting in a new Task CreateServerClient(Uri host, string username, string password, TimeSpan timeout = default, CancellationToken cancellationToken = default); diff --git a/src/Tgstation.Server.Client/InstanceManagerClient.cs b/src/Tgstation.Server.Client/InstanceManagerClient.cs index 0aad027e0b..62d99de98d 100644 --- a/src/Tgstation.Server.Client/InstanceManagerClient.cs +++ b/src/Tgstation.Server.Client/InstanceManagerClient.cs @@ -24,7 +24,7 @@ namespace Tgstation.Server.Client /// /// Construct an /// - /// + /// The value of public InstanceManagerClient(IApiClient apiClient) { this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient)); @@ -55,6 +55,7 @@ namespace Tgstation.Server.Client client = new InstanceClient(apiClient, instance); cachedClients.Add(instance.Id, client); } + return client; } } diff --git a/src/Tgstation.Server.Client/ServerClientFactory.cs b/src/Tgstation.Server.Client/ServerClientFactory.cs index b0a7d3b547..62eec5644c 100644 --- a/src/Tgstation.Server.Client/ServerClientFactory.cs +++ b/src/Tgstation.Server.Client/ServerClientFactory.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Client /// /// The for the /// - static readonly IApiClientFactory apiClientFactory = new ApiClientFactory(); + static readonly IApiClientFactory ApiClientFactory = new ApiClientFactory(); /// /// The for the @@ -40,12 +40,13 @@ namespace Tgstation.Server.Client throw new ArgumentNullException(nameof(password)); Token token; - using (var api = apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password))) + using (var api = ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password))) { if (timeout != default) api.Timeout = timeout; token = await api.Update(Routes.Root, cancellationToken).ConfigureAwait(false); } + return CreateServerClient(host, token, timeout); } @@ -56,7 +57,7 @@ namespace Tgstation.Server.Client throw new ArgumentNullException(nameof(host)); if (token == null) throw new ArgumentNullException(nameof(token)); - var result = new ServerClient(apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token); + var result = new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token); if (timeout != default) result.Timeout = timeout; return result;