| | | 1 | | using System.Diagnostics.CodeAnalysis; |
| | | 2 | | |
| | | 3 | | namespace NGql.Core.Abstractions; |
| | | 4 | | |
| | | 5 | | /// <summary> |
| | | 6 | | /// Shared directive-list mutation logic for the two node kinds that carry a <c>List<FieldDirective></c> |
| | | 7 | | /// — <see cref="FieldDefinition"/> and <see cref="InlineFragmentDefinition"/>. Kept as a single |
| | | 8 | | /// static implementation (rather than duplicated per type) so the dedup/collapse rules — and any |
| | | 9 | | /// future fix to them — apply identically everywhere a directive can be attached. |
| | | 10 | | /// </summary> |
| | | 11 | | internal static class DirectiveListOps |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// Append <paramref name="directive"/> to <paramref name="directives"/>. Order is preserved. A |
| | | 15 | | /// directive that is structurally identical to one already present is skipped. Directives that |
| | | 16 | | /// differ (name or arguments) are all kept, EXCEPT for the two non-repeatable, spec-constrained |
| | | 17 | | /// directive names <c>include</c> and <c>skip</c> (GraphQL spec §5.7.3: a non-repeatable |
| | | 18 | | /// directive may appear at most once per location) — see <see cref="AddOrReplaceIfDirective"/>. |
| | | 19 | | /// </summary> |
| | | 20 | | [SuppressMessage( |
| | | 21 | | "Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method", |
| | | 22 | | Justification = "The plain loop short-circuits without allocating an enumerator on the builder hot path.")] |
| | | 23 | | public static void Add(ref List<FieldDirective>? directives, FieldDirective directive) |
| | | 24 | | { |
| | 9894 | 25 | | if (IsNonRepeatableConditional(directive.Name)) |
| | | 26 | | { |
| | 240 | 27 | | AddOrReplaceIfDirective(ref directives, directive); |
| | 240 | 28 | | return; |
| | | 29 | | } |
| | | 30 | | |
| | 9654 | 31 | | directives ??= new List<FieldDirective>(); |
| | 19311 | 32 | | foreach (var existing in directives) |
| | | 33 | | { |
| | 3 | 34 | | if (existing.IsStructurallyEqualTo(directive)) |
| | | 35 | | { |
| | 3 | 36 | | return; |
| | | 37 | | } |
| | | 38 | | } |
| | 9651 | 39 | | directives.Add(directive); |
| | 9654 | 40 | | } |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// <c>@include</c> and <c>@skip</c> are non-repeatable per the GraphQL spec (§5.7.3) — a field |
| | | 44 | | /// or inline fragment may carry at most one of each. Rather than appending a second occurrence |
| | | 45 | | /// (which would render spec-invalid GraphQL like <c>@include(if:$a) @include(if:$b)</c>), a |
| | | 46 | | /// later call with the SAME directive name replaces the earlier one IN PLACE (same list |
| | | 47 | | /// position), so ordering relative to any other directives is preserved. This is |
| | | 48 | | /// last-call-wins: calling <c>IncludeIf(a)</c> then <c>IncludeIf(b)</c> keeps only |
| | | 49 | | /// <c>@include(if:$b)</c>. A structurally identical replacement (same name AND same arguments) |
| | | 50 | | /// is a no-op, matching the existing dedup behavior for repeatable directives. |
| | | 51 | | /// </summary> |
| | | 52 | | [SuppressMessage( |
| | | 53 | | "Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method", |
| | | 54 | | Justification = "The plain loop finds the replacement index without allocating an enumerator.")] |
| | | 55 | | private static void AddOrReplaceIfDirective(ref List<FieldDirective>? directives, FieldDirective directive) |
| | | 56 | | { |
| | 240 | 57 | | directives ??= new List<FieldDirective>(); |
| | 504 | 58 | | for (var i = 0; i < directives.Count; i++) |
| | | 59 | | { |
| | 45 | 60 | | var existing = directives[i]; |
| | 45 | 61 | | if (!string.Equals(existing.Name, directive.Name, StringComparison.Ordinal)) |
| | | 62 | | { |
| | | 63 | | continue; |
| | | 64 | | } |
| | | 65 | | |
| | 33 | 66 | | if (existing.IsStructurallyEqualTo(directive)) |
| | | 67 | | { |
| | 21 | 68 | | return; |
| | | 69 | | } |
| | | 70 | | |
| | 12 | 71 | | directives[i] = directive; |
| | 12 | 72 | | return; |
| | | 73 | | } |
| | | 74 | | |
| | 207 | 75 | | directives.Add(directive); |
| | 207 | 76 | | } |
| | | 77 | | |
| | | 78 | | /// <summary> |
| | | 79 | | /// Whether <paramref name="name"/> is one of the two universal client directives constrained by |
| | | 80 | | /// the GraphQL spec to appear at most once per location. Checked by <see cref="Add"/> so |
| | | 81 | | /// <c>Directive("include", …)</c> called directly collapses exactly like |
| | | 82 | | /// <c>IncludeIf(Variable)</c> — there is no entry point that can silently emit two |
| | | 83 | | /// <c>@include</c>s on one field or fragment. |
| | | 84 | | /// </summary> |
| | | 85 | | private static bool IsNonRepeatableConditional(string name) |
| | 9894 | 86 | | => name.Equals("include", StringComparison.Ordinal) || name.Equals("skip", StringComparison.Ordinal); |
| | | 87 | | } |