| | | 1 | | using System.Runtime.CompilerServices; |
| | | 2 | | using NGql.Core.Abstractions; |
| | | 3 | | using NGql.Core.Features; |
| | | 4 | | |
| | | 5 | | namespace NGql.Core.Extensions; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Extension methods for QueryDefinition and field navigation/lookup utilities. |
| | | 9 | | /// </summary> |
| | | 10 | | internal static class QueryDefinitionExtensions |
| | | 11 | | { |
| | | 12 | | /// <summary> |
| | | 13 | | /// Navigates through a dot-separated path using name or alias matching. |
| | | 14 | | /// Returns the final field and optionally builds the resolved path. |
| | | 15 | | /// Optimized with ReadOnlySpan to avoid allocations. |
| | | 16 | | /// </summary> |
| | | 17 | | /// <param name="fields">Starting field collection</param> |
| | | 18 | | /// <param name="path">Dot-separated path (can use ReadOnlySpan for zero-alloc)</param> |
| | | 19 | | /// <param name="resolvedPath">Optional: outputs the actual path with field keys (not aliases)</param> |
| | | 20 | | /// <param name="prependPath">Optional: path to prepend to resolvedPath</param> |
| | | 21 | | /// <returns>The final field definition, or null if not found</returns> |
| | | 22 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 23 | | internal static FieldDefinition? NavigatePath( |
| | | 24 | | IReadOnlyDictionary<string, FieldDefinition>? fields, |
| | | 25 | | ReadOnlySpan<char> path, |
| | | 26 | | out string? resolvedPath, |
| | | 27 | | string? prependPath = null) |
| | | 28 | | { |
| | 321 | 29 | | resolvedPath = null; |
| | 336 | 30 | | if (fields is null || path.Length == 0) return null; |
| | | 31 | | |
| | 306 | 32 | | var pathSegments = new List<string>(); |
| | 306 | 33 | | var currentField = WalkPath(fields, path, pathSegments); |
| | 336 | 34 | | if (currentField is null) return null; |
| | | 35 | | |
| | 276 | 36 | | var joinedPath = string.Join(".", pathSegments); |
| | 276 | 37 | | resolvedPath = prependPath is not null ? $"{prependPath}.{joinedPath}" : joinedPath; |
| | 276 | 38 | | return currentField; |
| | | 39 | | } |
| | | 40 | | |
| | | 41 | | private static FieldDefinition? WalkPath(IReadOnlyDictionary<string, FieldDefinition> fields, ReadOnlySpan<char> pat |
| | | 42 | | { |
| | 306 | 43 | | var currentFields = fields; |
| | 306 | 44 | | FieldDefinition? currentField = null; |
| | | 45 | | |
| | 651 | 46 | | while (path.Length > 0) |
| | | 47 | | { |
| | 651 | 48 | | var dotIndex = path.IndexOf('.'); |
| | 651 | 49 | | var segment = dotIndex >= 0 ? path[..dotIndex] : path; |
| | | 50 | | |
| | 651 | 51 | | var match = PreserveExtensions.FindFieldByNameOrAlias(currentFields, segment); |
| | 678 | 52 | | if (!match.HasValue) return null; |
| | | 53 | | |
| | 624 | 54 | | currentField = match.Value.Value; |
| | 624 | 55 | | pathSegments.Add(match.Value.Key); |
| | | 56 | | |
| | 624 | 57 | | if (dotIndex < 0) break; |
| | 351 | 58 | | if (currentField._children is null) return null; |
| | | 59 | | |
| | 345 | 60 | | currentFields = currentField.Fields; |
| | 345 | 61 | | path = path[(dotIndex + 1)..]; |
| | | 62 | | } |
| | | 63 | | |
| | 276 | 64 | | return currentField; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | /// <summary> |
| | | 68 | | /// Recursively searches for a field by name through all descendant nodes. |
| | | 69 | | /// Returns all matching paths. |
| | | 70 | | /// </summary> |
| | | 71 | | internal static List<string> FindFieldRecursively( |
| | | 72 | | IReadOnlyDictionary<string, FieldDefinition> fields, |
| | | 73 | | string fieldName, |
| | | 74 | | string basePath) |
| | | 75 | | { |
| | 18 | 76 | | var results = new List<string>(); |
| | 18 | 77 | | FindFieldRecursivelyCore(fields, fieldName, basePath, results); |
| | 18 | 78 | | return results; |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | private static void FindFieldRecursivelyCore( |
| | | 82 | | IReadOnlyDictionary<string, FieldDefinition> fields, |
| | | 83 | | string fieldName, |
| | | 84 | | string basePath, |
| | | 85 | | List<string> results) |
| | | 86 | | { |
| | 228 | 87 | | foreach (var (key, fieldDef) in fields) |
| | | 88 | | { |
| | 78 | 89 | | var currentPath = string.IsNullOrEmpty(basePath) ? key : $"{basePath}.{key}"; |
| | | 90 | | |
| | 78 | 91 | | if (NameOrAliasMatches(fieldDef, fieldName)) |
| | | 92 | | { |
| | 18 | 93 | | results.Add(currentPath); |
| | | 94 | | } |
| | | 95 | | |
| | 78 | 96 | | if (fieldDef.HasFields) |
| | | 97 | | { |
| | 18 | 98 | | FindFieldRecursivelyCore(fieldDef._children!, fieldName, currentPath, results); |
| | | 99 | | } |
| | | 100 | | } |
| | 36 | 101 | | } |
| | | 102 | | |
| | | 103 | | private static bool NameOrAliasMatches(FieldDefinition field, string name) |
| | | 104 | | { |
| | 96 | 105 | | if (string.Equals(field.Name, name, StringComparison.OrdinalIgnoreCase)) return true; |
| | 60 | 106 | | if (string.IsNullOrEmpty(field.Alias)) return false; |
| | 60 | 107 | | return string.Equals(field.Alias, name, StringComparison.OrdinalIgnoreCase); |
| | | 108 | | } |
| | | 109 | | } |