Skip to content

Instantly share code, notes, and snippets.

@MelbourneDeveloper
Last active February 5, 2022 04:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MelbourneDeveloper/efae92a34af0f2ade0c6f6ee10105b2c to your computer and use it in GitHub Desktop.
Save MelbourneDeveloper/efae92a34af0f2ade0c6f6ee10105b2c to your computer and use it in GitHub Desktop.
Typeless GraphQL Select
using GraphQLParser;
using GraphQLParser.AST;
using Jayse;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System.Collections;
using System.Collections.Immutable;
namespace TypelessGraphQL;
#pragma warning disable IDE1006 // Naming Styles
#pragma warning disable CA2201 // Do not raise reserved exception types
#pragma warning disable CA1062 // Validate arguments of public methods
#pragma warning disable IDE0007 // Use implicit type
/*
.NET 6 unit test project that uses a GraphQL query to select on an object's
JSON, return the JSON and deserialize back to the original object.
It doesn't use types.The idea is to explore what is possible without
defining types
<PackageReference Include="GraphQL-Parser" Version="7.2.0" />
<PackageReference Include="Jayse" Version="0.6.0-alpha" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
*/
public record Root(ImmutableList<Item> items);
public record Item(string name, int id, string? category, Details details);
public record Details(Location location, Size size, bool isround);
public record Location(double latitude, double longitude);
public record Size(double width, double height);
public class SelectionNode
{
public string Name { get; }
public List<SelectionNode> Children { get; } = new List<SelectionNode>();
public SelectionNode(string name) => Name = name;
public override string ToString() => Name;
}
public static class ProcessingExtensions
{
public static string ToSelectionName(this GraphQLName selectionName) => selectionName.Value.Span.ToString();
public static SelectionNode ToSelectionNodes(this string graphQL)
{
var graphQlDocument = Parser.Parse(graphQL);
if (graphQlDocument.Definitions == null) throw new Exception("It's broken");
var rootSelectionNode = new SelectionNode("Root");
foreach (dynamic definition in graphQlDocument.Definitions)
{
foreach (var selection in (IEnumerable)definition.SelectionSet.Selections)
{
ProcessSelectionNode(selection, rootSelectionNode);
}
}
return rootSelectionNode;
}
public static JsonValue Process(this SelectionNode selectionNode, JsonValue inputJsonValue) =>
inputJsonValue.ValueType switch
{
JsonValueType.OfString => inputJsonValue,
JsonValueType.OfNumber => inputJsonValue,
JsonValueType.OfObject =>
new JsonValue(
new OrderedImmutableDictionary<string, JsonValue>(
selectionNode.Children.ToDictionary(childSelectionNode =>
childSelectionNode.Name, childSelectionNode =>
//Note this converts undefined to null. Perhaps this is a hole in Jayse?
inputJsonValue.ObjectValue.ContainsKey(childSelectionNode.Name)
? Process(childSelectionNode, inputJsonValue.ObjectValue[childSelectionNode.Name]) :
new JsonValue()))
),
JsonValueType.OfArray =>
new JsonValue
(inputJsonValue.ArrayValue.Select(jsonValue =>
Process(selectionNode, jsonValue)).ToImmutableList()
),
JsonValueType.OfBoolean => inputJsonValue,
JsonValueType.OfNull => inputJsonValue,
_ => throw new NotImplementedException(),
};
private static void ProcessSelectionNode(dynamic definition, SelectionNode parent)
{
var selectionNodeName = ((GraphQLName)definition.Name).ToSelectionName();
var selectionNode = new SelectionNode(selectionNodeName);
parent.Children.Add(selectionNode);
if (definition.SelectionSet == null) return;
foreach (dynamic selection in definition.SelectionSet.Selections)
{
ProcessSelectionNode(selection, selectionNode);
}
}
}
[TestClass]
public class Tests
{
[TestMethod]
public void TestTransformObjectWithTypelessGraphQL()
{
//Our input model
var expectedData = new Root(ImmutableList.Create(new Item[] {
new Item("TheName1", 1, null, new Details(new Location(100, 200),new Size(50,100), true)),
new Item("TheName2", 2, null, new Details(new Location(102, 202),new Size(52, 102), true))
}));
//Convert input to JSON
var expectedJson = JsonConvert.SerializeObject(expectedData);
//Convert the input JSON in to a Jayse JSON model
var inputJsonValue = expectedJson.ToJsonObject();
//Parse our GraphQL query in to a selection model we can work with
var rootSelectionNode =
"{ items { id name category details { location { latitude longitude } size { width height} isround undefinedValue } } }"
.ToSelectionNodes();
//Process the GraphQL selections
var outputJsonModel = new Dictionary<string, JsonValue>();
foreach (var selectionNode in rootSelectionNode.Children)
{
outputJsonModel.Add(selectionNode.Name, selectionNode.Process(inputJsonValue[selectionNode.Name]));
}
//Convert the output Jayse model in to JSON
var actualJson = outputJsonModel.ToJson();
//Derialize the JSON back to the original model
var actualSelectedData = JsonConvert.DeserializeObject<Root>(actualJson);
//Ensure that the model stayed intact
if (actualSelectedData == null) throw new Exception();
Assert.AreEqual(expectedData.items.Count, actualSelectedData.items.Count);
for (var i = 0; i < expectedData.items.Count; i++)
{
Assert.IsTrue(expectedData.items[i].Equals(actualSelectedData.items[i]));
}
//Check that the output model has the added undefined value from the GraphQL selection
Assert.IsTrue(outputJsonModel["items"][0]["details"]["undefinedValue"].ValueType == JsonValueType.OfNull);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment