using System.Globalization; using System.Net.Http; using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; namespace OpenNest.Data; /// Wire conventions shared by the nest server's HTTP clients. internal static class NestServerHttp { /// The server's JSON shape: camel-case names and enum names. public static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true, Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; /// Guards a write on the revision the caller last read. public static void AddIfMatch(HttpRequestMessage request, int expectedRevision) => request.Headers.IfMatch.Add(new EntityTagHeaderValue($"\"{expectedRevision.ToString(CultureInfo.InvariantCulture)}\"")); public static void ValidatePaging(int offset, int limit, int maxLimit) { if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); if (limit < 1 || limit > maxLimit) throw new ArgumentOutOfRangeException(nameof(limit)); } public static string Paging(int offset, int limit) => $"offset={offset.ToString(CultureInfo.InvariantCulture)}&limit={limit.ToString(CultureInfo.InvariantCulture)}"; /// Whether a page is the one asked for and holds no more than its window and total allow. public static bool IsBoundedPage(int count, int total, int pageOffset, int pageLimit, int offset, int limit) => count <= limit && pageOffset == offset && pageLimit == limit && total >= 0 && count <= System.Math.Max(0L, (long)total - offset); /// Whether a history page holds only revisions of , newest first. public static bool IsRevisionHistory(IReadOnlyList items, Guid id, Func idOf, Func revisionOf) where T : class => !items.Any(item => item is null || revisionOf(item) < 1 || idOf(item) != id) && !items.Zip(items.Skip(1)).Any(pair => revisionOf(pair.First) <= revisionOf(pair.Second)); /// Throws an naming the action, status and the start of the body. public static async Task EnsureSuccessAsync(HttpResponseMessage response, string action, CancellationToken cancellationToken) { if (response.IsSuccessStatusCode) return; var detail = ""; try { detail = await response.Content.ReadAsStringAsync(cancellationToken); } catch (Exception ex) when (ex is IOException or InvalidOperationException) { // The status code is the useful part; the body is best-effort. } if (detail.Length > 200) detail = detail[..200] + "…"; throw new IOException( $"Could not {action} on the nest server: HTTP {(int)response.StatusCode} {response.ReasonPhrase}. {detail}".TrimEnd()); } }