| | | 1 | | using System.Diagnostics.CodeAnalysis; |
| | | 2 | | using System.Text.Json.Serialization; |
| | | 3 | | |
| | | 4 | | namespace NGql.Core.Abstractions; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// Represents a GraphQL inline fragment — a type-conditional selection set rendered as |
| | | 8 | | /// <c>... on TypeName { … }</c>. Used to narrow a parent field whose schema type is a |
| | | 9 | | /// union or interface to a specific concrete type. |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// Inline fragments are siblings of regular fields under their parent <see cref="FieldDefinition"/> |
| | | 13 | | /// (see <see cref="FieldDefinition.InlineFragments"/>). They are kept in a separate collection |
| | | 14 | | /// because their access pattern (lookup by type name) and rendering shape (the <c>... on</c> |
| | | 15 | | /// prefix) differ from regular fields. Nested inline fragments — fragments inside fragments — |
| | | 16 | | /// are supported recursively. |
| | | 17 | | /// </remarks> |
| | | 18 | | [SuppressMessage("Minor Code Smell", "S2292:Trivial properties should be auto-implemented")] |
| | | 19 | | public sealed record InlineFragmentDefinition |
| | | 20 | | { |
| | | 21 | | internal FieldChildren? _fields; |
| | | 22 | | internal Dictionary<string, InlineFragmentDefinition>? _fragments; |
| | | 23 | | internal List<string>? _spreadFragments; |
| | | 24 | | internal List<FieldDirective>? _directives; |
| | | 25 | | |
| | | 26 | | /// <summary> |
| | | 27 | | /// Creates an inline fragment for <paramref name="typeName"/> with no children. |
| | | 28 | | /// </summary> |
| | | 29 | | /// <param name="typeName">The GraphQL type the fragment narrows to (e.g. <c>"Repository"</c>). |
| | | 30 | | /// Rendered verbatim — case matters, GraphQL type names are case-sensitive.</param> |
| | 9906 | 31 | | public InlineFragmentDefinition(string typeName) |
| | | 32 | | { |
| | 9906 | 33 | | if (string.IsNullOrWhiteSpace(typeName)) |
| | | 34 | | { |
| | 9 | 35 | | throw new ArgumentException("Inline fragment type name cannot be null or whitespace.", nameof(typeName)); |
| | | 36 | | } |
| | | 37 | | |
| | 9897 | 38 | | TypeName = typeName; |
| | 9897 | 39 | | } |
| | | 40 | | |
| | | 41 | | /// <summary> |
| | | 42 | | /// The GraphQL type the fragment narrows to. Rendered verbatim after <c>... on </c>. |
| | | 43 | | /// </summary> |
| | | 44 | | [JsonPropertyName("typeName")] |
| | 10116 | 45 | | public string TypeName { get; init; } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// Fields selected when the parent value's runtime type is <see cref="TypeName"/>. |
| | | 49 | | /// </summary> |
| | | 50 | | [JsonPropertyName("fields")] |
| | | 51 | | public IReadOnlyDictionary<string, FieldDefinition> Fields |
| | 33 | 52 | | => (IReadOnlyDictionary<string, FieldDefinition>?)_fields ?? EmptyFields; |
| | | 53 | | |
| | | 54 | | /// <summary> |
| | | 55 | | /// Get-or-add the underlying <see cref="FieldChildren"/> store. Internal so the builder |
| | | 56 | | /// can pass a stable reference when the user's lambda adds fields to the fragment. |
| | | 57 | | /// </summary> |
| | | 58 | | internal FieldChildren GetOrCreateFieldsStore() |
| | 210 | 59 | | => _fields ??= new FieldChildren(); |
| | | 60 | | |
| | | 61 | | /// <summary> |
| | | 62 | | /// Nested inline fragments inside this fragment's selection set. Useful when the fragment's |
| | | 63 | | /// fields themselves return union/interface types that need further narrowing. |
| | | 64 | | /// </summary> |
| | | 65 | | [JsonPropertyName("inlineFragments")] |
| | | 66 | | public IReadOnlyDictionary<string, InlineFragmentDefinition> InlineFragments |
| | 15 | 67 | | => (IReadOnlyDictionary<string, InlineFragmentDefinition>?)_fragments ?? EmptyFragments; |
| | | 68 | | |
| | | 69 | | /// <summary> |
| | | 70 | | /// Names of <see cref="NamedFragmentDefinition"/>s spread into this inline fragment's |
| | | 71 | | /// selection set. See <see cref="FieldDefinition.SpreadFragments"/> for the contract; this |
| | | 72 | | /// property mirrors it. |
| | | 73 | | /// </summary> |
| | | 74 | | [JsonPropertyName("spreadFragments")] |
| | | 75 | | public IReadOnlyList<string> SpreadFragments |
| | 6 | 76 | | => (IReadOnlyList<string>?)_spreadFragments ?? EmptySpreadFragments; |
| | | 77 | | |
| | | 78 | | /// <summary> |
| | | 79 | | /// Directives attached to this inline fragment, in the order they were added. Rendered after |
| | | 80 | | /// <c>... on TypeName</c> and before the selection set, e.g. <c>... on Admin @include(if:$a){ … }</c>. |
| | | 81 | | /// Returns an empty list (never null) when the fragment has no directives. |
| | | 82 | | /// </summary> |
| | | 83 | | /// <remarks> |
| | | 84 | | /// See <see cref="Builders.FieldBuilder.IncludeIf(Variable)"/>/<see cref="Builders.FieldBuilder.SkipIf(Variable)"/> |
| | | 85 | | /// <see cref="Builders.FieldBuilder.Directive(string, System.Collections.Generic.Dictionary{string, object?})"/> |
| | | 86 | | /// called on the <see cref="Builders.FieldBuilder"/> passed to |
| | | 87 | | /// <see cref="Builders.FieldBuilder.OnType(string, System.Action{Builders.FieldBuilder})"/>'s |
| | | 88 | | /// lambda — the same directive API fields expose, attaching to the fragment itself rather than |
| | | 89 | | /// its parent field. |
| | | 90 | | /// </remarks> |
| | | 91 | | [JsonPropertyName("directives")] |
| | | 92 | | public IReadOnlyList<FieldDirective> Directives |
| | 6 | 93 | | => (IReadOnlyList<FieldDirective>?)_directives ?? EmptyDirectives; |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// Gets a value indicating whether this inline fragment carries any directives. Unlike reading |
| | | 97 | | /// <see cref="Directives"/>, this check allocates nothing on directive-less fragments. |
| | | 98 | | /// </summary> |
| | | 99 | | [JsonIgnore] |
| | 6 | 100 | | public bool HasDirectives => _directives is { Count: > 0 }; |
| | | 101 | | |
| | | 102 | | /// <summary> |
| | | 103 | | /// Append a directive to this fragment's directive list. Shares <see cref="DirectiveListOps"/> |
| | | 104 | | /// with <see cref="FieldDefinition.AddDirective"/>, so <c>include</c>/<c>skip</c> collapse |
| | | 105 | | /// (last-call-wins) and every other directive dedups structurally, identically to fields. |
| | | 106 | | /// </summary> |
| | 3 | 107 | | internal void AddDirective(FieldDirective directive) => DirectiveListOps.Add(ref _directives, directive); |
| | | 108 | | |
| | 3 | 109 | | private static readonly IReadOnlyDictionary<string, FieldDefinition> EmptyFields = new Dictionary<string, FieldDefin |
| | 3 | 110 | | private static readonly IReadOnlyDictionary<string, InlineFragmentDefinition> EmptyFragments = new Dictionary<string |
| | 3 | 111 | | private static readonly IReadOnlyList<string> EmptySpreadFragments = Array.Empty<string>(); |
| | 3 | 112 | | private static readonly IReadOnlyList<FieldDirective> EmptyDirectives = Array.Empty<FieldDirective>(); |
| | | 113 | | |
| | | 114 | | public bool Equals(InlineFragmentDefinition? other) |
| | | 115 | | { |
| | 21 | 116 | | if (other is null) return false; |
| | 18 | 117 | | if (ReferenceEquals(this, other)) return true; |
| | 12 | 118 | | return string.Equals(TypeName, other.TypeName, StringComparison.Ordinal); |
| | | 119 | | } |
| | | 120 | | |
| | 6 | 121 | | public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(TypeName); |
| | | 122 | | } |