< Summary

Information
Class: Chronicis.Api.Services.Routing.SlugPathResolver
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/Routing/SlugPathResolver.cs
Line coverage
100%
Covered lines: 10
Uncovered lines: 0
Coverable lines: 10
Total lines: 282
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.Api/Services/Routing/SlugPathResolver.cs

#LineLine coverage
 1using Chronicis.Shared.Routing;
 2
 3namespace Chronicis.Api.Services.Routing;
 4
 5public sealed class SlugPathResolver : ISlugPathResolver
 6{
 7    private readonly IWorldService _worldService;
 8    private readonly IWorldMembershipService _membershipService;
 9    private readonly ICampaignService _campaignService;
 10    private readonly IArcService _arcService;
 11    private readonly ISessionService _sessionService;
 12    private readonly IWorldMapService _mapService;
 13    private readonly IArticleService _articleService;
 14    private readonly IReadAccessPolicyService _readAccessPolicy;
 15    private readonly IReservedSlugProvider _reservedSlugProvider;
 16
 17    public SlugPathResolver(
 18        IWorldService worldService,
 19        IWorldMembershipService membershipService,
 20        ICampaignService campaignService,
 21        IArcService arcService,
 22        ISessionService sessionService,
 23        IWorldMapService mapService,
 24        IArticleService articleService,
 25        IReadAccessPolicyService readAccessPolicy,
 26        IReservedSlugProvider reservedSlugProvider)
 27    {
 2528        _worldService = worldService;
 2529        _membershipService = membershipService;
 2530        _campaignService = campaignService;
 2531        _arcService = arcService;
 2532        _sessionService = sessionService;
 2533        _mapService = mapService;
 2534        _articleService = articleService;
 2535        _readAccessPolicy = readAccessPolicy;
 2536        _reservedSlugProvider = reservedSlugProvider;
 2537    }
 38
 39    public async Task<SlugPathResolution?> ResolveAsync(
 40        IReadOnlyList<string> segments,
 41        Guid? currentUserId,
 42        CancellationToken cancellationToken = default)
 43    {
 44        // 1. Empty path
 45        if (segments.Count == 0)
 46            return null;
 47
 48        var worldSlug = segments[0];
 49
 50        // 2. Tutorials short-circuit (before reserved slug check)
 51        if (worldSlug.Equals("tutorials", StringComparison.OrdinalIgnoreCase))
 52        {
 53            if (segments.Count < 2)
 54                return null;
 55
 56            var tutorialSlug = segments[1];
 57            var tutorialInfo = await _articleService.GetTutorialBySlugAsync(tutorialSlug, cancellationToken);
 58            if (tutorialInfo == null)
 59                return null;
 60
 61            return new SlugPathResolution(
 62                Kind: ResolvedEntityKind.Tutorial,
 63                WorldId: null,
 64                CampaignId: null,
 65                ArcId: null,
 66                SessionId: null,
 67                MapId: null,
 68                ArticleId: tutorialInfo.Value.ArticleId,
 69                Breadcrumbs: [new SlugPathBreadcrumb(ResolvedEntityKind.Tutorial, tutorialSlug, tutorialInfo.Value.Title
 70        }
 71
 72        // 3. Reserved slug check
 73        if (_reservedSlugProvider.IsReserved(worldSlug))
 74            return null;
 75
 76        // 3. World resolution
 77        var worldInfo = await _worldService.GetIdBySlugAsync(worldSlug);
 78        if (worldInfo == null)
 79            return null;
 80
 81        var (worldId, isPublic, worldName) = worldInfo.Value;
 82
 83        var userIsMember = currentUserId.HasValue &&
 84            await _membershipService.UserHasAccessAsync(worldId, currentUserId.Value);
 85
 86        if (!_readAccessPolicy.CanReadWorld(isPublic, userIsMember))
 87            return null;
 88
 89        var worldBreadcrumb = new SlugPathBreadcrumb(ResolvedEntityKind.World, worldSlug, worldName);
 90
 91        // 4. One segment → World
 92        if (segments.Count == 1)
 93        {
 94            return new SlugPathResolution(
 95                Kind: ResolvedEntityKind.World,
 96                WorldId: worldId,
 97                CampaignId: null,
 98                ArcId: null,
 99                SessionId: null,
 100                MapId: null,
 101                ArticleId: null,
 102                Breadcrumbs: [worldBreadcrumb]);
 103        }
 104
 105        var seg1 = segments[1];
 106
 107        // 5. Two segments — maps short-circuit
 108        if (seg1 == "maps" && segments.Count == 2)
 109        {
 110            return new SlugPathResolution(
 111                Kind: ResolvedEntityKind.MapListing,
 112                WorldId: worldId,
 113                CampaignId: null,
 114                ArcId: null,
 115                SessionId: null,
 116                MapId: null,
 117                ArticleId: null,
 118                Breadcrumbs: [worldBreadcrumb]);
 119        }
 120
 121        // 6. Three segments — maps/{mapSlug}
 122        if (seg1 == "maps" && segments.Count == 3)
 123        {
 124            var mapSlug = segments[2];
 125            var mapInfo = await _mapService.GetIdBySlugAsync(worldId, mapSlug);
 126            if (mapInfo == null)
 127                return null;
 128
 129            return new SlugPathResolution(
 130                Kind: ResolvedEntityKind.Map,
 131                WorldId: worldId,
 132                CampaignId: null,
 133                ArcId: null,
 134                SessionId: null,
 135                MapId: mapInfo.Value.Id,
 136                ArticleId: null,
 137                Breadcrumbs:
 138                [
 139                    worldBreadcrumb,
 140                    new SlugPathBreadcrumb(ResolvedEntityKind.Map, mapSlug, mapInfo.Value.Name)
 141                ]);
 142        }
 143
 144        // maps with wrong segment count
 145        if (seg1 == "maps")
 146            return null;
 147
 148        // 7. wiki/... short-circuit
 149        if (seg1 == "wiki")
 150        {
 151            if (segments.Count < 3)
 152                return null;
 153
 154            var articleSlugs = segments.Skip(2).ToList();
 155            var articlePath = await _articleService.ResolveWorldArticlePathAsync(
 156                worldId, articleSlugs, currentUserId, cancellationToken);
 157
 158            if (articlePath == null)
 159                return null;
 160
 161            var articleBreadcrumbs = articlePath.Value.PathBreadcrumbs
 162                .Select(p => new SlugPathBreadcrumb(ResolvedEntityKind.WikiArticle, p.Slug, p.Title))
 163                .ToList();
 164
 165            return new SlugPathResolution(
 166                Kind: ResolvedEntityKind.WikiArticle,
 167                WorldId: worldId,
 168                CampaignId: null,
 169                ArcId: null,
 170                SessionId: null,
 171                MapId: null,
 172                ArticleId: articlePath.Value.ArticleId,
 173                Breadcrumbs: [worldBreadcrumb, .. articleBreadcrumbs]);
 174        }
 175
 176        // 8. Two segments — campaign
 177        var campaignSlug = seg1;
 178        var campaignInfo = await _campaignService.GetIdBySlugAsync(worldId, campaignSlug);
 179        if (campaignInfo == null)
 180            return null;
 181
 182        if (!_readAccessPolicy.CanReadMemberScopedEntity(userIsMember))
 183            return null;
 184
 185        var campaignBreadcrumb = new SlugPathBreadcrumb(ResolvedEntityKind.Campaign, campaignSlug, campaignInfo.Value.Na
 186
 187        if (segments.Count == 2)
 188        {
 189            return new SlugPathResolution(
 190                Kind: ResolvedEntityKind.Campaign,
 191                WorldId: worldId,
 192                CampaignId: campaignInfo.Value.Id,
 193                ArcId: null,
 194                SessionId: null,
 195                MapId: null,
 196                ArticleId: null,
 197                Breadcrumbs: [worldBreadcrumb, campaignBreadcrumb]);
 198        }
 199
 200        // 9. Three segments — arc
 201        var arcSlug = segments[2];
 202        var arcInfo = await _arcService.GetIdBySlugAsync(campaignInfo.Value.Id, arcSlug);
 203        if (arcInfo == null)
 204            return null;
 205
 206        if (!_readAccessPolicy.CanReadMemberScopedEntity(userIsMember))
 207            return null;
 208
 209        var arcBreadcrumb = new SlugPathBreadcrumb(ResolvedEntityKind.Arc, arcSlug, arcInfo.Value.Name);
 210
 211        if (segments.Count == 3)
 212        {
 213            return new SlugPathResolution(
 214                Kind: ResolvedEntityKind.Arc,
 215                WorldId: worldId,
 216                CampaignId: campaignInfo.Value.Id,
 217                ArcId: arcInfo.Value.Id,
 218                SessionId: null,
 219                MapId: null,
 220                ArticleId: null,
 221                Breadcrumbs: [worldBreadcrumb, campaignBreadcrumb, arcBreadcrumb]);
 222        }
 223
 224        // 10. Four segments — session
 225        var sessionSlug = segments[3];
 226        var sessionInfo = await _sessionService.GetIdBySlugAsync(arcInfo.Value.Id, sessionSlug);
 227        if (sessionInfo == null)
 228            return null;
 229
 230        if (!_readAccessPolicy.CanReadMemberScopedEntity(userIsMember))
 231            return null;
 232
 233        var sessionBreadcrumb = new SlugPathBreadcrumb(ResolvedEntityKind.Session, sessionSlug, sessionInfo.Value.Name);
 234
 235        if (segments.Count == 4)
 236        {
 237            return new SlugPathResolution(
 238                Kind: ResolvedEntityKind.Session,
 239                WorldId: worldId,
 240                CampaignId: campaignInfo.Value.Id,
 241                ArcId: arcInfo.Value.Id,
 242                SessionId: sessionInfo.Value.Id,
 243                MapId: null,
 244                ArticleId: null,
 245                Breadcrumbs: [worldBreadcrumb, campaignBreadcrumb, arcBreadcrumb, sessionBreadcrumb]);
 246        }
 247
 248        // 11. Five segments — session note
 249        if (segments.Count == 5)
 250        {
 251            var noteSlug = segments[4];
 252            var noteInfo = await _articleService.GetSessionNoteBySlugAsync(
 253                sessionInfo.Value.Id, noteSlug, currentUserId, cancellationToken);
 254
 255            if (noteInfo == null)
 256                return null;
 257
 258            if (!_readAccessPolicy.CanReadMemberScopedEntity(userIsMember))
 259                return null;
 260
 261            return new SlugPathResolution(
 262                Kind: ResolvedEntityKind.SessionNote,
 263                WorldId: worldId,
 264                CampaignId: campaignInfo.Value.Id,
 265                ArcId: arcInfo.Value.Id,
 266                SessionId: sessionInfo.Value.Id,
 267                MapId: null,
 268                ArticleId: noteInfo.Value.ArticleId,
 269                Breadcrumbs:
 270                [
 271                    worldBreadcrumb,
 272                    campaignBreadcrumb,
 273                    arcBreadcrumb,
 274                    sessionBreadcrumb,
 275                    new SlugPathBreadcrumb(ResolvedEntityKind.SessionNote, noteSlug, noteInfo.Value.Title)
 276                ]);
 277        }
 278
 279        // 12. Six or more segments
 280        return null;
 281    }
 282}