< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.ParameterForInjectionInfo
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/ParameterForInjectionInfo.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
2%
Covered lines: 4
Uncovered lines: 183
Coverable lines: 187
Total lines: 458
Line coverage: 2.1%
Branch coverage
1%
Covered branches: 2
Total branches: 182
Branch coverage: 1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 12/12/2025 - 17:27:19 Line coverage: 2.1% (4/182) Branch coverage: 1.2% (2/160) Total lines: 437 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 2.1% (4/187) Branch coverage: 1% (2/182) Total lines: 458 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a 12/12/2025 - 17:27:19 Line coverage: 2.1% (4/182) Branch coverage: 1.2% (2/160) Total lines: 437 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/14/2025 - 20:04:52 Line coverage: 2.1% (4/187) Branch coverage: 1% (2/182) Total lines: 458 Tag: Kestrun/Kestrun@a05ac8de57c6207e227b92ba360e9d58869ac80a

Metrics

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/ParameterForInjectionInfo.cs

#LineLine coverage
 1
 2using System.Collections;
 3using System.Management.Automation;
 4using System.Text.Json;
 5using System.Text.Json.Nodes;
 6using System.Xml.Linq;
 7using Kestrun.Logging;
 8using Kestrun.Models;
 9using Microsoft.Extensions.Primitives;
 10using Microsoft.OpenApi;
 11using YamlDotNet.Serialization;
 12using YamlDotNet.Serialization.NamingConventions;
 13
 14namespace Kestrun.Languages;
 15/// <summary>
 16/// Information about a parameter to be injected into a script.
 17/// </summary>
 18public record ParameterForInjectionInfo
 19{
 20    /// <summary>
 21    /// The name of the parameter.
 22    /// </summary>
 023    public string Name { get; init; }
 24
 25    /// <summary>
 26    /// The .NET type of the parameter.
 27    /// </summary>
 028    public Type ParameterType { get; }
 29
 30    /// <summary>
 31    /// The JSON schema type of the parameter.
 32    /// </summary>
 033    public JsonSchemaType? Type { get; init; }
 34
 35    /// <summary>
 36    /// The default value of the parameter.
 37    /// </summary>
 038    public JsonNode? DefaultValue { get; }
 39
 40    /// <summary>
 41    /// The location of the parameter.
 42    /// </summary>
 043    public ParameterLocation? In { get; init; }
 44
 45    /// <summary>
 46    /// Indicates whether the parameter is from the request body.
 47    /// </summary>
 048    public bool IsRequestBody => In is null;
 49
 50    /// <summary>
 51    /// Constructs a ParameterForInjectionInfo from an OpenApiParameter.
 52    /// </summary>
 53    /// <param name="paramInfo">The parameter metadata.</param>
 54    /// <param name="parameter">The OpenApiParameter to construct from.</param>
 055    public ParameterForInjectionInfo(ParameterMetadata paramInfo, OpenApiParameter? parameter)
 56    {
 057        ArgumentNullException.ThrowIfNull(parameter);
 058        ArgumentNullException.ThrowIfNull(paramInfo);
 059        Name = paramInfo.Name;
 060        ParameterType = paramInfo.ParameterType;
 061        Type = parameter.Schema?.Type;
 062        DefaultValue = parameter.Schema?.Default;
 063        In = parameter.In;
 064    }
 65    /// <summary>
 66    /// Constructs a ParameterForInjectionInfo from an OpenApiRequestBody.
 67    /// </summary>
 68    /// <param name="paramInfo">The parameter metadata.</param>
 69    /// <param name="requestBody">The OpenApiRequestBody to construct from.</param>
 070    public ParameterForInjectionInfo(ParameterMetadata paramInfo, OpenApiRequestBody requestBody)
 71    {
 072        ArgumentNullException.ThrowIfNull(requestBody);
 073        ArgumentNullException.ThrowIfNull(paramInfo);
 074        Name = paramInfo.Name;
 075        ParameterType = paramInfo.ParameterType;
 076        Type = requestBody.Content?.Values.FirstOrDefault()?.Schema?.Type;
 077        var schema = requestBody.Content?.Values.FirstOrDefault()?.Schema;
 078        if (schema is OpenApiSchemaReference)
 79        {
 080            Type = JsonSchemaType.Object;
 81        }
 082        else if (schema is OpenApiSchema sch)
 83        {
 084            Type = sch.Type;
 085            DefaultValue = sch.Default;
 86        }
 087        In = null;
 088    }
 89
 90    /// <summary>
 91    /// Adds parameters from the HTTP context to the PowerShell instance.
 92    /// </summary>
 93    /// <param name="context">The current HTTP context.</param>
 94    /// <param name="ps">The PowerShell instance to which parameters will be added.</param>
 95    internal static void InjectParameters(KestrunContext context, PowerShell ps)
 96    {
 497        if (context.HttpContext.GetEndpoint()?
 498               .Metadata
 499               .FirstOrDefault(m => m is List<ParameterForInjectionInfo>) is not List<ParameterForInjectionInfo> paramet
 100        {
 4101            return;
 102        }
 103
 0104        var logger = context.Host.Logger;
 0105        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 106        {
 0107            logger.Debug("Injecting {Count} parameters into PowerShell script.", parameters.Count);
 108        }
 109
 0110        foreach (var param in parameters)
 111        {
 0112            InjectSingleParameter(context, ps, param);
 113        }
 0114    }
 115
 116    /// <summary>
 117    /// Injects a single parameter into the PowerShell instance based on its location and type.
 118    /// </summary>
 119    /// <param name="context">The current Kestrun context.</param>
 120    /// <param name="ps">The PowerShell instance to inject parameters into.</param>
 121    /// <param name="param">The parameter information to inject.</param>
 122    private static void InjectSingleParameter(KestrunContext context, PowerShell ps, ParameterForInjectionInfo param)
 123    {
 0124        var logger = context.Host.Logger;
 0125        var name = param.Name;
 126
 0127        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 128        {
 0129            logger.Debug("Injecting parameter '{Name}' of type '{Type}' from '{In}'.", name, param.Type, param.In);
 130        }
 131
 132        object? converted;
 0133        var shouldLog = true;
 134
 0135        converted = context.Request.Form is not null && context.Request.HasFormContentType
 0136            ? ConvertFormToValue(context.Request.Form, param)
 0137            : GetParameterValueFromContext(context, param, out shouldLog);
 138
 0139        if (shouldLog && logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 140        {
 0141            logger.DebugSanitized("Adding parameter '{Name}': {ConvertedValue}", name, converted);
 142        }
 143
 0144        _ = ps.AddParameter(name, converted);
 0145    }
 146
 147    private static object? GetParameterValueFromContext(KestrunContext context, ParameterForInjectionInfo param, out boo
 148    {
 0149        shouldLog = true;
 0150        var logger = context.Host.Logger;
 0151        var raw = GetRawValue(param, context);
 152
 0153        if (raw is null)
 154        {
 0155            if (param.DefaultValue is not null)
 156            {
 0157                raw = param.DefaultValue.GetValue<object>();
 158            }
 159            else
 160            {
 0161                shouldLog = false;
 0162                return null;
 163            }
 164        }
 165
 0166        if (logger.IsEnabled(Serilog.Events.LogEventLevel.Debug))
 167        {
 0168            logger.Debug("Raw value for parameter '{Name}': {RawValue}", param.Name, raw);
 169        }
 170
 0171        var (singleValue, multiValue) = NormalizeRaw(raw);
 172
 0173        if (singleValue is null && multiValue is null)
 174        {
 0175            shouldLog = false;
 0176            return null;
 177        }
 178
 0179        return ConvertValue(context, param, singleValue, multiValue);
 180    }
 181
 182    /// <summary>
 183    /// Retrieves the raw value of a parameter from the HTTP context based on its location.
 184    /// </summary>
 185    /// <param name="param">The parameter information.</param>
 186    /// <param name="context">The current HTTP context.</param>
 187    /// <returns>The raw value of the parameter.</returns>
 188    private static object? GetRawValue(ParameterForInjectionInfo param, KestrunContext context)
 189    {
 0190        return param.In switch
 0191        {
 0192            ParameterLocation.Path =>
 0193            context.Request.RouteValues.TryGetValue(param.Name, out var routeVal)
 0194                ? routeVal
 0195                : null,
 0196
 0197            ParameterLocation.Query =>
 0198                context.Request.Query.TryGetValue(param.Name, out var queryVal)
 0199                    ? (string?)queryVal
 0200                    : null,
 0201
 0202            ParameterLocation.Header =>
 0203                context.Request.Headers.TryGetValue(param.Name, out var headerVal)
 0204                    ? (string?)headerVal
 0205                    : null,
 0206
 0207            ParameterLocation.Cookie =>
 0208                context.Request.Cookies.TryGetValue(param.Name, out var cookieVal)
 0209                    ? cookieVal
 0210                    : null,
 0211            null => (context.Request.Form is not null && context.Request.HasFormContentType) ?
 0212                    context.Request.Form :
 0213                    context.Request.Body,
 0214
 0215            _ => null,
 0216        };
 217    }
 218
 219    /// <summary>
 220    /// Normalizes the raw parameter value into single and multi-value forms.
 221    /// </summary>
 222    /// <param name="raw">The raw parameter value.</param>
 223    /// <returns>A tuple containing the single and multi-value forms of the parameter.</returns>
 224    private static (string? single, string?[]? multi) NormalizeRaw(object raw)
 225    {
 0226        string?[]? multiValue = null;
 227
 228        string? singleValue;
 229        switch (raw)
 230        {
 231            case StringValues sv:
 0232                multiValue = [.. sv];
 0233                singleValue = sv.Count > 0 ? sv[0] : null;
 0234                break;
 235
 236            case string s:
 0237                singleValue = s;
 0238                break;
 239
 240            default:
 0241                singleValue = raw?.ToString();
 242                break;
 243        }
 244
 0245        return (singleValue, multiValue);
 246    }
 247
 248    /// <summary>
 249    /// Converts the parameter value to the appropriate type based on the JSON schema type.
 250    /// </summary>
 251    /// <param name="context">The current HTTP context.</param>
 252    /// <param name="param">The parameter information.</param>
 253    /// <param name="singleValue">The single value of the parameter.</param>
 254    /// <param name="multiValue">The multi-value of the parameter.</param>
 255    /// <returns>The converted parameter value.</returns>
 256    private static object? ConvertValue(KestrunContext context, ParameterForInjectionInfo param,
 257    string? singleValue, string?[]? multiValue)
 258    {
 259        // Convert based on schema type
 0260        return param.Type switch
 0261        {
 0262            JsonSchemaType.Integer => int.TryParse(singleValue, out var i) ? (int?)i : null,
 0263            JsonSchemaType.Number => double.TryParse(singleValue, out var d) ? (double?)d : null,
 0264            JsonSchemaType.Boolean => bool.TryParse(singleValue, out var b) ? (bool?)b : null,
 0265            JsonSchemaType.Array => multiValue ?? (singleValue is not null ? new[] { singleValue } : null), // keep your
 0266            JsonSchemaType.Object =>
 0267                ConvertBodyBasedOnContentType(context, singleValue ?? ""),
 0268            JsonSchemaType.String => singleValue,
 0269            _ => singleValue,
 0270        };
 271    }
 272    //todo: test Yaml and XML bodies
 273    private static object? ConvertBodyBasedOnContentType(
 274       KestrunContext context,
 275       string rawBodyString)
 276    {
 0277        var contentType = context.Request.ContentType?.ToLowerInvariant() ?? "";
 278
 0279        if (contentType.Contains("json"))
 280        {
 0281            return ConvertJsonToHashtable(rawBodyString);
 282        }
 283
 0284        if (contentType.Contains("yaml") || contentType.Contains("yml"))
 285        {
 0286            return ConvertYamlToHashtable(rawBodyString);
 287        }
 288
 0289        if (contentType.Contains("xml"))
 290        {
 0291            return ConvertXmlToHashtable(rawBodyString);
 292        }
 293
 0294        if (contentType.Contains("application/x-www-form-urlencoded"))
 295        {
 0296            return ConvertFormToHashtable(context.Request.Form);
 297        }
 298
 0299        return rawBodyString; // fallback
 300    }
 301
 0302    private static readonly IDeserializer YamlDeserializer =
 0303    new DeserializerBuilder()
 0304        .WithNamingConvention(CamelCaseNamingConvention.Instance)
 0305        .Build();
 306
 307    private static Hashtable? ConvertYamlToHashtable(string yaml)
 308    {
 0309        if (string.IsNullOrWhiteSpace(yaml))
 310        {
 0311            return null;
 312        }
 313
 314        // Top-level YAML mapping → Hashtable
 0315        var ht = YamlDeserializer.Deserialize<Hashtable>(yaml);
 0316        return ht;
 317    }
 318    private static object? ConvertJsonToHashtable(string? json)
 319    {
 0320        if (string.IsNullOrWhiteSpace(json))
 321        {
 0322            return null;
 323        }
 324
 0325        using var doc = JsonDocument.Parse(json);
 0326        return JsonElementToClr(doc.RootElement);
 0327    }
 328
 329    private static object? JsonElementToClr(JsonElement element)
 330    {
 0331        return element.ValueKind switch
 0332        {
 0333            JsonValueKind.Object => ToHashtable(element),
 0334            JsonValueKind.Array => ToArray(element),
 0335            JsonValueKind.String => element.GetString(),
 0336            JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
 0337            JsonValueKind.True => true,
 0338            JsonValueKind.False => false,
 0339            JsonValueKind.Null => null,
 0340            JsonValueKind.Undefined => null,
 0341            _ => null
 0342        };
 343    }
 344
 345    private static Hashtable ToHashtable(JsonElement element)
 346    {
 0347        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 0348        foreach (var prop in element.EnumerateObject())
 349        {
 0350            ht[prop.Name] = JsonElementToClr(prop.Value);
 351        }
 0352        return ht;
 353    }
 354
 355    private static object?[] ToArray(JsonElement element)
 356    {
 0357        var list = new List<object?>();
 0358        foreach (var item in element.EnumerateArray())
 359        {
 0360            list.Add(JsonElementToClr(item));
 361        }
 0362        return [.. list];
 363    }
 364
 365    private static object? ConvertXmlToHashtable(string xml)
 366    {
 0367        if (string.IsNullOrWhiteSpace(xml))
 368        {
 0369            return null;
 370        }
 371
 0372        var root = XElement.Parse(xml);
 0373        return XElementToClr(root);
 374    }
 375
 376    private static object? XElementToClr(XElement element)
 377    {
 378        // If element has no children and no attributes → return primitive string
 0379        if (!element.HasElements && !element.HasAttributes)
 380        {
 0381            var trimmed = element.Value?.Trim();
 0382            return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
 383        }
 384
 0385        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 386
 387        // Attributes as @name entries
 0388        foreach (var attr in element.Attributes())
 389        {
 0390            ht["@" + attr.Name.LocalName] = attr.Value;
 391        }
 392
 393        // Children
 0394        var childGroups = element.Elements().GroupBy(e => e.Name.LocalName);
 395
 0396        foreach (var group in childGroups)
 397        {
 0398            var key = group.Key;
 0399            var items = group.ToList();
 400
 0401            if (items.Count == 1)
 402            {
 403                // Single element → convert directly
 0404                ht[key] = XElementToClr(items[0]);
 405            }
 406            else
 407            {
 408                // Multiple elements with same name → array
 0409                var arr = new object?[items.Count];
 0410                for (var i = 0; i < items.Count; i++)
 411                {
 0412                    arr[i] = XElementToClr(items[i]);
 413                }
 414
 0415                ht[key] = arr;
 416            }
 417        }
 418
 0419        return ht;
 420    }
 421
 422    /// <summary>
 423    /// Converts a form dictionary to a hashtable.
 424    /// </summary>
 425    /// <param name="form">The form dictionary to convert.</param>
 426    /// <returns>A hashtable representing the form data.</returns>
 427    private static Hashtable? ConvertFormToHashtable(Dictionary<string, string>? form)
 428    {
 0429        if (form is null || form.Count == 0)
 430        {
 0431            return null;
 432        }
 433
 0434        var ht = new Hashtable(StringComparer.OrdinalIgnoreCase);
 435
 0436        foreach (var kvp in form)
 437        {
 438            // x-www-form-urlencoded in your case has a single value per key
 0439            ht[kvp.Key] = kvp.Value;
 440        }
 441
 0442        return ht;
 443    }
 444
 445    private static object? ConvertFormToValue(Dictionary<string, string>? form, ParameterForInjectionInfo param)
 446    {
 0447        if (form is null || form.Count == 0)
 448        {
 0449            return null;
 450        }
 451
 452        // If the parameter is a simple type, return the first key if there's only one key-value pair
 453        // and it's a simple type (not an object or array)
 0454        return param.Type is JsonSchemaType.Integer or JsonSchemaType.Number or JsonSchemaType.Boolean or JsonSchemaType
 0455            ? form.Count == 1 ? form.First().Key : null
 0456            : ConvertFormToHashtable(form);
 457    }
 458}