< Summary

Information
Class: NGql.Core.Abstractions.FieldDirective
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Abstractions/FieldDirective.cs
Line coverage
100%
Covered lines: 26
Uncovered lines: 0
Coverable lines: 26
Total lines: 111
Line coverage: 100%
Branch coverage
100%
Covered branches: 26
Total branches: 26
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%22100%
NormalizeArguments(...)100%66100%
get_Name()100%11100%
get_Arguments()100%11100%
IsStructurallyEqualTo(...)100%1818100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Abstractions/FieldDirective.cs

#LineLine coverage
 1using System.Diagnostics.CodeAnalysis;
 2using System.Text.Json.Serialization;
 3using NGql.Core.Extensions;
 4
 5namespace NGql.Core.Abstractions;
 6
 7/// <summary>
 8///     Represents a GraphQL directive attached to a field — rendered as
 9///     <c>@Name</c> optionally followed by <c>(argName:value, …)</c> after the field's name and
 10///     arguments and before its selection set. Examples: <c>@include(if:$show)</c>,
 11///     <c>@skip(if:$hide)</c>, <c>@deprecated</c>.
 12/// </summary>
 13/// <remarks>
 14///     Directives are immutable value objects. A no-argument directive (<see cref="Arguments"/>
 15///     is <c>null</c>) carries no dictionary, so the common <c>@deprecated</c>-style case allocates
 16///     nothing beyond the record itself. Argument values reuse the same formatting as field
 17///     arguments, so <c>if:$x</c> and complex values render identically.
 18/// </remarks>
 19[SuppressMessage("Minor Code Smell", "S2292:Trivial properties should be auto-implemented")]
 20public sealed record FieldDirective
 21{
 22    /// <summary>
 23    /// Creates a directive with the given name and optional arguments.
 24    /// </summary>
 25    /// <param name="name">The directive name WITHOUT the leading <c>@</c> (e.g. <c>"include"</c>).
 26    /// Rendered with the <c>@</c> prefix.</param>
 27    /// <param name="arguments">Optional directive arguments; <c>null</c> for a no-argument directive.
 28    /// Argument keys are normalized through the same case-insensitive pipeline as field arguments:
 29    /// keys that collide under <see cref="StringComparer.OrdinalIgnoreCase"/> throw (rather than
 30    /// silently last-win), and the resulting dictionary is sorted for deterministic rendering.</param>
 31    /// <exception cref="ArgumentException">Thrown when <paramref name="name"/> is null or whitespace,
 32    /// or when <paramref name="arguments"/> contains keys colliding under OrdinalIgnoreCase.</exception>
 994233    public FieldDirective(string name, IReadOnlyDictionary<string, object?>? arguments = null)
 34    {
 994235        if (string.IsNullOrWhiteSpace(name))
 36        {
 937            throw new ArgumentException("Directive name cannot be null or whitespace.", nameof(name));
 38        }
 39
 993340        Name = name;
 993341        Arguments = NormalizeArguments(arguments);
 993042    }
 43
 44    // Route directive arguments through the same normalization field arguments use
 45    // (Helpers.SortArgumentValue over the dictionary): keys colliding under OrdinalIgnoreCase
 46    // fail fast instead of silently last-winning, nested values are normalized identically, and
 47    // the result is a sorted dictionary so rendering is deterministic — matching field-argument
 48    // behavior exactly.
 49    private static IReadOnlyDictionary<string, object?>? NormalizeArguments(IReadOnlyDictionary<string, object?>? argume
 50    {
 993351        if (arguments is not { Count: > 0 })
 52        {
 965153            return null;
 54        }
 55
 28256        var source = arguments as IDictionary<string, object?>
 28257            ?? new Dictionary<string, object?>(arguments);
 28258        return (IReadOnlyDictionary<string, object?>)Helpers.SortArgumentValue(source)!;
 59    }
 60
 61    /// <summary>
 62    /// The directive name WITHOUT the leading <c>@</c> (stored as <c>"include"</c>, rendered as
 63    /// <c>@include</c>).
 64    /// </summary>
 65    [JsonPropertyName("name")]
 2064966    public string Name { get; init; }
 67
 68    /// <summary>
 69    /// The directive's arguments, or <c>null</c> when the directive takes no arguments. Argument
 70    /// values render with the same formatter used for field arguments.
 71    /// </summary>
 72    [JsonPropertyName("arguments")]
 1039873    public IReadOnlyDictionary<string, object?>? Arguments { get; init; }
 74
 75    /// <summary>
 76    /// Structural equality for dedup on the merge path. The compiler-generated record
 77    /// <see cref="object.Equals(object?)"/> compares <see cref="Arguments"/> by REFERENCE (an
 78    /// <see cref="IReadOnlyDictionary{TKey, TValue}"/> has no value equality), so two independently
 79    /// built <c>@include(if:$x)</c> instances are never record-equal. This compares by
 80    /// <see cref="Name"/> plus argument entries (case-insensitive keys, value-equal values —
 81    /// <see cref="Variable"/> equality is value-based), which is what "the same directive" means
 82    /// for merge deduplication.
 83    /// </summary>
 84    internal bool IsStructurallyEqualTo(FieldDirective other)
 85    {
 8186        if (!string.Equals(Name, other.Name, StringComparison.Ordinal))
 87        {
 388            return false;
 89        }
 90
 7891        var a = Arguments;
 7892        var b = other.Arguments;
 7893        if (a is null || a.Count == 0)
 94        {
 995            return b is null || b.Count == 0;
 96        }
 6997        if (b is null || a.Count != b.Count)
 98        {
 699            return false;
 100        }
 101
 228102        foreach (var (key, value) in a)
 103        {
 63104            if (!b.TryGetValue(key, out var otherValue) || !Equals(value, otherValue))
 105            {
 24106                return false;
 107            }
 108        }
 39109        return true;
 24110    }
 111}