< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Callback.CallbackRequestFactory
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Callback/CallbackRequestFactory.cs
Tag: Kestrun/Kestrun@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
97%
Covered lines: 77
Uncovered lines: 2
Coverable lines: 79
Total lines: 150
Line coverage: 97.4%
Branch coverage
79%
Covered branches: 19
Total branches: 24
Branch coverage: 79.1%
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: 97.4% (77/79) Branch coverage: 79.1% (19/24) Total lines: 150 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d0 01/02/2026 - 00:16:25 Line coverage: 97.4% (77/79) Branch coverage: 79.1% (19/24) Total lines: 150 Tag: Kestrun/Kestrun@8405dc23b786b9d436fba0d65fb80baa4171e1d0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
ExtractTemplateParams(...)83.33%6687.5%
FromPlan(...)77.77%181898.55%

File(s)

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

#LineLine coverage
 1using Kestrun.Models;
 2using System.Text.RegularExpressions;
 3using Serilog.Events;
 4namespace Kestrun.Callback;
 5
 6/// <summary>
 7/// Factory for creating <see cref="CallbackRequest"/> instances from callback plans and runtime context.
 8/// </summary>
 9public static partial class CallbackRequestFactory
 10{
 111    private static readonly Regex TemplateParamRegex =
 112        TemplateParameterRegex();
 13    // - captures {id} and also {id:int} style constraints (keeps "id")
 14
 15    private static HashSet<string> ExtractTemplateParams(string urlTemplate)
 16    {
 217        var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 18
 219        if (string.IsNullOrWhiteSpace(urlTemplate))
 20        {
 021            return names;
 22        }
 23
 824        foreach (Match m in TemplateParamRegex.Matches(urlTemplate))
 25        {
 226            var name = m.Groups["name"].Value.Trim();
 227            if (!string.IsNullOrEmpty(name))
 28            {
 229                _ = names.Add(name);
 30            }
 31        }
 32
 233        return names;
 34    }
 35
 36    /// <summary>
 37    /// Creates a list of <see cref="CallbackRequest"/> instances from the given callback plans and runtime context.
 38    /// </summary>
 39    /// <param name="executionPlan">The callback plan to create a request from.</param>
 40    /// <param name="ctx">The callback runtime context providing values for tokens and JSON data.</param>
 41    /// <param name="urlResolver">The URL resolver to resolve callback URLs.</param>
 42    /// <param name="bodySerializer">The body serializer to serialize callback request bodies.</param>
 43    /// <param name="options">The callback dispatch options.</param>
 44    /// <returns>A list of created <see cref="CallbackRequest"/> instances.</returns>
 45    public static CallbackRequest FromPlan(
 46        CallBackExecutionPlan executionPlan,
 47        KestrunContext ctx,
 48        ICallbackUrlResolver urlResolver,
 49        ICallbackBodySerializer bodySerializer,
 50        CallbackDispatchOptions options)
 51    {
 252        ArgumentNullException.ThrowIfNull(executionPlan);
 253        ArgumentNullException.ThrowIfNull(ctx);
 254        ArgumentNullException.ThrowIfNull(urlResolver);
 255        ArgumentNullException.ThrowIfNull(bodySerializer);
 256        ArgumentNullException.ThrowIfNull(options);
 57
 258        var correlationId = ctx.TraceIdentifier;
 259        var plan = executionPlan.Plan;
 60        // Build callback runtime context:
 61        // - Start with request-derived vars/body (for {$request.body#/...} runtime expressions)
 62        // - Overlay callback execution-plan parameters (for {token} placeholders like {paymentId})
 263        var requestRt = CallbackRuntimeContextFactory.FromHttpContext(ctx);
 264        var mergedVars = new Dictionary<string, object?>(requestRt.Vars, StringComparer.OrdinalIgnoreCase);
 1065        foreach (var (name, value) in executionPlan.Parameters)
 66        {
 367            mergedVars[name] = value;
 68        }
 69
 270        var rt = requestRt with { Vars = mergedVars };
 71
 72        // 1) Extract placeholder names from the template
 273        var templateParamNames = ExtractTemplateParams(plan.UrlTemplate);
 74
 75        // 2) Build a stable seed based on those placeholders + resolved values
 76        // (sorted so ordering in template doesn't change the key)
 277        var seedParts = templateParamNames
 278            .OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
 279            .Select(n =>
 280            {
 281                if (!mergedVars.TryGetValue(n, out var resolved) || resolved is null)
 282                {
 083                    return null;
 284                }
 285
 286                var s = resolved.ToString();
 287                return string.IsNullOrWhiteSpace(s) ? null : $"{n}={s}";
 288            })
 289            .Where(x => x is not null)
 290            .ToArray();
 91
 292        var idSeed = seedParts.Length > 0
 293            ? string.Join("&", seedParts)
 294            : correlationId;
 95
 296        var idempotencyKey = $"{idSeed}:{plan.CallbackId}:{plan.OperationId}";
 97
 298        var targetUrl = urlResolver.Resolve(plan.UrlTemplate, rt);
 99        //Todo: Add option to override content type?
 2100        var (contentType, _) = bodySerializer.Serialize(plan, rt);
 101        //Todo: Add option to override content type?
 102        //Todo: Add custom headers
 2103        var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 2104        {
 2105            ["X-Correlation-Id"] = correlationId,
 2106            ["Idempotency-Key"] = idempotencyKey,
 2107            ["X-Kestrun-CallbackId"] = plan.CallbackId
 2108        };
 109        // Add body
 2110        var bodyBytes = (executionPlan.BodyParameterName == null) ?
 2111            null :
 2112            System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(executionPlan.Parameters[executionPlan.BodyParameterNam
 2113        if (ctx.Logger.IsEnabled(LogEventLevel.Debug))
 114        {
 115            // Sanitize idempotency key for logging
 2116            var safeIdempotencyKeyForLog = idempotencyKey
 2117            .Replace("\r", string.Empty)
 2118            .Replace("\n", string.Empty);
 119            // Log details
 2120            ctx.Logger.Debug("Created CallbackRequest: CallbackId={CallbackId}, OperationId={OperationId}, TargetUrl={Ta
 2121                plan.CallbackId,
 2122                plan.OperationId,
 2123                targetUrl,
 2124                plan.Method.Method.ToUpperInvariant(),
 2125                contentType,
 2126                bodyBytes?.Length ?? 0,
 2127                correlationId,
 2128                safeIdempotencyKeyForLog);
 129
 2130            ctx.Logger.Debug("CallbackRequest Headers: {Headers}", headers);
 2131            ctx.Logger.Debug("CallbackRequest Body: {Body}", bodyBytes is null ? "<null>" : System.Text.Encoding.UTF8.Ge
 132        }
 133        // Create CallbackRequest
 2134        return new CallbackRequest(
 2135            callbackId: plan.CallbackId,
 2136            operationId: plan.OperationId,
 2137            targetUrl: targetUrl,
 2138            httpMethod: plan.Method.Method.ToUpperInvariant(),
 2139            headers: headers,
 2140            contentType: contentType,
 2141            body: bodyBytes,
 2142            correlationId: correlationId,
 2143            idempotencyKey: idempotencyKey,
 2144            timeout: options.DefaultTimeout
 2145        );
 146    }
 147
 148    [GeneratedRegex(@"\{(?<name>[^{}:/\?]+)(?:[:][^{}]+)?\}", RegexOptions.Compiled)]
 149    private static partial Regex TemplateParameterRegex();
 150}