< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Callback.DefaultCallbackUrlResolver
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/DefaultCallbackUrlResolver.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
78%
Covered lines: 52
Uncovered lines: 14
Coverable lines: 66
Total lines: 164
Line coverage: 78.7%
Branch coverage
61%
Covered branches: 27
Total branches: 44
Branch coverage: 61.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 01/02/2026 - 00:16:25 Line coverage: 78.7% (52/66) Branch coverage: 61.3% (27/44) Total lines: 164 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d0 01/02/2026 - 00:16:25 Line coverage: 78.7% (52/66) Branch coverage: 61.3% (27/44) Total lines: 164 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Resolve(...)81.25%161696.77%
ReplaceTokens(...)100%44100%
SerializePayloadToJsonElement(...)100%1140%
ResolveJsonPointer(...)41.66%1082447.36%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/DefaultCallbackUrlResolver.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using System.Text.Json;
 3namespace Kestrun.Callback;
 4
 5/// <summary>
 6/// Default implementation of <see cref="ICallbackUrlResolver"/> that resolves callback URLs
 7/// using JSON Pointer expressions and variable tokens.
 8/// </summary>
 9public sealed partial class DefaultCallbackUrlResolver : ICallbackUrlResolver
 10{
 111    private static readonly Regex RuntimeExpr = GeneratedRuntimeRegex();
 12
 113    private static readonly Regex Token = GeneratedTokenRegex();
 14
 15    /// <summary>
 16    /// Resolves the given URL template into a full URI using the provided callback runtime context.
 17    /// </summary>
 18    /// <param name="urlTemplate">The URL template containing tokens and JSON Pointer expressions.</param>
 19    /// <param name="ctx">The callback runtime context providing values for tokens and JSON data.</param>
 20    /// <returns>A fully resolved URI based on the template and context.</returns>
 21    /// <exception cref="ArgumentException">Thrown when the urlTemplate is null, empty, or whitespace.</exception>
 22    /// <exception cref="InvalidOperationException">Thrown when the URL template cannot be resolved to a valid URI.</exc
 23    public Uri Resolve(string urlTemplate, CallbackRuntimeContext ctx)
 24    {
 525        if (string.IsNullOrWhiteSpace(urlTemplate))
 26        {
 027            throw new ArgumentException("urlTemplate is empty.", nameof(urlTemplate));
 28        }
 29
 530        ArgumentNullException.ThrowIfNull(ctx);
 31
 32        // 1) Resolve {$request.body#/json/pointer} using the typed body
 533        var s = RuntimeExpr.Replace(urlTemplate, m =>
 534        {
 335            var ptr = m.Groups["ptr"].Value; // like "/callbackUrls/status"
 536
 337            if (ctx.CallbackPayload is null)
 538            {
 139                throw new InvalidOperationException(
 140                    $"Callback url uses request.body pointer '{ptr}' but the request body is null.");
 541            }
 542            // Convert typed object -> JsonElement (on demand)
 243            var root = SerializePayloadToJsonElement(ctx.CallbackPayload, ptr);
 544
 245            var value = ResolveJsonPointer(root, ptr);
 546
 547            // Runtime expressions are inserted as text; strings are inserted raw,
 548            // non-strings use their JSON textual form.
 249            return value.ValueKind == JsonValueKind.String
 250                ? (value.GetString() ?? "")
 251                : value.GetRawText();
 552        });
 53
 54        // 2) Replace {token} placeholders from Vars
 455        s = ReplaceTokens(s, ctx);
 56
 57        // 3) Make Uri
 358        if (Uri.TryCreate(s, UriKind.Absolute, out var abs))
 59        {
 60            // On Unix, a leading-slash path like "/v1/foo" parses as an absolute file URI (file:///v1/foo).
 61            // Callback URLs are expected to be HTTP(S). Treat file URIs from leading-slash inputs as relative.
 362            if (!(abs.Scheme == Uri.UriSchemeFile && s.StartsWith('/')))
 63            {
 164                return abs;
 65            }
 66        }
 67        // Relative Uri: combine with DefaultBaseUri
 268        return ctx.DefaultBaseUri is null
 269            ? throw new InvalidOperationException(
 270                $"Callback url resolved to '{s}' (not absolute) and DefaultBaseUri is null.")
 271            : new Uri(ctx.DefaultBaseUri, s);
 72    }
 73
 74    /// Replaces {token} placeholders in the input string using values from the callback runtime context.
 75    /// <param name="input">The input string containing {token} placeholders.</param>
 76    /// <param name="ctx">The callback runtime context providing values for tokens.</param>
 77    /// <returns>The input string with all {token} placeholders replaced with their corresponding values.</returns>
 78    private static string ReplaceTokens(string input, CallbackRuntimeContext ctx)
 79    {
 480        return Token.Replace(input, m =>
 481        {
 482            var name = m.Groups["name"].Value;
 483
 484            return !ctx.Vars.TryGetValue(name, out var v) || v is null
 485                ? throw new InvalidOperationException(
 486                    $"Callback url requires token '{name}' but it was not found in runtime Vars.")
 487                : Uri.EscapeDataString(v.ToString()!);
 488        });
 89    }
 90
 91    /// <summary>
 92    /// Serializes the given payload object to a JsonElement for JSON Pointer resolution.
 93    /// </summary>
 94    /// <param name="payload">The payload object to serialize.</param>
 95    /// <param name="ptr">The JSON Pointer string (for error context).</param>
 96    /// <returns>The serialized JsonElement.</returns>
 97    private static JsonElement SerializePayloadToJsonElement(object payload, string ptr)
 98    {
 99        try
 100        {
 2101            return JsonSerializer.SerializeToElement(payload);
 102        }
 0103        catch (Exception ex)
 104        {
 0105            throw new InvalidOperationException(
 0106                $"Failed to serialize request body for evaluating pointer '{ptr}'.", ex);
 107        }
 2108    }
 109    // Minimal JSON Pointer resolver (RFC 6901-ish)
 110    private static JsonElement ResolveJsonPointer(JsonElement root, string pointer)
 111    {
 2112        if (pointer is "" or "/")
 113        {
 0114            return root;
 115        }
 116
 2117        if (!pointer.StartsWith('/'))
 118        {
 0119            throw new FormatException($"Invalid JSON pointer '{pointer}'.");
 120        }
 121
 2122        var current = root;
 2123        var segments = pointer.Split('/', StringSplitOptions.RemoveEmptyEntries);
 124
 12125        foreach (var raw in segments)
 126        {
 4127            var seg = raw.Replace("~1", "/").Replace("~0", "~");
 128
 4129            if (current.ValueKind == JsonValueKind.Object)
 130            {
 4131                if (!current.TryGetProperty(seg, out current))
 132                {
 0133                    throw new KeyNotFoundException($"JSON pointer segment '{seg}' not found.");
 134                }
 135            }
 0136            else if (current.ValueKind == JsonValueKind.Array)
 137            {
 0138                if (!int.TryParse(seg, out var idx))
 139                {
 0140                    throw new FormatException($"JSON pointer segment '{seg}' is not a valid array index.");
 141                }
 142
 0143                if (idx < 0 || idx >= current.GetArrayLength())
 144                {
 0145                    throw new IndexOutOfRangeException($"JSON pointer index {idx} out of range.");
 146                }
 147
 0148                current = current[idx];
 149            }
 150            else
 151            {
 0152                throw new InvalidOperationException($"Cannot traverse JSON pointer through {current.ValueKind}.");
 153            }
 154        }
 155
 2156        return current;
 157    }
 158
 159    [GeneratedRegex(@"\{\$request\.body#(?<ptr>\/[^}]*)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
 160    private static partial Regex GeneratedRuntimeRegex();
 161
 162    [GeneratedRegex(@"\{(?<name>[A-Za-z_][A-Za-z0-9_]*)\}", RegexOptions.Compiled | RegexOptions.CultureInvariant)]
 163    private static partial Regex GeneratedTokenRegex();
 164}