From 50320f85912f66edbf8e246af458906626579bff Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Tue, 7 Oct 2025 02:02:40 +0300 Subject: [PATCH] feat: implement Bitip.Client with API client, options, and response models --- src/clients/dotnet/Bitip.Client/Class1.cs | 7 --- .../Bitip.Client/Constants/ApiConstants.cs | 16 ++++++ .../DependencyInjectionExtension.cs | 27 +++++++++ .../Extensions/HttpMessageExtensions.cs | 24 ++++++++ .../Bitip.Client/Models/BitipOptions.cs | 10 ++++ .../dotnet/Bitip.Client/Models/SystemDtos.cs | 28 ++++++++++ .../Bitip.Client/Services/BitipClient.cs | 55 +++++++++++++++++++ .../Bitip.Client/Services/IBitipClient.cs | 14 +++++ 8 files changed, 174 insertions(+), 7 deletions(-) delete mode 100644 src/clients/dotnet/Bitip.Client/Class1.cs create mode 100644 src/clients/dotnet/Bitip.Client/Constants/ApiConstants.cs create mode 100644 src/clients/dotnet/Bitip.Client/DependencyInjectionExtension.cs create mode 100644 src/clients/dotnet/Bitip.Client/Extensions/HttpMessageExtensions.cs create mode 100644 src/clients/dotnet/Bitip.Client/Models/BitipOptions.cs create mode 100644 src/clients/dotnet/Bitip.Client/Models/SystemDtos.cs create mode 100644 src/clients/dotnet/Bitip.Client/Services/BitipClient.cs create mode 100644 src/clients/dotnet/Bitip.Client/Services/IBitipClient.cs diff --git a/src/clients/dotnet/Bitip.Client/Class1.cs b/src/clients/dotnet/Bitip.Client/Class1.cs deleted file mode 100644 index 4b9f758..0000000 --- a/src/clients/dotnet/Bitip.Client/Class1.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bitip.Client -{ - public class Class1 - { - - } -} diff --git a/src/clients/dotnet/Bitip.Client/Constants/ApiConstants.cs b/src/clients/dotnet/Bitip.Client/Constants/ApiConstants.cs new file mode 100644 index 0000000..0bff885 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Constants/ApiConstants.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2025 Tudor Stanciu + +namespace Bitip.Client.Constants +{ + internal struct ApiKeys + { + public const string HttpHeader = "X-API-Key"; + } + + internal struct ApiRoutes + { + public const string + Health = "health", + Version = "version"; + } +} diff --git a/src/clients/dotnet/Bitip.Client/DependencyInjectionExtension.cs b/src/clients/dotnet/Bitip.Client/DependencyInjectionExtension.cs new file mode 100644 index 0000000..c82eba0 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/DependencyInjectionExtension.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2025 Tudor Stanciu + +using Bitip.Client.Models; +using Bitip.Client.Services; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace Bitip.Client +{ + public static class DependencyInjectionExtension + { + public static void UseBitipClient(this IServiceCollection services, string baseUrl, string apiKey) + { + if (string.IsNullOrWhiteSpace(baseUrl)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(baseUrl)); + if (string.IsNullOrWhiteSpace(apiKey)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(apiKey)); + + services.Configure(options => + { + options.BaseUrl = baseUrl; + options.ApiKey = apiKey; + }); + services.AddHttpClient(); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client/Extensions/HttpMessageExtensions.cs b/src/clients/dotnet/Bitip.Client/Extensions/HttpMessageExtensions.cs new file mode 100644 index 0000000..6da1bc0 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Extensions/HttpMessageExtensions.cs @@ -0,0 +1,24 @@ +using Bitip.Client.Models; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Bitip.Client.Extensions +{ + internal static class HttpMessageExtensions + { + public static async Task EnsureSuccessOperation(this HttpResponseMessage response, CancellationToken cancellationToken = default) + { + if (response.IsSuccessStatusCode) + return response; + var errorResponse = await response.Content.ReadFromJsonAsync(cancellationToken); + if (errorResponse is null) + throw new HttpRequestException("An error occurred while fetching Bitip information."); + var message = $"{errorResponse.Error}: {errorResponse.Message}"; + if (!string.IsNullOrWhiteSpace(errorResponse.Ip)) + message += $" (IP: {errorResponse.Ip})"; + throw new HttpRequestException(message); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client/Models/BitipOptions.cs b/src/clients/dotnet/Bitip.Client/Models/BitipOptions.cs new file mode 100644 index 0000000..c3bb614 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Models/BitipOptions.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2025 Tudor Stanciu + +namespace Bitip.Client.Models +{ + internal record BitipOptions + { + public required string BaseUrl { get; set; } + public required string ApiKey { get; set; } + } +} diff --git a/src/clients/dotnet/Bitip.Client/Models/SystemDtos.cs b/src/clients/dotnet/Bitip.Client/Models/SystemDtos.cs new file mode 100644 index 0000000..a5d0600 --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Models/SystemDtos.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2025 Tudor Stanciu + +using System; + +namespace Bitip.Client.Models +{ + internal record HealthResponse + { + public required string Status { get; init; } + public required string Service { get; init; } + public DateTime Timestamp { get; init; } + public string? Error { get; init; } + } + + internal record VersionResponse + { + public required string Version { get; init; } + public required string CommitHash { get; init; } + public DateTime BuildDate { get; init; } + } + + internal record ErrorResponse + { + public required string Error { get; init; } + public required string Message { get; init; } + public string? Ip { get; init; } + } +} diff --git a/src/clients/dotnet/Bitip.Client/Services/BitipClient.cs b/src/clients/dotnet/Bitip.Client/Services/BitipClient.cs new file mode 100644 index 0000000..edcddcd --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Services/BitipClient.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2025 Tudor Stanciu + +using Bitip.Client.Constants; +using Bitip.Client.Extensions; +using Bitip.Client.Models; +using Microsoft.Extensions.Options; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Bitip.Client.Services +{ + internal class BitipClient : IBitipClient + { + private readonly HttpClient _httpClient; + + public BitipClient(HttpClient httpClient, IOptions options) + { + httpClient.BaseAddress = EnsureTrailingSlash(options.Value.BaseUrl); + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + httpClient.DefaultRequestHeaders.Add(ApiKeys.HttpHeader, options.Value.ApiKey); + + _httpClient = httpClient; + } + + public async Task GetHealth(CancellationToken cancellationToken = default) + { + var response = await _httpClient.GetAsync(ApiRoutes.Health, cancellationToken); + await response.EnsureSuccessOperation(cancellationToken); + var data = await response.Content.ReadFromJsonAsync(cancellationToken); + if (data is null) + throw new InvalidOperationException("The response content is null."); + return data; + } + + public async Task GetVersion(CancellationToken cancellationToken = default) + { + var response = await _httpClient.GetAsync(ApiRoutes.Version, cancellationToken); + await response.EnsureSuccessOperation(cancellationToken); + var data = await response.Content.ReadFromJsonAsync(cancellationToken); + if (data is null) + throw new InvalidOperationException("The response content is null."); + return data; + } + + private Uri EnsureTrailingSlash(string url) + { + var address = url.EndsWith("/") ? url : $"{url}/"; + return new Uri(address); + } + } +} diff --git a/src/clients/dotnet/Bitip.Client/Services/IBitipClient.cs b/src/clients/dotnet/Bitip.Client/Services/IBitipClient.cs new file mode 100644 index 0000000..4c16d2e --- /dev/null +++ b/src/clients/dotnet/Bitip.Client/Services/IBitipClient.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2025 Tudor Stanciu + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bitip.Client.Services +{ + public interface IBitipClient + { + } +}