< Summary

Information
Class: Chronicis.Client.Services.RenderDefinitionGeneratorService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/RenderDefinitionGeneratorService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 182
Coverable lines: 182
Total lines: 297
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 75
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%210%
Generate(...)0%1640400%
DetectPrefixGroups(...)0%210140%
IsAbilityScoreGroup(...)0%620%
IsNullOrEmpty(...)0%132110%
IsDescriptionField(...)0%620%
FormatFieldName(...)100%210%
FormatGroupLabel(...)0%2040%
StripPrefix(...)0%620%
CreateMinimal()100%210%
get_Name()100%210%
get_Value()100%210%
get_Kind()100%210%
get_IsNull()100%210%
get_IsComplex()100%210%
get_Prefix()100%210%
get_Label()100%210%
get_Fields()100%210%

File(s)

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

#LineLine coverage
 1using System.Text.Json;
 2using Chronicis.Client.Models;
 3
 4namespace Chronicis.Client.Services;
 5
 6/// <summary>
 7/// Generates a starter RenderDefinition from a sample JSON record.
 8/// Uses heuristics to group fields, detect ability scores, and choose render hints.
 9/// The output is a starting point for manual refinement.
 10/// </summary>
 11public static class RenderDefinitionGeneratorService
 12{
 13    // Well-known metadata fields to hide
 014    private static readonly HashSet<string> HiddenFields = new(StringComparer.OrdinalIgnoreCase)
 015    {
 016        "pk", "model", "document", "illustration", "url", "key", "slug",
 017        "hover", "v2_converted_path", "img_main", "document__slug",
 018        "document__title", "document__license_url", "document__url",
 019        "page_no", "spell_list", "environments"
 020    };
 21
 022    private static readonly string[] TitleCandidates = { "name", "title" };
 23
 024    private static readonly string[] AbilitySuffixes =
 025        { "strength", "dexterity", "constitution", "intelligence", "wisdom", "charisma" };
 26
 027    private static readonly string[] AbilityLabels =
 028        { "STR", "DEX", "CON", "INT", "WIS", "CHA" };
 29
 30    public static RenderDefinition Generate(JsonElement sample)
 31    {
 032        var dataSource = sample;
 033        if (sample.ValueKind == JsonValueKind.Object &&
 034            sample.TryGetProperty("fields", out var fields) &&
 035            fields.ValueKind == JsonValueKind.Object)
 36        {
 037            dataSource = fields;
 38        }
 39
 040        if (dataSource.ValueKind != JsonValueKind.Object)
 041            return CreateMinimal();
 42
 043        var fieldInfos = new List<FieldInfo>();
 044        foreach (var prop in dataSource.EnumerateObject())
 45        {
 046            fieldInfos.Add(new FieldInfo
 047            {
 048                Name = prop.Name,
 049                Value = prop.Value,
 050                Kind = prop.Value.ValueKind,
 051                IsNull = IsNullOrEmpty(prop.Value),
 052                IsComplex = prop.Value.ValueKind == JsonValueKind.Object ||
 053                            prop.Value.ValueKind == JsonValueKind.Array
 054            });
 55        }
 56
 057        var titleField = fieldInfos
 058            .FirstOrDefault(f => TitleCandidates.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
 059            ?.Name ?? "name";
 60
 061        var hidden = new List<string>();
 062        var remaining = new List<FieldInfo>();
 63
 064        foreach (var fi in fieldInfos)
 65        {
 066            if (fi.Name.Equals(titleField, StringComparison.OrdinalIgnoreCase))
 67                continue;
 068            if (HiddenFields.Contains(fi.Name))
 069                hidden.Add(fi.Name);
 70            else
 071                remaining.Add(fi);
 72        }
 73
 074        var prefixGroups = DetectPrefixGroups(remaining);
 075        var groupedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 076        foreach (var group in prefixGroups)
 077            foreach (var fi in group.Fields)
 078                groupedNames.Add(fi.Name);
 79
 080        var ungrouped = remaining.Where(f => !groupedNames.Contains(f.Name)).ToList();
 081        var sections = new List<RenderSection>();
 82
 83        // Overview section
 084        var overviewFields = ungrouped
 085            .Where(f => !f.IsComplex)
 086            .OrderBy(f => IsDescriptionField(f.Name) ? 1 : 0)
 087            .ToList();
 88
 089        if (overviewFields.Count > 0)
 90        {
 091            sections.Add(new RenderSection
 092            {
 093                Label = "Overview",
 094                Render = "fields",
 095                Fields = overviewFields.Select(f => new RenderField
 096                {
 097                    Path = f.Name,
 098                    Label = FormatFieldName(f.Name),
 099                    Render = IsDescriptionField(f.Name) ? "richtext" : "text"
 0100                }).ToList()
 0101            });
 102        }
 103
 104        // Prefix-grouped sections
 0105        foreach (var group in prefixGroups.OrderBy(g => g.Label))
 106        {
 0107            var isAbilityScores = IsAbilityScoreGroup(group);
 0108            var allNull = group.Fields.All(f => f.IsNull);
 0109            var mostlyNull = group.Fields.Count(f => f.IsNull) > group.Fields.Count / 2;
 110
 0111            if (isAbilityScores)
 112            {
 113                // Stat-row rendering for ability scores
 0114                sections.Add(new RenderSection
 0115                {
 0116                    Label = "Ability Scores",
 0117                    Render = "stat-row",
 0118                    Fields = AbilitySuffixes.Select((suffix, i) =>
 0119                    {
 0120                        var match = group.Fields.FirstOrDefault(f =>
 0121                            f.Name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase));
 0122                        return new RenderField
 0123                        {
 0124                            Path = match?.Name ?? $"ability_score_{suffix}",
 0125                            Label = AbilityLabels[i]
 0126                        };
 0127                    }).ToList()
 0128                });
 129            }
 130            else
 131            {
 0132                sections.Add(new RenderSection
 0133                {
 0134                    Label = FormatGroupLabel(group.Prefix),
 0135                    Render = "fields",
 0136                    Collapsed = mostlyNull,
 0137                    Fields = group.Fields
 0138                        .OrderBy(f => f.IsNull ? 1 : 0)
 0139                        .Select(f => new RenderField
 0140                        {
 0141                            Path = f.Name,
 0142                            Label = FormatFieldName(StripPrefix(f.Name, group.Prefix))
 0143                        }).ToList()
 0144                });
 145            }
 146        }
 147
 148        // Complex fields section (arrays/objects)
 0149        var complexFields = ungrouped.Where(f => f.IsComplex).ToList();
 0150        if (complexFields.Count > 0)
 151        {
 0152            sections.Add(new RenderSection
 0153            {
 0154                Label = "Additional Data",
 0155                Render = "fields",
 0156                Collapsed = true,
 0157                Fields = complexFields.Select(f => new RenderField
 0158                {
 0159                    Path = f.Name,
 0160                    Label = FormatFieldName(f.Name)
 0161                }).ToList()
 0162            });
 163        }
 164
 0165        return new RenderDefinition
 0166        {
 0167            TitleField = titleField,
 0168            CatchAll = true,
 0169            Hidden = hidden,
 0170            Sections = sections
 0171        };
 172    }
 173
 174    // --- Heuristic helpers ---
 175
 176    private static List<PrefixGroup> DetectPrefixGroups(List<FieldInfo> fields)
 177    {
 0178        var groups = new List<PrefixGroup>();
 0179        var candidates = new Dictionary<string, List<FieldInfo>>(StringComparer.OrdinalIgnoreCase);
 180
 0181        foreach (var fi in fields)
 182        {
 0183            var lastUnderscore = fi.Name.LastIndexOf('_');
 0184            if (lastUnderscore <= 0)
 185                continue;
 186
 187            // Try progressively shorter prefixes
 0188            var prefix = fi.Name[..lastUnderscore];
 189            // Normalize: for multi-segment like "ability_score", try that first
 0190            if (!candidates.ContainsKey(prefix))
 0191                candidates[prefix] = new List<FieldInfo>();
 0192            candidates[prefix].Add(fi);
 193        }
 194
 195        // Only keep groups with 3+ fields (meaningful grouping)
 196        // Also consolidate nested prefixes: prefer "ability_score" over "ability"
 0197        var validPrefixes = candidates
 0198            .Where(kv => kv.Value.Count >= 3)
 0199            .OrderByDescending(kv => kv.Key.Length)
 0200            .ToList();
 201
 0202        var claimed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 0203        foreach (var kv in validPrefixes)
 204        {
 0205            var unclaimed = kv.Value.Where(f => !claimed.Contains(f.Name)).ToList();
 0206            if (unclaimed.Count < 3)
 207                continue;
 208
 0209            groups.Add(new PrefixGroup
 0210            {
 0211                Prefix = kv.Key,
 0212                Label = FormatGroupLabel(kv.Key),
 0213                Fields = unclaimed
 0214            });
 0215            foreach (var f in unclaimed)
 0216                claimed.Add(f.Name);
 217        }
 218
 0219        return groups;
 220    }
 221
 222    private static bool IsAbilityScoreGroup(PrefixGroup group)
 223    {
 0224        if (group.Fields.Count != 6)
 0225            return false;
 0226        var suffixes = group.Fields
 0227            .Select(f => StripPrefix(f.Name, group.Prefix).ToLowerInvariant())
 0228            .ToHashSet();
 0229        return AbilitySuffixes.All(s => suffixes.Contains(s));
 230    }
 231
 232    private static bool IsNullOrEmpty(JsonElement value)
 233    {
 0234        return value.ValueKind switch
 0235        {
 0236            JsonValueKind.Null or JsonValueKind.Undefined => true,
 0237            JsonValueKind.String => string.IsNullOrWhiteSpace(value.GetString()) ||
 0238                                    value.GetString() == "—" || value.GetString() == "-",
 0239            JsonValueKind.Array => value.GetArrayLength() == 0,
 0240            _ => false
 0241        };
 242    }
 243
 244    private static bool IsDescriptionField(string name) =>
 0245        name.Contains("description", StringComparison.OrdinalIgnoreCase) ||
 0246        name.Contains("desc", StringComparison.OrdinalIgnoreCase);
 247
 248    private static string FormatFieldName(string name)
 249    {
 250        // "ability_score_strength" -> "Strength", "hit_points" -> "Hit Points"
 0251        return string.Join(' ', name
 0252            .Split('_')
 0253            .Where(s => !string.IsNullOrEmpty(s))
 0254            .Select(s => char.ToUpperInvariant(s[0]) + s[1..]));
 255    }
 256
 257    private static string FormatGroupLabel(string prefix)
 258    {
 259        // "saving_throw" -> "Saving Throws", "skill_bonus" -> "Skill Bonuses"
 0260        var label = FormatFieldName(prefix);
 261        // Simple pluralization
 0262        if (!label.EndsWith('s') && !label.EndsWith("es"))
 0263            label += "s";
 0264        return label;
 265    }
 266
 267    private static string StripPrefix(string name, string prefix)
 268    {
 0269        if (name.StartsWith(prefix + "_", StringComparison.OrdinalIgnoreCase))
 0270            return name[(prefix.Length + 1)..];
 0271        return name;
 272    }
 273
 0274    private static RenderDefinition CreateMinimal() => new()
 0275    {
 0276        CatchAll = true,
 0277        Sections = new List<RenderSection>()
 0278    };
 279
 280    // --- Internal types ---
 281
 282    private class FieldInfo
 283    {
 0284        public string Name { get; set; } = "";
 0285        public JsonElement Value { get; set; }
 0286        public JsonValueKind Kind { get; set; }
 0287        public bool IsNull { get; set; }
 0288        public bool IsComplex { get; set; }
 289    }
 290
 291    private class PrefixGroup
 292    {
 0293        public string Prefix { get; set; } = "";
 0294        public string Label { get; set; } = "";
 0295        public List<FieldInfo> Fields { get; set; } = new();
 296    }
 297}