< Summary

Information
Class: NGql.Core.ValueFormatter
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/ValueFormatter.cs
Line coverage
100%
Covered lines: 90
Uncovered lines: 0
Coverable lines: 90
Total lines: 252
Line coverage: 100%
Branch coverage
100%
Covered branches: 129
Total branches: 129
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
IsPrimitiveType(...)100%1212100%
IsString(...)100%11100%
IsBoolean(...)100%11100%
IsInteger(...)100%22100%
IsSignedInteger(...)100%88100%
IsUnsignedInteger(...)100%88100%
IsFloating(...)100%66100%
IsDate(...)100%44100%
IsEnumLike(...)100%44100%
TryAppendPrimitive(...)100%66100%
TryAppendScalar(...)100%88100%
TryAppendNamed(...)100%66100%
FormatEnumName(...)100%22100%
AppendString(...)100%22100%
NeedsEscape(...)100%88100%
AppendEscapedBody(...)100%1515100%
AppendBoolean(...)100%22100%
TryAppendInteger(...)100%22100%
TryAppendSignedInteger(...)100%88100%
TryAppendUnsignedInteger(...)100%88100%
TryAppendFloating(...)100%66100%
GuardFiniteDouble(...)100%44100%
GuardFiniteFloat(...)100%44100%
AppendFormattable(...)100%11100%
AppendSpanFormatted(...)100%22100%
AppendFormattedFallback(...)100%22100%
AppendQuotedFormattable(...)100%11100%

File(s)

/home/runner/work/NGql/NGql/src/Core/ValueFormatter.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Text;
 3
 4namespace NGql.Core;
 5
 6internal static class ValueFormatter
 7{
 8    internal const string DateFormat = "yyyy-MM-dd'T'HH:mm:ss.fffK";
 9
 10    // Pre-allocated strings for common boolean values to avoid repeated allocations
 11    private const string TrueString = "true";
 12    private const string FalseString = "false";
 13
 14    /// <summary>
 15    /// Returns true when <paramref name="value"/> is a primitive type this formatter handles.
 16    /// Single is-test per category keeps cyclomatic complexity flat.
 17    /// </summary>
 18    internal static bool IsPrimitiveType(object value)
 10392319        => IsString(value) || IsBoolean(value) || IsInteger(value) || IsFloating(value)
 10392320        || IsDate(value) || IsEnumLike(value) || value is Variable;
 21
 10392322    private static bool IsString(object v) => v is string;
 10092323    private static bool IsBoolean(object v) => v is bool;
 9957324    private static bool IsInteger(object v) => IsSignedInteger(v) || IsUnsignedInteger(v);
 9957325    private static bool IsSignedInteger(object v) => v is sbyte or short or int or long;
 849326    private static bool IsUnsignedInteger(object v) => v is byte or ushort or uint or ulong;
 846927    private static bool IsFloating(object v) => v is float or double or decimal;
 834028    private static bool IsDate(object v) => v is DateTime or DateTimeOffset;
 831029    private static bool IsEnumLike(object v) => v is EnumValue or Enum;
 30
 31    /// <summary>
 32    /// Writes the primitive representation of <paramref name="value"/> directly to
 33    /// <paramref name="builder"/> without allocating an intermediate string.
 34    /// Each value category dispatches to a small dedicated helper to keep this top-level
 35    /// switch's cyclomatic complexity low.
 36    /// </summary>
 37    /// <returns>True if the value was handled; false if it is not a known primitive.</returns>
 38    internal static bool TryAppendPrimitive(object value, StringBuilder builder)
 2624339        => TryAppendScalar(value, builder)
 2624340        || TryAppendNamed(value, builder)
 2624341        || TryAppendInteger(value, builder)
 2624342        || TryAppendFloating(value, builder);
 43
 44    private static bool TryAppendScalar(object value, StringBuilder builder)
 45    {
 46        switch (value)
 47        {
 675048            case string s: AppendString(builder, s); return true;
 137449            case bool b: AppendBoolean(builder, b); return true;
 4250            case DateTime dt: AppendQuotedFormattable(builder, dt); return true;
 2451            case DateTimeOffset dto: AppendQuotedFormattable(builder, dto); return true;
 2214852            default: return false;
 53        }
 54    }
 55
 56    private static bool TryAppendNamed(object value, StringBuilder builder)
 57    {
 58        switch (value)
 59        {
 4260            case Enum e: builder.Append(FormatEnumName(e)); return true;
 21061            case EnumValue ev: builder.Append(ev.Value); return true;
 66662            case Variable variable: builder.Append(variable.Name); return true;
 2168663            default: return false;
 64        }
 65    }
 66
 67    /// <summary>
 68    /// Renders an enum as its single GraphQL <c>Name</c>. Per spec § 3.9 an EnumValue must be a
 69    /// Name, so undefined numeric values and unnamed <c>[Flags]</c> combinations (whose
 70    /// <c>ToString()</c> yields <c>"999"</c> or <c>"Read, Write"</c>) are rejected rather than
 71    /// emitted as invalid output.
 72    /// </summary>
 73    internal static string FormatEnumName(Enum value)
 74    {
 5475        if (!Enum.IsDefined(value.GetType(), value))
 76        {
 1277            throw new ArgumentException(
 1278                $"GraphQL enum arguments must be a single defined enum member. " +
 1279                $"The value '{value}' of type '{value.GetType().Name}' is not a defined member " +
 1280                $"(undefined numeric values and unnamed [Flags] combinations are not valid GraphQL enum names).",
 1281                nameof(value));
 82        }
 4283        return value.ToString();
 84    }
 85
 86    private static void AppendString(StringBuilder builder, string s)
 87    {
 337588        builder.Append('"');
 337589        if (NeedsEscape(s))
 90        {
 3391            AppendEscapedBody(builder, s);
 92        }
 93        else
 94        {
 334295            builder.Append(s);
 96        }
 337597        builder.Append('"');
 337598    }
 99
 100    /// <summary>
 101    /// Per GraphQL spec § 2.9.4 a regular string literal must escape <c>\</c>, <c>"</c>, and
 102    /// the C0 control characters. Non-control Unicode characters pass through verbatim — the
 103    /// transport carries UTF-8.
 104    /// </summary>
 105    private static bool NeedsEscape(string s)
 106    {
 176514107        for (var i = 0; i < s.Length; i++)
 108        {
 84915109            var c = s[i];
 84915110            if (c == '"' || c == '\\' || c < 0x20)
 33111                return true;
 112        }
 3342113        return false;
 114    }
 115
 116    private static void AppendEscapedBody(StringBuilder builder, string s)
 117    {
 900118        for (var i = 0; i < s.Length; i++)
 119        {
 417120            var c = s[i];
 121            switch (c)
 122            {
 48123                case '"':  builder.Append("\\\""); break;
 12124                case '\\': builder.Append("\\\\"); break;
 6125                case '\b': builder.Append("\\b");  break;
 6126                case '\f': builder.Append("\\f");  break;
 6127                case '\n': builder.Append("\\n");  break;
 6128                case '\r': builder.Append("\\r");  break;
 6129                case '\t': builder.Append("\\t");  break;
 130                default:
 372131                    if (c < 0x20)
 132                    {
 133                        // Other C0 controls — emit as \u00XX (GraphQL accepts EscapedUnicode).
 3134                        builder.Append("\\u");
 3135                        builder.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
 136                    }
 137                    else
 138                    {
 369139                        builder.Append(c);
 140                    }
 141                    break;
 142            }
 143        }
 33144    }
 145
 146    private static void AppendBoolean(StringBuilder builder, bool b)
 687147        => builder.Append(b ? TrueString : FalseString);
 148
 149    private static bool TryAppendInteger(object value, StringBuilder builder)
 21686150        => TryAppendSignedInteger(value, builder) || TryAppendUnsignedInteger(value, builder);
 151
 152    private static bool TryAppendSignedInteger(object value, StringBuilder builder)
 153    {
 154        switch (value)
 155        {
 35770156            case int v: AppendFormattable(builder, v); return true;
 4854157            case long v: AppendFormattable(builder, v); return true;
 36158            case short v: AppendFormattable(builder, (int)v); return true;
 36159            case sbyte v: AppendFormattable(builder, (int)v); return true;
 1338160            default: return false;
 161        }
 162    }
 163
 164    private static bool TryAppendUnsignedInteger(object value, StringBuilder builder)
 165    {
 166        switch (value)
 167        {
 24168            case uint v: builder.Append(v); return true;
 24169            case ulong v: builder.Append(v); return true;
 24170            case ushort v: builder.Append(v); return true;
 24171            case byte v: builder.Append(v); return true;
 1290172            default: return false;
 173        }
 174    }
 175
 176    private static bool TryAppendFloating(object value, StringBuilder builder)
 177    {
 178        switch (value)
 179        {
 90180            case float v: GuardFiniteFloat(v); AppendFormattable(builder, v); return true;
 162181            case double v: GuardFiniteDouble(v); AppendFormattable(builder, v); return true;
 66182            case decimal v: AppendFormattable(builder, v); return true;
 1161183            default: return false;
 184        }
 185    }
 186
 187    /// <summary>
 188    /// GraphQL FloatValue (spec § 3.5.2) has no representation for NaN or Infinity. The BCL would
 189    /// otherwise emit the bare tokens <c>NaN</c> / <c>Infinity</c> / <c>-Infinity</c>, which a
 190    /// conforming server rejects, so such values are refused at build time instead.
 191    /// </summary>
 192    private static void GuardFiniteDouble(double v)
 193    {
 60194        if (double.IsNaN(v) || double.IsInfinity(v))
 9195            throw new FormatException($"GraphQL does not support NaN/Infinity float values (got '{v.ToString(CultureInfo
 51196    }
 197
 198    private static void GuardFiniteFloat(float v)
 199    {
 36200        if (float.IsNaN(v) || float.IsInfinity(v))
 9201            throw new FormatException($"GraphQL does not support NaN/Infinity float values (got '{v.ToString(CultureInfo
 27202    }
 203
 204    /// <summary>Formats <paramref name="value"/> with invariant culture. Used for
 205    /// int/long/float/double/decimal — none of those produce strings longer than the BCL guarantees.</summary>
 206    private static void AppendFormattable<T>(StringBuilder builder, T value, ReadOnlySpan<char> format = default)
 207        where T : struct, ISpanFormattable
 208    {
 209        // Enough for the supported numeric types and DateFormat. The fallback inside
 210        // AppendSpanFormatted keeps this helper safe if another format or value type is
 211        // introduced later.
 20492212        Span<char> buffer = stackalloc char[StackBufferLength];
 20492213        AppendSpanFormatted(builder, value, format, buffer);
 20492214    }
 215
 216    /// <summary>Stack buffer size used by <see cref="AppendFormattable{T}"/>; comfortably above the
 217    /// longest output of any currently-supported value type and format.</summary>
 218    internal const int StackBufferLength = 64;
 219
 220    /// <summary>
 221    /// Formats <paramref name="value"/> into <paramref name="buffer"/> and appends the result,
 222    /// falling back to an allocating <c>ToString</c> when the buffer is too small. Split out from
 223    /// <see cref="AppendFormattable{T}"/> so the undersized-buffer fallback is exercisable without
 224    /// changing the production buffer size.
 225    /// </summary>
 226    internal static void AppendSpanFormatted<T>(StringBuilder builder, T value, ReadOnlySpan<char> format, Span<char> bu
 227        where T : struct, ISpanFormattable
 228    {
 20540229        if (value.TryFormat(buffer, out var written, format, CultureInfo.InvariantCulture))
 230        {
 20516231            builder.Append(buffer[..written]);
 20516232            return;
 233        }
 234
 24235        AppendFormattedFallback(builder, value, format);
 24236    }
 237
 238    /// <summary>Allocating path taken only when the destination span cannot hold the formatted
 239    /// value. Non-generic so it is compiled once rather than per value type.</summary>
 240    private static void AppendFormattedFallback(StringBuilder builder, IFormattable value, ReadOnlySpan<char> format)
 24241        => builder.Append(value.ToString(format.IsEmpty ? null : format.ToString(), CultureInfo.InvariantCulture));
 242
 243    /// <summary>Formats <paramref name="value"/> with the NGql DateFormat and quotes it.
 244    /// Used for DateTime/DateTimeOffset.</summary>
 245    private static void AppendQuotedFormattable<T>(StringBuilder builder, T value)
 246        where T : struct, ISpanFormattable
 247    {
 33248        builder.Append('"');
 33249        AppendFormattable(builder, value, DateFormat);
 33250        builder.Append('"');
 33251    }
 252}