using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using TrajetKmTracker.Api.Models.DTOs; using TrajetKmTracker.Api.Services.Exceptions; namespace TrajetKmTracker.Api.Services; /// /// Client typé de l'API BAN. Appelle GET /search/?q={query}&limit=5, /// parse la réponse GeoJSON (FeatureCollection) et mappe les features en /// . Lève /// en cas d'échec ou de timeout. /// public class BANClient : IBANClient { private const int MaxResultats = 5; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; private readonly HttpClient _httpClient; public BANClient(HttpClient httpClient) { _httpClient = httpClient; } public async Task> RechercherAsync( string query, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(query)) { return Array.Empty(); } var requestUri = $"/search/?q={Uri.EscapeDataString(query)}&limit={MaxResultats}"; BANFeatureCollection? collection; try { using var response = await _httpClient.GetAsync(requestUri, cancellationToken); response.EnsureSuccessStatusCode(); collection = await response.Content.ReadFromJsonAsync( JsonOptions, cancellationToken); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { // Le timeout configuré sur le HttpClient déclenche une annulation // non demandée par l'appelant. throw new BANIndisponibleException( "L'appel à l'API BAN a dépassé le délai imparti (timeout)."); } catch (HttpRequestException ex) { throw new BANIndisponibleException( "L'API BAN est indisponible ou a renvoyé une réponse en erreur.", ex); } catch (JsonException ex) { throw new BANIndisponibleException( "La réponse de l'API BAN n'a pas pu être interprétée.", ex); } if (collection?.Features is null || collection.Features.Count == 0) { return Array.Empty(); } var resultats = new List(collection.Features.Count); foreach (var feature in collection.Features) { var coordinates = feature.Geometry?.Coordinates; if (coordinates is null || coordinates.Count < 2) { continue; } // GeoJSON : coordinates = [longitude, latitude]. var longitude = coordinates[0]; var latitude = coordinates[1]; resultats.Add(new BANAdresseDto( Label: feature.Properties?.Label ?? string.Empty, Latitude: latitude, Longitude: longitude, Postcode: feature.Properties?.Postcode ?? string.Empty, City: feature.Properties?.City ?? string.Empty)); } return resultats; } // --- Modèles internes de désérialisation de la réponse GeoJSON BAN --- private sealed class BANFeatureCollection { [JsonPropertyName("features")] public List? Features { get; set; } } private sealed class BANFeature { [JsonPropertyName("properties")] public BANProperties? Properties { get; set; } [JsonPropertyName("geometry")] public BANGeometry? Geometry { get; set; } } private sealed class BANProperties { [JsonPropertyName("label")] public string? Label { get; set; } [JsonPropertyName("postcode")] public string? Postcode { get; set; } [JsonPropertyName("city")] public string? City { get; set; } } private sealed class BANGeometry { // [longitude, latitude] [JsonPropertyName("coordinates")] public List? Coordinates { get; set; } } }