Skip to content

Commit 8d40687

Browse files
authored
[Fusion] Added support for merging @serializeAs directives (#8874)
1 parent a32c695 commit 8d40687

18 files changed

+811
-297
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Extensions;
3+
using HotChocolate.Fusion.Options;
4+
using HotChocolate.Language;
5+
using HotChocolate.Types;
6+
using HotChocolate.Types.Mutable;
7+
using HotChocolate.Types.Mutable.Definitions;
8+
using HotChocolate.Types.Mutable.Directives;
9+
using ArgumentNames = HotChocolate.Fusion.WellKnownArgumentNames;
10+
using DirectiveNames = HotChocolate.Fusion.WellKnownDirectiveNames;
11+
12+
namespace HotChocolate.Fusion.DirectiveMergers;
13+
14+
internal class CacheControlDirectiveMerger(DirectiveMergeBehavior mergeBehavior)
15+
: DirectiveMergerBase(mergeBehavior)
16+
{
17+
public override string DirectiveName { get; } =
18+
mergeBehavior is DirectiveMergeBehavior.IncludePrivate
19+
? $"fusion__{DirectiveNames.CacheControl}"
20+
: DirectiveNames.CacheControl;
21+
22+
public override MutableDirectiveDefinition GetCanonicalDirectiveDefinition(ISchemaDefinition schema)
23+
{
24+
return CacheControlMutableDirectiveDefinition.Create(schema);
25+
}
26+
27+
public override void MergeDirectiveDefinition(
28+
MutableDirectiveDefinition directiveDefinition,
29+
MutableSchemaDefinition mergedSchema)
30+
{
31+
if (MergeBehavior is DirectiveMergeBehavior.IncludePrivate)
32+
{
33+
var scopeArgType = (MutableEnumTypeDefinition)directiveDefinition.Arguments["scope"].Type;
34+
scopeArgType.Name = $"fusion__{scopeArgType.Name}";
35+
mergedSchema.Types.Add(scopeArgType);
36+
}
37+
38+
base.MergeDirectiveDefinition(directiveDefinition, mergedSchema);
39+
}
40+
41+
public override void MergeDirectives(
42+
IDirectivesProvider mergedMember,
43+
ImmutableArray<IDirectivesProvider> memberDefinitions,
44+
MutableSchemaDefinition mergedSchema)
45+
{
46+
if (MergeBehavior is DirectiveMergeBehavior.Ignore)
47+
{
48+
return;
49+
}
50+
51+
if (!mergedSchema.DirectiveDefinitions.TryGetDirective(DirectiveName, out var directiveDefinition))
52+
{
53+
// Merged definition not found.
54+
return;
55+
}
56+
57+
var cacheControlDirectives =
58+
memberDefinitions
59+
.SelectMany(
60+
d => d.Directives.Where(dir => dir.Name == DirectiveNames.CacheControl))
61+
.Select(CacheControlDirective.From)
62+
.ToArray();
63+
64+
if (cacheControlDirectives.Length != memberDefinitions.Length)
65+
{
66+
// Only merge if all member definitions have the @cacheControl directive.
67+
return;
68+
}
69+
70+
// Null is the lowest value.
71+
var min = (int? acc, int? val) => (int?)(acc is null || val is null ? null : Math.Min(acc.Value, val.Value));
72+
var maxAge = cacheControlDirectives.Select(d => d.MaxAge).Aggregate(min);
73+
var sharedMaxAge = cacheControlDirectives.Select(d => d.SharedMaxAge).Aggregate(min);
74+
var inheritMaxAge = cacheControlDirectives.All(d => d.InheritMaxAge == true);
75+
var scope =
76+
cacheControlDirectives.Any(d => d.Scope is CacheControlScope.Private)
77+
? CacheControlScope.Private
78+
: CacheControlScope.Public;
79+
var vary = cacheControlDirectives.Where(d => d.Vary.HasValue).SelectMany(d => d.Vary!.Value).ToHashSet();
80+
81+
var argumentAssignments = new List<ArgumentAssignment>();
82+
83+
if (maxAge is not null)
84+
{
85+
argumentAssignments.Add(new ArgumentAssignment(ArgumentNames.MaxAge, maxAge.Value));
86+
}
87+
88+
if (sharedMaxAge is not null)
89+
{
90+
argumentAssignments.Add(new ArgumentAssignment(ArgumentNames.SharedMaxAge, sharedMaxAge.Value));
91+
}
92+
93+
if (!cacheControlDirectives.All(d => d.InheritMaxAge is null))
94+
{
95+
argumentAssignments.Add(new ArgumentAssignment(ArgumentNames.InheritMaxAge, inheritMaxAge));
96+
}
97+
98+
if (!cacheControlDirectives.All(d => d.Scope is null))
99+
{
100+
argumentAssignments.Add(
101+
new ArgumentAssignment(
102+
ArgumentNames.Scope,
103+
new EnumValueNode(Enum.GetName(scope)!.ToUpperInvariant())));
104+
}
105+
106+
if (vary.Count != 0)
107+
{
108+
argumentAssignments.Add(
109+
new ArgumentAssignment(
110+
ArgumentNames.Vary,
111+
new ListValueNode(vary.Select(v => new StringValueNode(v)).ToList())));
112+
}
113+
114+
var cacheControlDirective = new Directive(directiveDefinition, argumentAssignments);
115+
116+
mergedMember.AddDirective(cacheControlDirective);
117+
}
118+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Options;
3+
using HotChocolate.Types;
4+
using HotChocolate.Types.Mutable;
5+
6+
namespace HotChocolate.Fusion.DirectiveMergers;
7+
8+
internal abstract class DirectiveMergerBase(DirectiveMergeBehavior mergeBehavior) : IDirectiveMerger
9+
{
10+
public abstract string DirectiveName { get; }
11+
12+
public DirectiveMergeBehavior MergeBehavior { get; } = mergeBehavior;
13+
14+
public abstract MutableDirectiveDefinition GetCanonicalDirectiveDefinition(ISchemaDefinition schema);
15+
16+
public virtual void MergeDirectiveDefinition(
17+
MutableDirectiveDefinition directiveDefinition,
18+
MutableSchemaDefinition mergedSchema)
19+
{
20+
if (MergeBehavior is DirectiveMergeBehavior.IncludePrivate)
21+
{
22+
directiveDefinition.Name = DirectiveName;
23+
}
24+
25+
mergedSchema.DirectiveDefinitions.Add(directiveDefinition);
26+
}
27+
28+
public abstract void MergeDirectives(
29+
IDirectivesProvider mergedMember,
30+
ImmutableArray<IDirectivesProvider> memberDefinitions,
31+
MutableSchemaDefinition mergedSchema);
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Options;
3+
using HotChocolate.Types;
4+
using HotChocolate.Types.Mutable;
5+
6+
namespace HotChocolate.Fusion.DirectiveMergers;
7+
8+
internal interface IDirectiveMerger
9+
{
10+
string DirectiveName { get; }
11+
12+
DirectiveMergeBehavior MergeBehavior { get; }
13+
14+
MutableDirectiveDefinition GetCanonicalDirectiveDefinition(ISchemaDefinition schema);
15+
16+
void MergeDirectiveDefinition(
17+
MutableDirectiveDefinition directiveDefinition,
18+
MutableSchemaDefinition mergedSchema);
19+
20+
void MergeDirectives(
21+
IDirectivesProvider mergedMember,
22+
ImmutableArray<IDirectivesProvider> memberDefinitions,
23+
MutableSchemaDefinition mergedSchema);
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System.Collections.Immutable;
2+
using HotChocolate.Fusion.Extensions;
3+
using HotChocolate.Fusion.Options;
4+
using HotChocolate.Types;
5+
using HotChocolate.Types.Mutable;
6+
using DirectiveNames = HotChocolate.Fusion.WellKnownDirectiveNames;
7+
8+
namespace HotChocolate.Fusion.DirectiveMergers;
9+
10+
internal class OneOfDirectiveMerger(DirectiveMergeBehavior mergeBehavior)
11+
: DirectiveMergerBase(mergeBehavior)
12+
{
13+
public override string DirectiveName => DirectiveNames.OneOf;
14+
15+
public override MutableDirectiveDefinition GetCanonicalDirectiveDefinition(ISchemaDefinition schema)
16+
{
17+
return OneOfMutableDirectiveDefinition.Create(schema);
18+
}
19+
20+
public override void MergeDirectives(
21+
IDirectivesProvider mergedMember,
22+
ImmutableArray<IDirectivesProvider> memberDefinitions,
23+
MutableSchemaDefinition mergedSchema)
24+
{
25+
var oneOfDirective = memberDefinitions[0].Directives.FirstOrDefault(DirectiveNames.OneOf);
26+
27+
if (oneOfDirective is null)
28+
{
29+
// No @oneOf directive to merge.
30+
return;
31+
}
32+
33+
if (!mergedSchema.DirectiveDefinitions.TryGetDirective(DirectiveNames.OneOf, out var directiveDefinition))
34+
{
35+
// Merged definition not found.
36+
return;
37+
}
38+
39+
if (mergedMember is not MutableInputObjectTypeDefinition inputObjectType)
40+
{
41+
// @oneOf can only be applied to input object types.
42+
return;
43+
}
44+
45+
inputObjectType.IsOneOf = true;
46+
inputObjectType.AddDirective(new Directive(directiveDefinition));
47+
}
48+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
using System.Collections.Immutable;
2+
using System.Diagnostics;
3+
using HotChocolate.Fusion.Extensions;
4+
using HotChocolate.Fusion.Options;
5+
using HotChocolate.Language;
6+
using HotChocolate.Types;
7+
using HotChocolate.Types.Mutable;
8+
using HotChocolate.Types.Mutable.Definitions;
9+
using HotChocolate.Types.Mutable.Directives;
10+
using ArgumentNames = HotChocolate.Fusion.WellKnownArgumentNames;
11+
using DirectiveNames = HotChocolate.Fusion.WellKnownDirectiveNames;
12+
13+
namespace HotChocolate.Fusion.DirectiveMergers;
14+
15+
internal class SerializeAsDirectiveMerger(DirectiveMergeBehavior mergeBehavior)
16+
: DirectiveMergerBase(mergeBehavior)
17+
{
18+
public override string DirectiveName => DirectiveNames.SerializeAs;
19+
20+
public override MutableDirectiveDefinition GetCanonicalDirectiveDefinition(ISchemaDefinition schema)
21+
{
22+
return SerializeAsMutableDirectiveDefinition.Create(schema);
23+
}
24+
25+
public override void MergeDirectives(
26+
IDirectivesProvider mergedMember,
27+
ImmutableArray<IDirectivesProvider> memberDefinitions,
28+
MutableSchemaDefinition mergedSchema)
29+
{
30+
if (MergeBehavior is DirectiveMergeBehavior.Ignore)
31+
{
32+
return;
33+
}
34+
35+
if (!mergedSchema.DirectiveDefinitions.TryGetDirective(DirectiveName, out var directiveDefinition))
36+
{
37+
// Merged definition not found.
38+
return;
39+
}
40+
41+
var serializeAsDirectives =
42+
memberDefinitions
43+
.SelectMany(d => d.Directives.Where(dir => dir.Name == DirectiveNames.SerializeAs))
44+
.Select(SerializeAsDirective.From)
45+
.ToArray();
46+
47+
if (serializeAsDirectives.Length == 0)
48+
{
49+
return;
50+
}
51+
52+
// All @serializeAs directives must have the same type and pattern.
53+
var firstDirective = serializeAsDirectives[0];
54+
if (!serializeAsDirectives.All(
55+
d => d.Type == firstDirective.Type && d.Pattern == firstDirective.Pattern))
56+
{
57+
return;
58+
}
59+
60+
var argumentAssignments = new List<ArgumentAssignment>();
61+
var map = new Dictionary<ScalarSerializationType, EnumValueNode>();
62+
63+
foreach (var possibleValue in Enum.GetValues<ScalarSerializationType>())
64+
{
65+
if (possibleValue is ScalarSerializationType.Undefined)
66+
{
67+
continue;
68+
}
69+
70+
map.Add(possibleValue, new EnumValueNode(possibleValue.ToString().ToUpperInvariant()));
71+
}
72+
73+
using var types = GetSetTypes(firstDirective.Type).GetEnumerator();
74+
75+
IValueNode? typeArg = null;
76+
List<EnumValueNode>? listValue = null;
77+
78+
while (types.MoveNext())
79+
{
80+
if (listValue is null && typeArg is null)
81+
{
82+
typeArg = map[types.Current];
83+
}
84+
else if (typeArg is not null && listValue is null)
85+
{
86+
listValue = [(EnumValueNode)typeArg, map[types.Current]];
87+
typeArg = null;
88+
}
89+
else
90+
{
91+
listValue?.Add(map[types.Current]);
92+
}
93+
}
94+
95+
if (listValue is null && typeArg is null)
96+
{
97+
throw new InvalidOperationException("The @serializeAs directive has an invalid state.");
98+
}
99+
100+
if (typeArg is null)
101+
{
102+
Debug.Assert(listValue is not null);
103+
typeArg = new ListValueNode(listValue);
104+
}
105+
106+
argumentAssignments.Add(new ArgumentAssignment(ArgumentNames.Type, typeArg));
107+
108+
if (firstDirective.Pattern is not null)
109+
{
110+
argumentAssignments.Add(
111+
new ArgumentAssignment(ArgumentNames.Pattern, firstDirective.Pattern));
112+
}
113+
114+
var serializeAsDirective = new Directive(directiveDefinition, argumentAssignments);
115+
116+
mergedMember.AddDirective(serializeAsDirective);
117+
}
118+
119+
private static IEnumerable<ScalarSerializationType> GetSetTypes(ScalarSerializationType value)
120+
{
121+
var intValue = (int)value;
122+
for (var bit = 1; bit <= 32; bit <<= 1)
123+
{
124+
if ((intValue & bit) != 0)
125+
{
126+
yield return (ScalarSerializationType)bit;
127+
}
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)