< Summary

Information
Class: Chronicis.Client.Services.MapApiService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/MapApiService.cs
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 390
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/MapApiService.cs

#LineLine coverage
 1using System.Net.Http.Json;
 2using System.Text.Json;
 3using Chronicis.Shared.DTOs.Maps;
 4
 5namespace Chronicis.Client.Services;
 6
 7/// <summary>
 8/// Service for Map API operations.
 9/// Uses HttpClientExtensions for consistent error handling and logging.
 10/// </summary>
 11public class MapApiService : IMapApiService
 12{
 13    private readonly HttpClient _http;
 14    private readonly ILogger<MapApiService> _logger;
 15
 16    public MapApiService(HttpClient http, ILogger<MapApiService> logger)
 17    {
 3918        _http = http;
 3919        _logger = logger;
 3920    }
 21
 22    public async Task<List<MapSummaryDto>> ListMapsForWorldAsync(Guid worldId)
 23    {
 24        return await _http.GetListAsync<MapSummaryDto>(
 25            $"world/{worldId}/maps",
 26            _logger,
 27            $"maps for world {worldId}");
 28    }
 29
 30    public async Task<List<MapAutocompleteDto>> GetMapAutocompleteAsync(Guid worldId, string? query)
 31    {
 32        var route = $"world/{worldId}/maps/autocomplete";
 33        if (!string.IsNullOrWhiteSpace(query))
 34        {
 35            route = $"{route}?query={Uri.EscapeDataString(query)}";
 36        }
 37
 38        return await _http.GetListAsync<MapAutocompleteDto>(
 39            route,
 40            _logger,
 41            $"map autocomplete for world {worldId}");
 42    }
 43
 44    public async Task<List<MapFeatureAutocompleteDto>> GetMapFeatureAutocompleteAsync(Guid worldId, string? query)
 45    {
 46        var route = $"world/{worldId}/maps/features/autocomplete";
 47        if (!string.IsNullOrWhiteSpace(query))
 48        {
 49            route = $"{route}?query={Uri.EscapeDataString(query)}";
 50        }
 51
 52        return await _http.GetListAsync<MapFeatureAutocompleteDto>(
 53            route,
 54            _logger,
 55            $"map feature autocomplete for world {worldId}");
 56    }
 57
 58    public async Task<List<MapFeatureAutocompleteDto>> GetMapFeatureAutocompleteAsync(Guid worldId, Guid mapId, string? 
 59    {
 60        var route = $"world/{worldId}/maps/{mapId}/features/autocomplete";
 61        if (!string.IsNullOrWhiteSpace(query))
 62        {
 63            route = $"{route}?query={Uri.EscapeDataString(query)}";
 64        }
 65
 66        return await _http.GetListAsync<MapFeatureAutocompleteDto>(
 67            route,
 68            _logger,
 69            $"map feature autocomplete for map {mapId}");
 70    }
 71
 72    public async Task<MapDto?> CreateMapAsync(Guid worldId, MapCreateDto dto)
 73    {
 74        return await _http.PostEntityAsync<MapDto>(
 75            $"world/{worldId}/maps",
 76            dto,
 77            _logger,
 78            $"map in world {worldId}");
 79    }
 80
 81    public async Task<(MapDto? Map, int? StatusCode, string? Error)> GetMapAsync(Guid worldId, Guid mapId)
 82    {
 83        return await GetEntityWithStatusAsync<MapDto>(
 84            $"world/{worldId}/maps/{mapId}",
 85            $"map {mapId}");
 86    }
 87
 88    public async Task<List<MapLayerDto>> GetLayersForMapAsync(Guid worldId, Guid mapId)
 89    {
 90        return await _http.GetListAsync<MapLayerDto>(
 91            $"world/{worldId}/maps/{mapId}/layers",
 92            _logger,
 93            $"layers for map {mapId}");
 94    }
 95
 96    public async Task<MapLayerDto> CreateLayerAsync(Guid worldId, Guid mapId, string name, Guid? parentLayerId = null)
 97    {
 98        var request = new CreateLayerRequest
 99        {
 100            Name = name,
 101            ParentLayerId = parentLayerId,
 102        };
 103
 104        var createdLayer = await _http.PostEntityAsync<MapLayerDto>(
 105            $"world/{worldId}/maps/{mapId}/layers",
 106            request,
 107            _logger,
 108            $"layer for map {mapId}");
 109
 110        if (createdLayer == null)
 111        {
 112            throw new InvalidOperationException("Failed to create layer");
 113        }
 114
 115        return createdLayer;
 116    }
 117
 118    public async Task<List<MapPinResponseDto>> ListPinsForMapAsync(Guid worldId, Guid mapId)
 119    {
 120        return await _http.GetListAsync<MapPinResponseDto>(
 121            $"world/{worldId}/maps/{mapId}/pins",
 122            _logger,
 123            $"pins for map {mapId}");
 124    }
 125
 126    public async Task<MapPinResponseDto?> CreatePinAsync(Guid worldId, Guid mapId, MapPinCreateDto dto)
 127    {
 128        return await _http.PostEntityAsync<MapPinResponseDto>(
 129            $"world/{worldId}/maps/{mapId}/pins",
 130            dto,
 131            _logger,
 132            $"pin for map {mapId}");
 133    }
 134
 135    public async Task<MapFeatureDto?> CreateFeatureAsync(Guid worldId, Guid mapId, MapFeatureCreateDto dto)
 136    {
 137        return await _http.PostEntityAsync<MapFeatureDto>(
 138            $"world/{worldId}/maps/{mapId}/features",
 139            dto,
 140            _logger,
 141            $"feature for map {mapId}");
 142    }
 143
 144    public async Task<bool> DeletePinAsync(Guid worldId, Guid mapId, Guid pinId)
 145    {
 146        return await _http.DeleteEntityAsync(
 147            $"world/{worldId}/maps/{mapId}/pins/{pinId}",
 148            _logger,
 149            $"pin {pinId} for map {mapId}");
 150    }
 151
 152    public async Task<bool> DeleteFeatureAsync(Guid worldId, Guid mapId, Guid featureId)
 153    {
 154        return await _http.DeleteEntityAsync(
 155            $"world/{worldId}/maps/{mapId}/features/{featureId}",
 156            _logger,
 157            $"feature {featureId} for map {mapId}");
 158    }
 159
 160    public async Task<bool> UpdatePinPositionAsync(Guid worldId, Guid mapId, Guid pinId, MapPinPositionUpdateDto dto)
 161    {
 162        return await _http.PatchEntityAsync(
 163            $"world/{worldId}/maps/{mapId}/pins/{pinId}",
 164            dto,
 165            _logger,
 166            $"pin {pinId} for map {mapId}");
 167    }
 168
 169    public async Task<List<MapFeatureDto>> ListFeaturesForMapAsync(Guid worldId, Guid mapId)
 170    {
 171        return await _http.GetListAsync<MapFeatureDto>(
 172            $"world/{worldId}/maps/{mapId}/features",
 173            _logger,
 174            $"features for map {mapId}");
 175    }
 176
 177    public async Task<(MapFeatureDto? Feature, int? StatusCode, string? Error)> GetFeatureAsync(Guid worldId, Guid mapId
 178    {
 179        return await GetEntityWithStatusAsync<MapFeatureDto>(
 180            $"world/{worldId}/maps/{mapId}/features/{featureId}",
 181            $"feature {featureId} for map {mapId}");
 182    }
 183
 184    public async Task<List<MapFeatureSessionReferenceDto>> GetFeatureSessionReferencesAsync(Guid worldId, Guid mapId, Gu
 185    {
 186        return await _http.GetListAsync<MapFeatureSessionReferenceDto>(
 187            $"world/{worldId}/maps/{mapId}/features/{featureId}/session-references",
 188            _logger,
 189            $"session references for feature {featureId} on map {mapId}");
 190    }
 191
 192    public async Task<MapFeatureDto?> UpdateFeatureAsync(Guid worldId, Guid mapId, Guid featureId, MapFeatureUpdateDto d
 193    {
 194        return await _http.PutEntityAsync<MapFeatureDto>(
 195            $"world/{worldId}/maps/{mapId}/features/{featureId}",
 196            dto,
 197            _logger,
 198            $"feature {featureId} for map {mapId}");
 199    }
 200
 201    public async Task<MapDto?> UpdateMapAsync(Guid worldId, Guid mapId, MapUpdateDto dto)
 202    {
 203        return await _http.PutEntityAsync<MapDto>(
 204            $"world/{worldId}/maps/{mapId}",
 205            dto,
 206            _logger,
 207            $"map {mapId}");
 208    }
 209
 210    public async Task UpdateLayerVisibilityAsync(Guid worldId, Guid mapId, Guid layerId, bool isEnabled)
 211    {
 212        var request = new UpdateLayerVisibilityRequest
 213        {
 214            IsEnabled = isEnabled,
 215        };
 216
 217        _ = await _http.PutBoolAsync(
 218            $"world/{worldId}/maps/{mapId}/layers/{layerId}",
 219            request,
 220            _logger,
 221            $"layer {layerId} visibility for map {mapId}");
 222    }
 223
 224    public async Task ReorderLayersAsync(Guid worldId, Guid mapId, IList<Guid> layerIds)
 225    {
 226        var request = new ReorderLayersRequest
 227        {
 228            LayerIds = layerIds,
 229        };
 230
 231        _ = await _http.PutBoolAsync(
 232            $"world/{worldId}/maps/{mapId}/layers/reorder",
 233            request,
 234            _logger,
 235            $"layer reorder for map {mapId}");
 236    }
 237
 238    public async Task RenameLayerAsync(Guid worldId, Guid mapId, Guid layerId, string name)
 239    {
 240        var request = new RenameLayerRequest
 241        {
 242            Name = name,
 243        };
 244
 245        var success = await _http.PutBoolAsync(
 246            $"world/{worldId}/maps/{mapId}/layers/{layerId}/rename",
 247            request,
 248            _logger,
 249            $"rename layer {layerId} on map {mapId}");
 250
 251        if (!success)
 252        {
 253            throw new InvalidOperationException("Failed to rename layer");
 254        }
 255    }
 256
 257    public async Task SetLayerParentAsync(Guid worldId, Guid mapId, Guid layerId, Guid? parentLayerId)
 258    {
 259        var request = new SetLayerParentRequest
 260        {
 261            ParentLayerId = parentLayerId,
 262        };
 263
 264        var success = await _http.PutBoolAsync(
 265            $"world/{worldId}/maps/{mapId}/layers/{layerId}/parent",
 266            request,
 267            _logger,
 268            $"set parent for layer {layerId} on map {mapId}");
 269
 270        if (!success)
 271        {
 272            throw new InvalidOperationException("Failed to set layer parent");
 273        }
 274    }
 275
 276    public async Task DeleteLayerAsync(Guid worldId, Guid mapId, Guid layerId)
 277    {
 278        var success = await _http.DeleteEntityAsync(
 279            $"world/{worldId}/maps/{mapId}/layers/{layerId}",
 280            _logger,
 281            $"delete layer {layerId} on map {mapId}");
 282
 283        if (!success)
 284        {
 285            throw new InvalidOperationException("Failed to delete layer");
 286        }
 287    }
 288
 289    public async Task<RequestBasemapUploadResponseDto?> RequestBasemapUploadAsync(
 290        Guid worldId,
 291        Guid mapId,
 292        RequestBasemapUploadDto dto)
 293    {
 294        return await _http.PostEntityAsync<RequestBasemapUploadResponseDto>(
 295            $"world/{worldId}/maps/{mapId}/request-basemap-upload",
 296            dto,
 297            _logger,
 298            $"basemap upload request for map {mapId}");
 299    }
 300
 301    public async Task<MapDto?> ConfirmBasemapUploadAsync(
 302        Guid worldId,
 303        Guid mapId,
 304        string basemapBlobKey,
 305        string contentType,
 306        string originalFilename)
 307    {
 308        var dto = new
 309        {
 310            BasemapBlobKey = basemapBlobKey,
 311            ContentType = contentType,
 312            OriginalFilename = originalFilename
 313        };
 314
 315        return await _http.PostEntityAsync<MapDto>(
 316            $"world/{worldId}/maps/{mapId}/confirm-basemap-upload",
 317            dto,
 318            _logger,
 319            $"basemap confirm for map {mapId}");
 320    }
 321
 322    public async Task<(GetBasemapReadUrlResponseDto? Basemap, int? StatusCode, string? Error)> GetBasemapReadUrlAsync(
 323        Guid worldId,
 324        Guid mapId)
 325    {
 326        return await GetEntityWithStatusAsync<GetBasemapReadUrlResponseDto>(
 327            $"world/{worldId}/maps/{mapId}/basemap",
 328            $"basemap read URL for map {mapId}");
 329    }
 330
 331    public async Task<bool> DeleteMapAsync(Guid worldId, Guid mapId)
 332    {
 333        return await _http.DeleteEntityAsync(
 334            $"world/{worldId}/maps/{mapId}",
 335            _logger,
 336            $"map {mapId}");
 337    }
 338
 339    private async Task<(T? Entity, int? StatusCode, string? Error)> GetEntityWithStatusAsync<T>(
 340        string url,
 341        string description) where T : class
 342    {
 343        try
 344        {
 345            var response = await _http.GetAsync(url);
 346            var statusCode = (int)response.StatusCode;
 347
 348            if (!response.IsSuccessStatusCode)
 349            {
 350                var error = await TryReadErrorMessageAsync(response);
 351                _logger.LogWarning("Failed to fetch {Description}: {StatusCode}", description, response.StatusCode);
 352                return (null, statusCode, error);
 353            }
 354
 355            var entity = await response.Content.ReadFromJsonAsync<T>();
 356            return (entity, statusCode, null);
 357        }
 358        catch (Exception ex)
 359        {
 360            _logger.LogError(ex, "Error fetching {Description} from {Url}", description, url);
 361            return (null, null, ex.Message);
 362        }
 363    }
 364
 365    private static async Task<string?> TryReadErrorMessageAsync(HttpResponseMessage response)
 366    {
 367        try
 368        {
 369            var content = await response.Content.ReadAsStringAsync();
 370            if (string.IsNullOrWhiteSpace(content))
 371            {
 372                return null;
 373            }
 374
 375            using var payload = JsonDocument.Parse(content);
 376            if (payload.RootElement.ValueKind == JsonValueKind.Object
 377                && payload.RootElement.TryGetProperty("error", out var errorProperty)
 378                && errorProperty.ValueKind == JsonValueKind.String)
 379            {
 380                return errorProperty.GetString();
 381            }
 382
 383            return null;
 384        }
 385        catch (JsonException)
 386        {
 387            return null;
 388        }
 389    }
 390}