< Summary

Information
Class: Chronicis.Client.Services.RenderDefinitionService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/RenderDefinitionService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 52
Coverable lines: 52
Total lines: 117
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 12
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%210%
ResolveAsync()0%2040%
BuildCandidatePaths(...)0%2040%
TryLoadAsync()0%2040%

File(s)

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

#LineLine coverage
 1using System.Net.Http.Json;
 2using Chronicis.Client.Models;
 3
 4namespace Chronicis.Client.Services;
 5
 6/// <summary>
 7/// Loads render definitions from wwwroot/render-definitions/ as static assets.
 8/// Caches definitions in memory after first load.
 9/// Resolution walks from most-specific category to least-specific, then falls back to default.
 10/// </summary>
 11public class RenderDefinitionService : IRenderDefinitionService
 12{
 13    private readonly HttpClient _http;
 14    private readonly ILogger<RenderDefinitionService> _logger;
 015    private readonly Dictionary<string, RenderDefinition?> _cache = new(StringComparer.OrdinalIgnoreCase);
 16
 017    private static readonly RenderDefinition DefaultDefinition = new()
 018    {
 019        Version = 1,
 020        DisplayName = null,
 021        TitleField = "name",
 022        Sections = new List<RenderSection>(),
 023        Hidden = new List<string> { "pk", "model" },
 024        CatchAll = true
 025    };
 26
 027    public RenderDefinitionService(HttpClient http, ILogger<RenderDefinitionService> logger)
 28    {
 029        _http = http;
 030        _logger = logger;
 031    }
 32
 33    public async Task<RenderDefinition> ResolveAsync(string source, string? categoryPath)
 34    {
 35        // Build candidate paths from most-specific to least-specific
 36        // e.g., for source="ros", categoryPath="bestiary/Cultural-Being":
 37        //   1. render-definitions/ros/bestiary/Cultural-Being.json
 38        //   2. render-definitions/ros/bestiary.json
 39        //   3. render-definitions/ros.json
 40        //   4. render-definitions/_default.json
 41        //   5. Built-in default (hardcoded)
 42
 043        var candidates = BuildCandidatePaths(source, categoryPath);
 44
 045        foreach (var candidate in candidates)
 46        {
 047            var definition = await TryLoadAsync(candidate);
 048            if (definition != null)
 49            {
 050                _logger.LogDebug("Resolved render definition: {Path}", candidate);
 051                return definition;
 52            }
 053        }
 54
 055        _logger.LogDebug(
 056            "No render definition found for source={Source}, category={Category}. Using built-in default.",
 057            source, categoryPath);
 058        return DefaultDefinition;
 059    }
 60
 61    private static List<string> BuildCandidatePaths(string source, string? categoryPath)
 62    {
 063        var candidates = new List<string>();
 64
 065        if (!string.IsNullOrWhiteSpace(categoryPath))
 66        {
 67            // Walk up the category path
 68            // "bestiary/Cultural-Being" → ["bestiary/Cultural-Being", "bestiary"]
 069            var segments = categoryPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
 70
 071            for (var i = segments.Length; i > 0; i--)
 72            {
 073                var partialPath = string.Join("/", segments.Take(i));
 074                candidates.Add($"render-definitions/{source}/{partialPath}.json");
 75            }
 76        }
 77
 78        // Source-level fallback
 079        candidates.Add($"render-definitions/{source}.json");
 80
 81        // Global default
 082        candidates.Add("render-definitions/_default.json");
 83
 084        return candidates;
 85    }
 86
 87    private async Task<RenderDefinition?> TryLoadAsync(string path)
 88    {
 089        if (_cache.TryGetValue(path, out var cached))
 090            return cached;
 91
 92        try
 93        {
 094            var response = await _http.GetAsync(path);
 095            if (!response.IsSuccessStatusCode)
 96            {
 097                _cache[path] = null;
 098                return null;
 99            }
 100
 0101            var definition = await response.Content.ReadFromJsonAsync<RenderDefinition>();
 0102            _cache[path] = definition;
 0103            return definition;
 104        }
 0105        catch (HttpRequestException)
 106        {
 0107            _cache[path] = null;
 0108            return null;
 109        }
 0110        catch (Exception ex)
 111        {
 0112            _logger.LogWarning(ex, "Failed to parse render definition at {Path}", path);
 0113            _cache[path] = null;
 0114            return null;
 115        }
 0116    }
 117}