< Summary

Information
Class: NexusLabs.Needlr.AgentFramework.AgentFrameworkArgumentExtractor
Assembly: NexusLabs.Needlr.AgentFramework
File(s): /home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/AgentFrameworkArgumentExtractor.cs
Line coverage
77%
Covered lines: 158
Uncovered lines: 45
Coverable lines: 203
Total lines: 616
Line coverage: 77.8%
Branch coverage
64%
Covered branches: 105
Total branches: 164
Branch coverage: 64%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IsArgumentSupplied(...)100%88100%
GetStringArgument(...)87.5%1616100%
GetBooleanArgument(...)85.71%1414100%
GetInt32Argument(...)100%22100%
GetInt64Argument(...)0%22100%
GetInt16Argument(...)0%22100%
GetSByteArgument(...)0%620%
GetByteArgument(...)0%22100%
GetUInt16Argument(...)0%620%
GetUInt32Argument(...)0%22100%
GetUInt64Argument(...)0%620%
GetSingleArgument(...)50%22100%
GetDoubleArgument(...)0%2280%
GetDecimalArgument(...)56.25%271664.7%
GetGuidArgument(...)91.66%1212100%
GetDateTimeArgument(...)58.33%131283.33%
GetDateTimeOffsetArgument(...)50%171266.66%
GetTimeSpanArgument(...)58.33%131284.61%
TryParseTimeSpan(...)75%4481.81%
ExtractInteger(...)87.5%1616100%
ExtractFloat(...)50%952246.66%

File(s)

/home/runner/work/needlr/needlr/src/NexusLabs.Needlr.AgentFramework/AgentFrameworkArgumentExtractor.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text.Json;
 3
 4namespace NexusLabs.Needlr.AgentFramework;
 5
 6/// <summary>
 7/// Kind-tolerant argument extractors for source-generated <c>[AgentFunction]</c> wrappers.
 8/// </summary>
 9/// <remarks>
 10/// <para>
 11/// <c>Microsoft.Extensions.AI.AIFunctionArguments</c> delivers tool argument values as
 12/// <see langword="object"/> — but the underlying <c>IChatClient</c> determines the
 13/// runtime shape:
 14/// </para>
 15/// <list type="bullet">
 16/// <item>
 17/// <description>
 18/// GitHub Copilot's <c>IChatClient</c> stringifies values: arrays/objects arrive as
 19/// <see cref="JsonElement"/> with <see cref="JsonValueKind.String"/>.
 20/// </description>
 21/// </item>
 22/// <item>
 23/// <description>
 24/// <c>AzureOpenAIClient.AsIChatClient()</c> (and most others) parse the model's tool-call
 25/// JSON literally: arrays arrive as <see cref="JsonValueKind.Array"/>, objects as
 26/// <see cref="JsonValueKind.Object"/>, numbers as <see cref="JsonValueKind.Number"/>, etc.
 27/// </description>
 28/// </item>
 29/// </list>
 30/// <para>
 31/// Calling <c>JsonElement.GetString()</c> / <c>GetInt32()</c> / <c>GetBoolean()</c>
 32/// directly throws <see cref="InvalidOperationException"/> when <see cref="JsonValueKind"/>
 33/// doesn't match. These extractors translate per-kind so the source generator can emit a
 34/// single uniform call regardless of the chat client's delivery shape.
 35/// </para>
 36/// <para>
 37/// <strong>Extractors assume the value is present and non-null.</strong> Every extractor
 38/// throws on <see langword="null"/> raws, on <see cref="JsonValueKind.Null"/>, and on
 39/// <see cref="JsonValueKind.Undefined"/>. The generator is responsible for missing-key /
 40/// null / undefined handling — typically by gating each extraction call with
 41/// <see cref="IsArgumentSupplied(object?)"/> and short-circuiting to a declared default
 42/// value or to <see langword="null"/> for nullable parameter types. This keeps the
 43/// presence/optionality concern at the generator layer where the C# parameter metadata
 44/// (<c>HasExplicitDefaultValue</c>, <c>NullableAnnotation</c>) lives, and keeps the
 45/// extractors focused on kind-tolerant decoding of <em>present</em> values.
 46/// </para>
 47/// <para>
 48/// <strong>Strict semantics for typed primitives.</strong> Booleans receiving numeric kinds
 49/// throw rather than coerce <c>0</c>/<c>1</c> — the model is violating its own schema; the
 50/// helper does not paper over it. Numeric extractors prefer the precision-preserving
 51/// <c>TryGet*</c> overload (<c>TryGetDecimal</c> for decimal, <c>TryGetDouble</c> for double)
 52/// and fall back to invariant-culture parsing on <see cref="JsonValueKind.String"/>.
 53/// </para>
 54/// <para>
 55/// <strong>Supported parameter types.</strong> <see cref="string"/>, <see cref="bool"/>, the
 56/// 8 integer types (<see cref="byte"/>/<see cref="sbyte"/>/<see cref="short"/>/<see cref="ushort"/>/
 57/// <see cref="int"/>/<see cref="uint"/>/<see cref="long"/>/<see cref="ulong"/>),
 58/// <see cref="float"/>/<see cref="double"/>/<see cref="decimal"/>, <see cref="Guid"/>,
 59/// <see cref="DateTime"/>, <see cref="DateTimeOffset"/>, and <see cref="TimeSpan"/>. JSON has
 60/// no native kind for the temporal/GUID types — the model emits a string and the extractor
 61/// parses it (ISO 8601 for date-time / duration, GUID literal for <see cref="Guid"/>).
 62/// </para>
 63/// </remarks>
 64public static class AgentFrameworkArgumentExtractor
 65{
 66    /// <summary>
 67    /// Returns <see langword="true"/> when <paramref name="raw"/> represents a value the
 68    /// extractor methods are willing to decode. Returns <see langword="false"/> for
 69    /// <see langword="null"/> raws, <see cref="JsonValueKind.Null"/>, and
 70    /// <see cref="JsonValueKind.Undefined"/> — these mean the chat client did not supply the
 71    /// argument and the generator-emitted wrapper should fall back to the parameter's
 72    /// declared default value (or <see langword="null"/> for nullable parameters), or throw
 73    /// <see cref="ArgumentException"/> for required parameters.
 74    /// </summary>
 75    /// <param name="raw">The raw argument value as delivered by <c>AIFunctionArguments</c>.</param>
 76    /// <returns>
 77    /// <see langword="false"/> when the argument is missing/null/undefined; <see langword="true"/>
 78    /// otherwise (including for typed values, JSON strings, numbers, booleans, arrays, and objects).
 79    /// </returns>
 80    /// <remarks>
 81    /// This is the documented gate for the extractor methods on this class. Source-generated
 82    /// agent-function wrappers call this once per parameter site to decide whether to invoke the
 83    /// kind-tolerant extractor or to short-circuit to a fallback. The check is intentionally
 84    /// inexpensive — a single null check plus, for <see cref="JsonElement"/>, two
 85    /// <see cref="JsonValueKind"/> comparisons.
 86    /// </remarks>
 87    public static bool IsArgumentSupplied(object? raw)
 88    {
 5789        if (raw is null)
 90        {
 1791            return false;
 92        }
 93
 4094        if (raw is JsonElement je &&
 4095            (je.ValueKind == JsonValueKind.Null || je.ValueKind == JsonValueKind.Undefined))
 96        {
 897            return false;
 98        }
 99
 32100        return true;
 101    }
 102
 103    /// <summary>
 104    /// Extracts a <see cref="string"/> argument from a raw <see cref="object"/> delivered
 105    /// by <c>AIFunctionArguments</c>.
 106    /// </summary>
 107    /// <param name="raw">The raw argument value.</param>
 108    /// <returns>
 109    /// <see cref="JsonElement"/> with <see cref="JsonValueKind.String"/> →
 110    /// <see cref="JsonElement.GetString"/> (or empty string if the JSON string is null);<br/>
 111    /// <see cref="JsonValueKind.Array"/>, <see cref="JsonValueKind.Object"/>,
 112    /// <see cref="JsonValueKind.Number"/>, <see cref="JsonValueKind.True"/>,
 113    /// <see cref="JsonValueKind.False"/> → <see cref="JsonElement.GetRawText"/>;<br/>
 114    /// already-typed <see cref="string"/> → as-is;<br/>
 115    /// any other object → <see cref="object.ToString"/> (or empty string).
 116    /// </returns>
 117    /// <exception cref="InvalidOperationException">
 118    /// Thrown when <paramref name="raw"/> is <see langword="null"/>, <see cref="JsonValueKind.Null"/>,
 119    /// or <see cref="JsonValueKind.Undefined"/>. Callers must gate with
 120    /// <see cref="IsArgumentSupplied(object?)"/> first.
 121    /// </exception>
 122    public static string GetStringArgument(object? raw)
 123    {
 28124        if (raw is null)
 125        {
 1126            throw new InvalidOperationException(
 1127                "Cannot extract string argument from null.");
 128        }
 129
 27130        if (raw is JsonElement je)
 131        {
 24132            return je.ValueKind switch
 24133            {
 12134                JsonValueKind.String => je.GetString() ?? string.Empty,
 24135                JsonValueKind.Null or JsonValueKind.Undefined =>
 2136                    throw new InvalidOperationException(
 2137                        $"Cannot extract string argument: unexpected JsonValueKind {je.ValueKind}."),
 10138                _ => je.GetRawText(),
 24139            };
 140        }
 141
 3142        if (raw is string s)
 143        {
 2144            return s;
 145        }
 146
 1147        return raw.ToString() ?? string.Empty;
 148    }
 149
 150    /// <summary>
 151    /// Extracts a <see cref="bool"/> argument with strict kind semantics.
 152    /// </summary>
 153    /// <param name="raw">The raw argument value.</param>
 154    /// <returns>
 155    /// <see cref="JsonValueKind.True"/>/<see cref="JsonValueKind.False"/> →
 156    /// <see cref="JsonElement.GetBoolean"/>;<br/>
 157    /// <see cref="JsonValueKind.String"/> → <see cref="bool.TryParse(string?, out bool)"/>;<br/>
 158    /// already-typed <see cref="bool"/> → as-is.
 159    /// </returns>
 160    /// <exception cref="InvalidOperationException">
 161    /// Thrown for <see cref="JsonValueKind.Number"/>, <see cref="JsonValueKind.Array"/>,
 162    /// <see cref="JsonValueKind.Object"/>, <see cref="JsonValueKind.Null"/>,
 163    /// <see cref="JsonValueKind.Undefined"/>, or unparseable strings such as <c>"1"</c> or
 164    /// <c>"yes"</c>. This is intentional — silently coercing <c>0</c>/<c>1</c> or
 165    /// <c>"yes"</c>/<c>"no"</c> would mask schema violations from the model.
 166    /// </exception>
 167    public static bool GetBooleanArgument(object? raw)
 168    {
 17169        if (raw is bool b)
 170        {
 2171            return b;
 172        }
 173
 15174        if (raw is JsonElement je)
 175        {
 14176            switch (je.ValueKind)
 177            {
 178                case JsonValueKind.True:
 179                case JsonValueKind.False:
 4180                    return je.GetBoolean();
 181                case JsonValueKind.String:
 6182                    var s = je.GetString();
 6183                    if (bool.TryParse(s, out var parsed))
 184                    {
 4185                        return parsed;
 186                    }
 2187                    throw new InvalidOperationException(
 2188                        $"Cannot extract bool argument: JSON String '{s}' is not 'true' or 'false'.");
 189                default:
 4190                    throw new InvalidOperationException(
 4191                        $"Cannot extract bool argument: unexpected JsonValueKind {je.ValueKind}.");
 192            }
 193        }
 194
 1195        throw new InvalidOperationException(
 1196            $"Cannot extract bool argument from {raw?.GetType().FullName ?? "null"}.");
 197    }
 198
 199    /// <summary>Extracts an <see cref="int"/> argument.</summary>
 200    /// <param name="raw">The raw argument value.</param>
 201    public static int GetInt32Argument(object? raw)
 17202        => ExtractInteger<int>(
 17203            raw,
 10204            tryNumber: (JsonElement je, out int v) => je.TryGetInt32(out v),
 21205            tryParseInvariant: s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v :
 206
 207    /// <summary>Extracts a <see cref="long"/> argument.</summary>
 208    /// <param name="raw">The raw argument value.</param>
 209    public static long GetInt64Argument(object? raw)
 1210        => ExtractInteger<long>(
 1211            raw,
 1212            tryNumber: (JsonElement je, out long v) => je.TryGetInt64(out v),
 1213            tryParseInvariant: s => long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v 
 214
 215    /// <summary>Extracts a <see cref="short"/> argument.</summary>
 216    /// <param name="raw">The raw argument value.</param>
 217    public static short GetInt16Argument(object? raw)
 1218        => ExtractInteger<short>(
 1219            raw,
 1220            tryNumber: (JsonElement je, out short v) => je.TryGetInt16(out v),
 1221            tryParseInvariant: s => short.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v
 222
 223    /// <summary>Extracts an <see cref="sbyte"/> argument.</summary>
 224    /// <param name="raw">The raw argument value.</param>
 225    public static sbyte GetSByteArgument(object? raw)
 0226        => ExtractInteger<sbyte>(
 0227            raw,
 0228            tryNumber: (JsonElement je, out sbyte v) => je.TryGetSByte(out v),
 0229            tryParseInvariant: s => sbyte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v
 230
 231    /// <summary>Extracts a <see cref="byte"/> argument.</summary>
 232    /// <param name="raw">The raw argument value.</param>
 233    public static byte GetByteArgument(object? raw)
 1234        => ExtractInteger<byte>(
 1235            raw,
 1236            tryNumber: (JsonElement je, out byte v) => je.TryGetByte(out v),
 1237            tryParseInvariant: s => byte.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v 
 238
 239    /// <summary>Extracts a <see cref="ushort"/> argument.</summary>
 240    /// <param name="raw">The raw argument value.</param>
 241    public static ushort GetUInt16Argument(object? raw)
 0242        => ExtractInteger<ushort>(
 0243            raw,
 0244            tryNumber: (JsonElement je, out ushort v) => je.TryGetUInt16(out v),
 0245            tryParseInvariant: s => ushort.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? 
 246
 247    /// <summary>Extracts a <see cref="uint"/> argument.</summary>
 248    /// <param name="raw">The raw argument value.</param>
 249    public static uint GetUInt32Argument(object? raw)
 1250        => ExtractInteger<uint>(
 1251            raw,
 1252            tryNumber: (JsonElement je, out uint v) => je.TryGetUInt32(out v),
 1253            tryParseInvariant: s => uint.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v 
 254
 255    /// <summary>Extracts a <see cref="ulong"/> argument.</summary>
 256    /// <param name="raw">The raw argument value.</param>
 257    public static ulong GetUInt64Argument(object? raw)
 0258        => ExtractInteger<ulong>(
 0259            raw,
 0260            tryNumber: (JsonElement je, out ulong v) => je.TryGetUInt64(out v),
 0261            tryParseInvariant: s => ulong.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var v) ? v
 262
 263    /// <summary>Extracts a <see cref="float"/> argument.</summary>
 264    /// <param name="raw">The raw argument value.</param>
 265    /// <remarks>
 266    /// Rejects non-finite values (<see cref="float.NaN"/>, <see cref="float.PositiveInfinity"/>,
 267    /// <see cref="float.NegativeInfinity"/>) — JSON itself does not represent them, so receiving
 268    /// one indicates schema violation.
 269    /// </remarks>
 270    public static float GetSingleArgument(object? raw)
 4271        => ExtractFloat<float>(
 4272            raw,
 1273            tryNumber: (JsonElement je, out float v) => je.TryGetSingle(out v),
 1274            tryParseInvariant: s => float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v :
 4275            isFinite: float.IsFinite);
 276
 277    /// <summary>Extracts a <see cref="double"/> argument with precision-first decoding.</summary>
 278    /// <param name="raw">The raw argument value.</param>
 279    /// <remarks>
 280    /// Uses <see cref="JsonElement.TryGetDouble"/> on <see cref="JsonValueKind.Number"/> for
 281    /// IEEE-754 fidelity. Rejects non-finite values per JSON spec.
 282    /// </remarks>
 283    public static double GetDoubleArgument(object? raw)
 2284        => ExtractFloat<double>(
 2285            raw,
 1286            tryNumber: (JsonElement je, out double v) => je.TryGetDouble(out v),
 0287            tryParseInvariant: s => double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var v) ? v 
 2288            isFinite: double.IsFinite);
 289
 290    /// <summary>Extracts a <see cref="decimal"/> argument with precision-preserving decoding.</summary>
 291    /// <param name="raw">The raw argument value.</param>
 292    /// <remarks>
 293    /// Uses <see cref="JsonElement.TryGetDecimal"/> on <see cref="JsonValueKind.Number"/> first
 294    /// to preserve precision that would be lost via a <see cref="double"/> round-trip. Falls
 295    /// back to invariant-culture parsing for <see cref="JsonValueKind.String"/>.
 296    /// </remarks>
 297    public static decimal GetDecimalArgument(object? raw)
 298    {
 5299        if (raw is decimal d)
 300        {
 1301            return d;
 302        }
 303
 4304        if (raw is JsonElement je)
 305        {
 4306            switch (je.ValueKind)
 307            {
 308                case JsonValueKind.Number:
 2309                    if (je.TryGetDecimal(out var v))
 310                    {
 2311                        return v;
 312                    }
 0313                    throw new InvalidOperationException(
 0314                        $"Cannot extract decimal argument: JSON Number '{je.GetRawText()}' is not representable as decim
 315                case JsonValueKind.String:
 2316                    var s = je.GetString();
 2317                    if (decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out var parsed))
 318                    {
 1319                        return parsed;
 320                    }
 1321                    throw new InvalidOperationException(
 1322                        $"Cannot extract decimal argument: JSON String '{s}' is not a numeric literal.");
 323                default:
 0324                    throw new InvalidOperationException(
 0325                        $"Cannot extract decimal argument: unexpected JsonValueKind {je.ValueKind}.");
 326            }
 327        }
 328
 0329        throw new InvalidOperationException(
 0330            $"Cannot extract decimal argument from {raw?.GetType().FullName ?? "null"}.");
 331    }
 332
 333    /// <summary>Extracts a <see cref="Guid"/> argument.</summary>
 334    /// <param name="raw">The raw argument value.</param>
 335    /// <remarks>
 336    /// JSON has no native GUID kind — the model emits a string. Uses
 337    /// <see cref="JsonElement.TryGetGuid"/> on <see cref="JsonValueKind.String"/>; throws on
 338    /// any other kind to surface schema violations rather than silently substituting
 339    /// <see cref="Guid.Empty"/>.
 340    /// </remarks>
 341    public static Guid GetGuidArgument(object? raw)
 342    {
 11343        if (raw is Guid g)
 344        {
 1345            return g;
 346        }
 347
 10348        if (raw is JsonElement je)
 349        {
 9350            switch (je.ValueKind)
 351            {
 352                case JsonValueKind.String:
 7353                    if (je.TryGetGuid(out var v))
 354                    {
 5355                        return v;
 356                    }
 2357                    throw new InvalidOperationException(
 2358                        $"Cannot extract Guid argument: JSON String '{je.GetString()}' is not a valid GUID.");
 359                default:
 2360                    throw new InvalidOperationException(
 2361                        $"Cannot extract Guid argument: unexpected JsonValueKind {je.ValueKind}.");
 362            }
 363        }
 364
 1365        throw new InvalidOperationException(
 1366            $"Cannot extract Guid argument from {raw?.GetType().FullName ?? "null"}.");
 367    }
 368
 369    /// <summary>Extracts a <see cref="DateTime"/> argument from an ISO 8601 string.</summary>
 370    /// <param name="raw">The raw argument value.</param>
 371    /// <remarks>
 372    /// JSON has no native date-time kind — the model emits a string. Uses
 373    /// <see cref="JsonElement.TryGetDateTime"/> which accepts the JSON-Schema-spec
 374    /// <c>"date-time"</c> format (ISO 8601 with optional offset).
 375    /// </remarks>
 376    public static DateTime GetDateTimeArgument(object? raw)
 377    {
 7378        if (raw is DateTime dt)
 379        {
 1380            return dt;
 381        }
 382
 6383        if (raw is JsonElement je)
 384        {
 6385            switch (je.ValueKind)
 386            {
 387                case JsonValueKind.String:
 5388                    if (je.TryGetDateTime(out var v))
 389                    {
 4390                        return v;
 391                    }
 1392                    throw new InvalidOperationException(
 1393                        $"Cannot extract DateTime argument: JSON String '{je.GetString()}' is not a valid ISO 8601 date-
 394                default:
 1395                    throw new InvalidOperationException(
 1396                        $"Cannot extract DateTime argument: unexpected JsonValueKind {je.ValueKind}.");
 397            }
 398        }
 399
 0400        throw new InvalidOperationException(
 0401            $"Cannot extract DateTime argument from {raw?.GetType().FullName ?? "null"}.");
 402    }
 403
 404    /// <summary>Extracts a <see cref="DateTimeOffset"/> argument from an ISO 8601 string.</summary>
 405    /// <param name="raw">The raw argument value.</param>
 406    /// <remarks>
 407    /// JSON has no native date-time kind — the model emits a string with optional offset.
 408    /// Uses <see cref="JsonElement.TryGetDateTimeOffset"/>.
 409    /// </remarks>
 410    public static DateTimeOffset GetDateTimeOffsetArgument(object? raw)
 411    {
 4412        if (raw is DateTimeOffset dto)
 413        {
 1414            return dto;
 415        }
 416
 3417        if (raw is JsonElement je)
 418        {
 3419            switch (je.ValueKind)
 420            {
 421                case JsonValueKind.String:
 3422                    if (je.TryGetDateTimeOffset(out var v))
 423                    {
 2424                        return v;
 425                    }
 1426                    throw new InvalidOperationException(
 1427                        $"Cannot extract DateTimeOffset argument: JSON String '{je.GetString()}' is not a valid ISO 8601
 428                default:
 0429                    throw new InvalidOperationException(
 0430                        $"Cannot extract DateTimeOffset argument: unexpected JsonValueKind {je.ValueKind}.");
 431            }
 432        }
 433
 0434        throw new InvalidOperationException(
 0435            $"Cannot extract DateTimeOffset argument from {raw?.GetType().FullName ?? "null"}.");
 436    }
 437
 438    /// <summary>Extracts a <see cref="TimeSpan"/> argument from a string.</summary>
 439    /// <param name="raw">The raw argument value.</param>
 440    /// <remarks>
 441    /// <para>
 442    /// JSON has no native duration kind. This helper accepts both common LLM outputs:
 443    /// </para>
 444    /// <list type="bullet">
 445    /// <item>
 446    /// <description>
 447    /// .NET round-trip format like <c>"01:30:00"</c> or <c>"1.02:03:04"</c> via
 448    /// <see cref="TimeSpan.TryParse(string?, IFormatProvider?, out TimeSpan)"/> with invariant culture.
 449    /// </description>
 450    /// </item>
 451    /// <item>
 452    /// <description>
 453    /// JSON-Schema-spec ISO 8601 duration like <c>"PT1H30M"</c> via
 454    /// <see cref="System.Xml.XmlConvert.ToTimeSpan"/>.
 455    /// </description>
 456    /// </item>
 457    /// </list>
 458    /// </remarks>
 459    public static TimeSpan GetTimeSpanArgument(object? raw)
 460    {
 11461        if (raw is TimeSpan ts)
 462        {
 1463            return ts;
 464        }
 465
 10466        if (raw is JsonElement je)
 467        {
 10468            switch (je.ValueKind)
 469            {
 470                case JsonValueKind.String:
 8471                    var s = je.GetString();
 8472                    if (TryParseTimeSpan(s, out var v))
 473                    {
 6474                        return v;
 475                    }
 2476                    throw new InvalidOperationException(
 2477                        $"Cannot extract TimeSpan argument: JSON String '{s}' is neither a .NET duration ('hh:mm:ss') no
 478                default:
 2479                    throw new InvalidOperationException(
 2480                        $"Cannot extract TimeSpan argument: unexpected JsonValueKind {je.ValueKind}.");
 481            }
 482        }
 483
 0484        throw new InvalidOperationException(
 0485            $"Cannot extract TimeSpan argument from {raw?.GetType().FullName ?? "null"}.");
 486    }
 487
 488    private static bool TryParseTimeSpan(string? s, out TimeSpan result)
 489    {
 8490        if (string.IsNullOrWhiteSpace(s))
 491        {
 0492            result = default;
 0493            return false;
 494        }
 495
 8496        if (TimeSpan.TryParse(s, CultureInfo.InvariantCulture, out result))
 497        {
 2498            return true;
 499        }
 500
 501        try
 502        {
 6503            result = System.Xml.XmlConvert.ToTimeSpan(s!);
 4504            return true;
 505        }
 2506        catch (FormatException)
 507        {
 2508            result = default;
 2509            return false;
 510        }
 6511    }
 512
 513    private delegate bool TryNumber<T>(JsonElement je, out T value);
 514
 515    private static T ExtractInteger<T>(
 516        object? raw,
 517        TryNumber<T> tryNumber,
 518        Func<string?, T?> tryParseInvariant)
 519        where T : struct
 520    {
 21521        if (raw is T typed)
 522        {
 1523            return typed;
 524        }
 525
 20526        if (raw is JsonElement je)
 527        {
 19528            switch (je.ValueKind)
 529            {
 530                case JsonValueKind.Number:
 14531                    if (tryNumber(je, out var n))
 532                    {
 12533                        return n;
 534                    }
 2535                    throw new InvalidOperationException(
 2536                        $"Cannot extract {typeof(T).Name} argument: JSON Number '{je.GetRawText()}' " +
 2537                        $"is out of range or has a fractional component.");
 538                case JsonValueKind.String:
 4539                    var s = je.GetString();
 4540                    var parsed = tryParseInvariant(s);
 4541                    if (parsed.HasValue)
 542                    {
 3543                        return parsed.Value;
 544                    }
 1545                    throw new InvalidOperationException(
 1546                        $"Cannot extract {typeof(T).Name} argument: JSON String '{s}' is not a numeric literal.");
 547                default:
 1548                    throw new InvalidOperationException(
 1549                        $"Cannot extract {typeof(T).Name} argument: unexpected JsonValueKind {je.ValueKind}.");
 550            }
 551        }
 552
 1553        throw new InvalidOperationException(
 1554            $"Cannot extract {typeof(T).Name} argument from {raw?.GetType().FullName ?? "null"}.");
 555    }
 556
 557    private static T ExtractFloat<T>(
 558        object? raw,
 559        TryNumber<T> tryNumber,
 560        Func<string?, T?> tryParseInvariant,
 561        Func<T, bool> isFinite)
 562        where T : struct
 563    {
 6564        if (raw is T typed)
 565        {
 3566            if (!isFinite(typed))
 567            {
 3568                throw new InvalidOperationException(
 3569                    $"Cannot extract {typeof(T).Name} argument: value is not finite (NaN/Infinity).");
 570            }
 0571            return typed;
 572        }
 573
 3574        if (raw is JsonElement je)
 575        {
 3576            switch (je.ValueKind)
 577            {
 578                case JsonValueKind.Number:
 2579                    if (tryNumber(je, out var n))
 580                    {
 2581                        if (!isFinite(n))
 582                        {
 0583                            throw new InvalidOperationException(
 0584                                $"Cannot extract {typeof(T).Name} argument: " +
 0585                                "JSON Number is not finite (NaN/Infinity).");
 586                        }
 2587                        return n;
 588                    }
 0589                    throw new InvalidOperationException(
 0590                        $"Cannot extract {typeof(T).Name} argument: JSON Number '{je.GetRawText()}' " +
 0591                        $"is not representable as {typeof(T).Name}.");
 592                case JsonValueKind.String:
 1593                    var s = je.GetString();
 1594                    var parsed = tryParseInvariant(s);
 1595                    if (parsed.HasValue)
 596                    {
 1597                        if (!isFinite(parsed.Value))
 598                        {
 0599                            throw new InvalidOperationException(
 0600                                $"Cannot extract {typeof(T).Name} argument: " +
 0601                                "parsed value is not finite (NaN/Infinity).");
 602                        }
 1603                        return parsed.Value;
 604                    }
 0605                    throw new InvalidOperationException(
 0606                        $"Cannot extract {typeof(T).Name} argument: JSON String '{s}' is not a numeric literal.");
 607                default:
 0608                    throw new InvalidOperationException(
 0609                        $"Cannot extract {typeof(T).Name} argument: unexpected JsonValueKind {je.ValueKind}.");
 610            }
 611        }
 612
 0613        throw new InvalidOperationException(
 0614            $"Cannot extract {typeof(T).Name} argument from {raw?.GetType().FullName ?? "null"}.");
 615    }
 616}