Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,20 @@ private static void CollectFields(
CollectFields(inlineFragment, outputType, fields, path);
}

// A named fragment whose type condition is an abstract type (a union or
// interface) is not represented as its own interface for a concrete member,
// so we have to descend into it to reach the member selections it carries.
// Without this, member selections that are only reachable through such a
// fragment (for example `... AnimalDetails` where `AnimalDetails` is defined
// on the union) are lost and the generated member class comes out empty.
foreach (var namedFragment in fragmentNode.Nodes.Where(
t => t.Fragment.Kind == FragmentKind.Named
&& t.Fragment.TypeCondition.IsAbstractType()
&& t.Fragment.TypeCondition.IsAssignableFrom(outputType)))
{
CollectFields(namedFragment, outputType, fields, path);
}

foreach (var selection in
fragmentNode.Fragment.SelectionSet.Selections.OfType<FieldNode>())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ private static void CheckStrictMode()
}
}

private static ClientModel CreateClientModel(
public static ClientModel CreateClientModel(
string[] sourceText,
bool strictValidation,
bool noStore)
bool strictValidation = true,
bool noStore = false)
{
var files = sourceText
.Select(s => new GraphQLFile(Utf8GraphQLParser.Parse(s)))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using static StrawberryShake.CodeGeneration.CSharp.GeneratorTestHelper;

namespace StrawberryShake.CodeGeneration.CSharp;

public class UnionTypeGeneratorTests
{
private const string Schema =
"union Animal = Dog | Cat "
+ "type Dog { id: ID! name: String barkVolume: Int } "
+ "type Cat { id: ID! name: String meowVolume: Int } "
+ "type Query { animal: Animal }";

private const string SchemaExtensions = "extend schema @key(fields: \"id\")";

[Fact]
public void UnionMemberClasses_Should_ContainSelectedFields_When_SelectedViaInlineFragments()
{
// arrange
const string query =
"""
query GetAnimal {
animal {
... on Dog { name barkVolume }
... on Cat { name meowVolume }
}
}
""";

// act
var source = GenerateUnionClientSource(query);

// assert
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Dog", "Name", "BarkVolume");
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Cat", "Name", "MeowVolume");
}

[Fact]
public void UnionMemberClasses_Should_ContainSelectedFields_When_SelectedViaNamedFragmentOnUnion()
{
// arrange
// The named fragment is declared on the union type itself and contains the
// member inline fragments. This is the shape from issue 5992.
const string query =
"""
query GetAnimal {
animal {
... AnimalDetails
}
}

fragment AnimalDetails on Animal {
... on Dog { name barkVolume }
... on Cat { name meowVolume }
}
""";

// act
var source = GenerateUnionClientSource(query);

// assert
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Dog", "Name", "BarkVolume");
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Cat", "Name", "MeowVolume");
}

[Fact]
public void UnionMemberClasses_Should_ContainSelectedFields_When_SelectedViaNamedFragmentsOnMembers()
{
// arrange
// Each named fragment is declared on a concrete union member.
const string query =
"""
query GetAnimal {
animal {
... DogDetails
... CatDetails
}
}

fragment DogDetails on Dog { name barkVolume }

fragment CatDetails on Cat { name meowVolume }
""";

// act
var source = GenerateUnionClientSource(query);

// assert
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Dog", "Name", "BarkVolume");
AssertMemberClassContainsProperties(source, "GetAnimal_Animal_Cat", "Name", "MeowVolume");
}

private static string GenerateUnionClientSource(string query)
{
var clientModel = CreateClientModel([query, Schema, SchemaExtensions]);

var result = CSharpGenerator.Generate(
clientModel,
new CSharpGeneratorSettings
{
Namespace = "Foo.Bar",
ClientName = "FooClient",
AccessModifier = AccessModifier.Public
});

Assert.False(
result.Errors.Any(),
"It is expected that the result has no generator errors!");

return string.Join(
"\n",
result.Documents
.Where(t => t.Kind == SourceDocumentKind.CSharp)
.Select(t => t.SourceText));
}

private static void AssertMemberClassContainsProperties(
string source,
string className,
params string[] propertyNames)
{
var classIndex = source.IndexOf(
$"public partial class {className}",
StringComparison.Ordinal);

Assert.True(
classIndex >= 0,
$"Generated source does not contain class `{className}`.");

var classBody = source[classIndex..];
var nextClassIndex = classBody.IndexOf(
"public partial class ",
startIndex: "public partial class ".Length,
StringComparison.Ordinal);

if (nextClassIndex >= 0)
{
classBody = classBody[..nextClassIndex];
}

foreach (var propertyName in propertyNames)
{
Assert.True(
classBody.Contains($" {propertyName} {{", StringComparison.Ordinal)
|| classBody.Contains($" {propertyName} =>", StringComparison.Ordinal),
$"Generated class `{className}` is missing property `{propertyName}`. "
+ $"Class body:\n{classBody}");
}
}
}
Loading