From 40fd080ce661cfa9562980634af97f4cfdf402c7 Mon Sep 17 00:00:00 2001 From: Fabio Murru Date: Tue, 28 Nov 2023 16:18:01 +0100 Subject: [PATCH] Implemented fragments shared between multiple queries .graphqlfrag files are parsed as GraphQLFragmentFile It's possible to add fragments in GraphQLConfig Queries now contains a list of all Fragments that they references, that list can be used to get Fragments from graphql client --- .../SimpleGraphQL/GraphQLFragmentImporter.cs | 58 +++++++++++++++++++ .../GraphQLFragmentImporter.cs.meta | 3 + Editor/SimpleGraphQL/GraphQLImporterV1.cs | 5 +- .../GraphQLParser/AST/ASTNode.cs | 40 ++++++++++++- Runtime/SimpleGraphQL/Fragment.cs | 52 +++++++++++++++++ Runtime/SimpleGraphQL/Fragment.cs.meta | 3 + Runtime/SimpleGraphQL/GraphQLClient.cs | 27 ++++++++- Runtime/SimpleGraphQL/GraphQLConfig.cs | 3 + Runtime/SimpleGraphQL/GraphQLFragmentFile.cs | 10 ++++ .../SimpleGraphQL/GraphQLFragmentFile.cs.meta | 3 + Runtime/SimpleGraphQL/Query.cs | 29 ++++++++-- 11 files changed, 222 insertions(+), 11 deletions(-) create mode 100644 Editor/SimpleGraphQL/GraphQLFragmentImporter.cs create mode 100644 Editor/SimpleGraphQL/GraphQLFragmentImporter.cs.meta create mode 100644 Runtime/SimpleGraphQL/Fragment.cs create mode 100644 Runtime/SimpleGraphQL/Fragment.cs.meta create mode 100644 Runtime/SimpleGraphQL/GraphQLFragmentFile.cs create mode 100644 Runtime/SimpleGraphQL/GraphQLFragmentFile.cs.meta diff --git a/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs b/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs new file mode 100644 index 0000000..dcdb086 --- /dev/null +++ b/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEngine; +using SimpleGraphQL.GraphQLParser; +using SimpleGraphQL.GraphQLParser.AST; + +// ifdef for different unity versions +#if UNITY_2020_2_OR_NEWER +using UnityEditor.AssetImporters; + +#elif UNITY_2017_1_OR_NEWER +using UnityEditor.Experimental.AssetImporters; +#endif + +namespace SimpleGraphQL +{ + [ScriptedImporter(1, "graphqlfrag")] + public class GraphQLFragmentImporter : ScriptedImporter + { + public override void OnImportAsset(AssetImportContext ctx) + { + var lexer = new Lexer(); + var parser = new Parser(lexer); + string contents = File.ReadAllText(ctx.assetPath); + var queryFile = ScriptableObject.CreateInstance(); + + GraphQLDocument graphQLDocument = parser.Parse(new Source(contents)); + + List operations = graphQLDocument.Definitions + .FindAll(x => x.Kind == ASTNodeKind.FragmentDefinition) + .Select(x => (GraphQLFragmentDefinition) x) + .ToList(); + + if (operations.Count > 0) + { + foreach (GraphQLFragmentDefinition operation in operations) + { + queryFile.Fragment = new Fragment + { + Name = operation.Name?.Value, + TypeCondition = operation.TypeCondition?.Name?.Value, + Source = contents + }; + } + } + else + { + throw new ArgumentException( + $"There were no operation definitions inside this graphql: {ctx.assetPath}\nPlease ensure that there is at least one operation defined!"); + } + + ctx.AddObjectToAsset("FragmentScriptableObject", queryFile); + ctx.SetMainObject(queryFile); + } + } +} \ No newline at end of file diff --git a/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs.meta b/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs.meta new file mode 100644 index 0000000..eae1a4b --- /dev/null +++ b/Editor/SimpleGraphQL/GraphQLFragmentImporter.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: afea354c10f04202a2f39b4c0c58dd3a +timeCreated: 1700827700 \ No newline at end of file diff --git a/Editor/SimpleGraphQL/GraphQLImporterV1.cs b/Editor/SimpleGraphQL/GraphQLImporterV1.cs index ed2cc7c..5253a44 100644 --- a/Editor/SimpleGraphQL/GraphQLImporterV1.cs +++ b/Editor/SimpleGraphQL/GraphQLImporterV1.cs @@ -51,12 +51,15 @@ public override void OnImportAsset(AssetImportContext ctx) Debug.LogWarning("Unable to convert operation type in " + ctx.assetPath); } + var fragments = operation.Descendants().Where(node => node is GraphQLFragmentSpread).Select(node => ((GraphQLFragmentSpread)node).Name.Value).ToArray(); + queryFile.Queries.Add(new Query { FileName = fileName, OperationName = operation.Name?.Value, OperationType = operationType, - Source = contents + Source = contents, + Fragments = fragments }); } } diff --git a/Plugins/SimpleGraphQL/GraphQLParser/AST/ASTNode.cs b/Plugins/SimpleGraphQL/GraphQLParser/AST/ASTNode.cs index 570c513..eddca59 100644 --- a/Plugins/SimpleGraphQL/GraphQLParser/AST/ASTNode.cs +++ b/Plugins/SimpleGraphQL/GraphQLParser/AST/ASTNode.cs @@ -1,4 +1,7 @@ -namespace SimpleGraphQL.GraphQLParser.AST +using System.Collections.Generic; +using System.Linq; + +namespace SimpleGraphQL.GraphQLParser.AST { public abstract class ASTNode { @@ -7,5 +10,38 @@ public abstract class ASTNode public GraphQLLocation Location { get; set; } public GraphQLComment Comment { get; set; } + + } + + public static class ASTNodeExtensions + { + public static IEnumerable Descendants(this ASTNode root) + { + var nodes = new Stack(new[] {root}); + while (nodes.Any()) + { + ASTNode node = nodes.Pop(); + yield return node; + + GraphQLSelectionSet selectionSet = null; + + if (node is GraphQLFieldSelection fieldSelection) + { + selectionSet = fieldSelection.SelectionSet; + } + + if (node is GraphQLOperationDefinition operationDefinition) + { + selectionSet = operationDefinition.SelectionSet; + } + + if(selectionSet != null && selectionSet.Selections != null) + { + foreach (var n in selectionSet.Selections) + nodes.Push(n); + } + } + } + } -} \ No newline at end of file +} diff --git a/Runtime/SimpleGraphQL/Fragment.cs b/Runtime/SimpleGraphQL/Fragment.cs new file mode 100644 index 0000000..ec43174 --- /dev/null +++ b/Runtime/SimpleGraphQL/Fragment.cs @@ -0,0 +1,52 @@ +using System; +using JetBrains.Annotations; +using UnityEngine; + +namespace SimpleGraphQL +{ + [PublicAPI] + [Serializable] + public class Fragment + { + /// + /// The name of the fragment. + /// + [CanBeNull] + public string Name; + + /// + /// The type the fragment is selecting from. + /// + public string TypeCondition; + + /// + /// The actual fragment itself. + /// + [TextArea] + public string Source; + + public override string ToString() + { + return $"fragment {Name} on {TypeCondition}"; + } + + protected bool Equals(Fragment other) + { + return Name == other.Name; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Fragment)obj); + } + + public override int GetHashCode() + { + return (Name != null ? Name.GetHashCode() : 0); + } + } + +} \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/Fragment.cs.meta b/Runtime/SimpleGraphQL/Fragment.cs.meta new file mode 100644 index 0000000..2fd2b33 --- /dev/null +++ b/Runtime/SimpleGraphQL/Fragment.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3e7c80271ce3481ea2730ffdd022ecdd +timeCreated: 1700841659 \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/GraphQLClient.cs b/Runtime/SimpleGraphQL/GraphQLClient.cs index c2b1143..8102ae0 100644 --- a/Runtime/SimpleGraphQL/GraphQLClient.cs +++ b/Runtime/SimpleGraphQL/GraphQLClient.cs @@ -16,6 +16,7 @@ public class GraphQLClient { public readonly List SearchableQueries; public readonly Dictionary CustomHeaders; + public readonly Dictionary Fragments; public string Endpoint; public string AuthScheme; @@ -24,13 +25,14 @@ public GraphQLClient( string endpoint, IEnumerable queries = null, Dictionary headers = null, - string authScheme = null - ) + string authScheme = null, + IEnumerable fragments = null) { Endpoint = endpoint; AuthScheme = authScheme; SearchableQueries = queries?.ToList(); CustomHeaders = headers; + Fragments = fragments?.ToDictionary(fragment => fragment.Name); } public GraphQLClient(GraphQLConfig config) @@ -39,6 +41,7 @@ public GraphQLClient(GraphQLConfig config) SearchableQueries = config.Files.SelectMany(x => x.Queries).ToList(); CustomHeaders = config.CustomHeaders.ToDictionary(header => header.Key, header => header.Value); AuthScheme = config.AuthScheme; + Fragments = config.Fragments.ToDictionary(file => file.Fragment.Name, file => file.Fragment); } /// @@ -342,5 +345,25 @@ public List FindQueriesByOperation(string operation) { return SearchableQueries?.FindAll(x => x.OperationName == operation); } + + public List FindFragments(IEnumerable fragmentNames) + { + List fragments = new(); + + if (fragmentNames == null) + { + return fragments; + } + + foreach (var fragmentName in fragmentNames) + { + if (Fragments.TryGetValue(fragmentName, out var fragment)) + { + fragments.Add(fragment); + } + } + + return fragments; + } } } \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/GraphQLConfig.cs b/Runtime/SimpleGraphQL/GraphQLConfig.cs index 41f6444..e982b00 100644 --- a/Runtime/SimpleGraphQL/GraphQLConfig.cs +++ b/Runtime/SimpleGraphQL/GraphQLConfig.cs @@ -20,6 +20,9 @@ public class GraphQLConfig : ScriptableObject [Header(".graphql Files")] public List Files; + [Header("Fragment files")] + public List Fragments; + /// /// Set the auth scheme to be used here if you need authentication. /// You can also use CustomHeaders to pass in authentication if needed, but this is inherently less secure. diff --git a/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs b/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs new file mode 100644 index 0000000..edbf977 --- /dev/null +++ b/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace SimpleGraphQL +{ + public class GraphQLFragmentFile : ScriptableObject + { + public Fragment Fragment; + } +} \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs.meta b/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs.meta new file mode 100644 index 0000000..b4393c7 --- /dev/null +++ b/Runtime/SimpleGraphQL/GraphQLFragmentFile.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ed536702a30f4d66a351082674fda5d8 +timeCreated: 1700841610 \ No newline at end of file diff --git a/Runtime/SimpleGraphQL/Query.cs b/Runtime/SimpleGraphQL/Query.cs index 9b4df0d..303ef8d 100644 --- a/Runtime/SimpleGraphQL/Query.cs +++ b/Runtime/SimpleGraphQL/Query.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Text; using JetBrains.Annotations; namespace SimpleGraphQL @@ -32,6 +34,11 @@ public class Query /// public string Source; + /// + /// Fragments used by this query + /// + public string[] Fragments; + public override string ToString() { return $"{FileName}:{OperationName}:{OperationType}"; @@ -41,14 +48,24 @@ public override string ToString() [PublicAPI] public static class QueryExtensions { - public static Request ToRequest(this Query query, object variables = null) + public static Request ToRequest(this Query query, object variables = null, IEnumerable fragments = null) { - return new Request + StringBuilder querySource = new StringBuilder(query.Source); + + if (fragments != null) { - Query = query.Source, - Variables = variables, - OperationName = query.OperationName, - }; + foreach (var fragment in fragments) + { + querySource.AppendLine(fragment?.Source); + } + } + + return new Request + { + Query = querySource.ToString(), + Variables = variables, + OperationName = query.OperationName, + }; } }