147 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			147 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Net.Http;
 | |
| using System.Text.Json;
 | |
| using System.Text.Json.Serialization;
 | |
| using System.Threading;
 | |
| using System.Threading.Tasks;
 | |
| using Microsoft.Extensions.Logging;
 | |
| using StellaOps.Feedser.Source.Cccs.Configuration;
 | |
| using StellaOps.Feedser.Source.Common.Fetch;
 | |
| 
 | |
| namespace StellaOps.Feedser.Source.Cccs.Internal;
 | |
| 
 | |
| public sealed class CccsFeedClient
 | |
| {
 | |
|     private static readonly string[] AcceptHeaders =
 | |
|     {
 | |
|         "application/json",
 | |
|         "application/vnd.api+json;q=0.9",
 | |
|         "text/json;q=0.8",
 | |
|         "application/*+json;q=0.7",
 | |
|     };
 | |
| 
 | |
|     private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
 | |
|     {
 | |
|         PropertyNameCaseInsensitive = true,
 | |
|         DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
 | |
|     };
 | |
| 
 | |
|     private readonly SourceFetchService _fetchService;
 | |
|     private readonly ILogger<CccsFeedClient> _logger;
 | |
| 
 | |
|     public CccsFeedClient(SourceFetchService fetchService, ILogger<CccsFeedClient> logger)
 | |
|     {
 | |
|         _fetchService = fetchService ?? throw new ArgumentNullException(nameof(fetchService));
 | |
|         _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 | |
|     }
 | |
| 
 | |
|     internal async Task<CccsFeedResult> FetchAsync(CccsFeedEndpoint endpoint, TimeSpan requestTimeout, CancellationToken cancellationToken)
 | |
|     {
 | |
|         ArgumentNullException.ThrowIfNull(endpoint);
 | |
|         if (endpoint.Uri is null)
 | |
|         {
 | |
|             throw new InvalidOperationException("Feed endpoint URI must be configured.");
 | |
|         }
 | |
| 
 | |
|         var request = new SourceFetchRequest(CccsOptions.HttpClientName, CccsConnectorPlugin.SourceName, endpoint.Uri)
 | |
|         {
 | |
|             AcceptHeaders = AcceptHeaders,
 | |
|             TimeoutOverride = requestTimeout,
 | |
|             Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
 | |
|             {
 | |
|                 ["cccs.language"] = endpoint.Language,
 | |
|                 ["cccs.feedUri"] = endpoint.Uri.ToString(),
 | |
|             },
 | |
|         };
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var result = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false);
 | |
| 
 | |
|             if (!result.IsSuccess || result.Content is null)
 | |
|             {
 | |
|                 _logger.LogWarning("CCCS feed fetch returned no content for {Uri} (status={Status})", endpoint.Uri, result.StatusCode);
 | |
|                 return CccsFeedResult.Empty;
 | |
|             }
 | |
| 
 | |
|             var feedResponse = Deserialize<CccsFeedResponse>(result.Content);
 | |
|             if (feedResponse is null || feedResponse.Error)
 | |
|             {
 | |
|                 _logger.LogWarning("CCCS feed response flagged an error for {Uri}", endpoint.Uri);
 | |
|                 return CccsFeedResult.Empty;
 | |
|             }
 | |
| 
 | |
|             var taxonomy = await FetchTaxonomyAsync(endpoint, requestTimeout, cancellationToken).ConfigureAwait(false);
 | |
|             var items = (IReadOnlyList<CccsFeedItem>)feedResponse.Response ?? Array.Empty<CccsFeedItem>();
 | |
|             return new CccsFeedResult(items, taxonomy, result.LastModified);
 | |
|         }
 | |
|         catch (Exception ex) when (ex is JsonException or InvalidOperationException)
 | |
|         {
 | |
|             _logger.LogError(ex, "CCCS feed deserialization failed for {Uri}", endpoint.Uri);
 | |
|             throw;
 | |
|         }
 | |
|         catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
 | |
|         {
 | |
|             _logger.LogWarning(ex, "CCCS feed fetch failed for {Uri}", endpoint.Uri);
 | |
|             throw;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private async Task<IReadOnlyDictionary<int, string>> FetchTaxonomyAsync(CccsFeedEndpoint endpoint, TimeSpan timeout, CancellationToken cancellationToken)
 | |
|     {
 | |
|         var taxonomyUri = endpoint.BuildTaxonomyUri();
 | |
|         var request = new SourceFetchRequest(CccsOptions.HttpClientName, CccsConnectorPlugin.SourceName, taxonomyUri)
 | |
|         {
 | |
|             AcceptHeaders = AcceptHeaders,
 | |
|             TimeoutOverride = timeout,
 | |
|             Metadata = new Dictionary<string, string>(StringComparer.Ordinal)
 | |
|             {
 | |
|                 ["cccs.language"] = endpoint.Language,
 | |
|                 ["cccs.taxonomyUri"] = taxonomyUri.ToString(),
 | |
|             },
 | |
|         };
 | |
| 
 | |
|         try
 | |
|         {
 | |
|             var result = await _fetchService.FetchContentAsync(request, cancellationToken).ConfigureAwait(false);
 | |
|             if (!result.IsSuccess || result.Content is null)
 | |
|             {
 | |
|                 _logger.LogDebug("CCCS taxonomy fetch returned no content for {Uri}", taxonomyUri);
 | |
|                 return new Dictionary<int, string>(0);
 | |
|             }
 | |
| 
 | |
|             var taxonomyResponse = Deserialize<CccsTaxonomyResponse>(result.Content);
 | |
|             if (taxonomyResponse is null || taxonomyResponse.Error)
 | |
|             {
 | |
|                 _logger.LogDebug("CCCS taxonomy response indicated error for {Uri}", taxonomyUri);
 | |
|                 return new Dictionary<int, string>(0);
 | |
|             }
 | |
| 
 | |
|             var map = new Dictionary<int, string>(taxonomyResponse.Response.Count);
 | |
|             foreach (var item in taxonomyResponse.Response)
 | |
|             {
 | |
|                 if (!string.IsNullOrWhiteSpace(item.Title))
 | |
|                 {
 | |
|                     map[item.Id] = item.Title!;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return map;
 | |
|         }
 | |
|         catch (Exception ex) when (ex is JsonException or InvalidOperationException)
 | |
|         {
 | |
|             _logger.LogWarning(ex, "Failed to deserialize CCCS taxonomy for {Uri}", taxonomyUri);
 | |
|             return new Dictionary<int, string>(0);
 | |
|         }
 | |
|         catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
 | |
|         {
 | |
|             _logger.LogWarning(ex, "CCCS taxonomy fetch failed for {Uri}", taxonomyUri);
 | |
|             return new Dictionary<int, string>(0);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private static T? Deserialize<T>(byte[] content)
 | |
|         => JsonSerializer.Deserialize<T>(content, SerializerOptions);
 | |
| }
 |