Skip to content

Commit 276032e

Browse files
authored
[gql_code_builder] add dataClassConfig option to reuse class definitions for field selections that include only a single inline fragment spread
1 parent e021a37 commit 276032e

File tree

10 files changed

+357
-71
lines changed

10 files changed

+357
-71
lines changed

codegen/gql_build/lib/gql_build.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ Builder dataBuilder(
1919
BuilderOptions options,
2020
) =>
2121
DataBuilder(
22-
AssetId.parse(
23-
options.config["schema"] as String,
24-
),
25-
(options.config["add_typenames"] ?? true) as bool,
26-
typeOverrideMap(options.config["type_overrides"]),
27-
whenExtensionConfig: whenExtensionConfig(options.config));
22+
AssetId.parse(
23+
options.config["schema"] as String,
24+
),
25+
(options.config["add_typenames"] ?? true) as bool,
26+
typeOverrideMap(options.config["type_overrides"]),
27+
whenExtensionConfig: whenExtensionConfig(options.config),
28+
dataClassConfig: dataClassConfig(options.config),
29+
);
2830

2931
/// Builds GraphQL type-safe request builder
3032
Builder reqBuilder(

codegen/gql_build/lib/src/data_builder.dart

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class DataBuilder implements Builder {
1414
final bool addTypenames;
1515
final Map<String, Reference> typeOverrides;
1616
final InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig;
17+
final DataClassConfig dataClassConfig;
1718

1819
DataBuilder(
1920
this.schemaId,
@@ -23,6 +24,9 @@ class DataBuilder implements Builder {
2324
generateWhenExtensionMethod: false,
2425
generateMaybeWhenExtensionMethod: false,
2526
),
27+
this.dataClassConfig = const DataClassConfig(
28+
reuseFragments: false,
29+
),
2630
});
2731

2832
@override
@@ -41,11 +45,13 @@ class DataBuilder implements Builder {
4145
.path;
4246

4347
final library = buildDataLibrary(
44-
addTypenames ? introspection.addTypenames(doc) : doc,
45-
introspection.addTypenames(schema),
46-
basename(generatedPartUrl),
47-
typeOverrides,
48-
whenExtensionConfig);
48+
addTypenames ? introspection.addTypenames(doc) : doc,
49+
introspection.addTypenames(schema),
50+
basename(generatedPartUrl),
51+
typeOverrides,
52+
whenExtensionConfig,
53+
dataClassConfig,
54+
);
4955

5056
return writeDocument(
5157
library,

codegen/gql_build/lib/src/utils/config.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ EnumFallbackConfig enumFallbackConfig(Map<String, dynamic> config) =>
4242
fallbackValueMap: enumFallbackMap(config["enum_fallbacks"]),
4343
);
4444

45+
DataClassConfig dataClassConfig(Map<String, dynamic> config) => DataClassConfig(
46+
reuseFragments: config["reuse_fragments"] == true,
47+
);
48+
4549
InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig(
4650
Map<String, dynamic> config) {
4751
final whenYamlConfig = config["when_extensions"] as YamlMap?;

codegen/gql_code_builder/lib/data.dart

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import "package:built_collection/built_collection.dart";
22
import "package:code_builder/code_builder.dart";
33
import "package:gql/ast.dart";
4+
import "package:gql_code_builder/src/common.dart";
5+
import "package:gql_code_builder/src/config/data_class_config.dart";
46
import "package:gql_code_builder/src/config/when_extension_config.dart";
57

68
import "./source.dart";
79
import "./src/operation/data.dart";
810

11+
export "package:gql_code_builder/src/config/data_class_config.dart";
912
export "package:gql_code_builder/src/config/when_extension_config.dart";
1013

1114
Library buildDataLibrary(
@@ -18,7 +21,15 @@ Library buildDataLibrary(
1821
generateWhenExtensionMethod: false,
1922
generateMaybeWhenExtensionMethod: false,
2023
),
24+
DataClassConfig dataClassConfig = const DataClassConfig(
25+
reuseFragments: false,
26+
),
2127
]) {
28+
final fragmentMap = _fragmentMap(docSource);
29+
final dataClassAliasMap = dataClassConfig.reuseFragments
30+
? _dataClassAliasMap(docSource, fragmentMap)
31+
: <String, Reference>{};
32+
2233
final operationDataClasses = docSource.document.definitions
2334
.whereType<OperationDefinitionNode>()
2435
.expand(
@@ -28,6 +39,8 @@ Library buildDataLibrary(
2839
schemaSource,
2940
typeOverrides,
3041
whenExtensionConfig,
42+
fragmentMap,
43+
dataClassAliasMap,
3144
),
3245
)
3346
.toList();
@@ -41,6 +54,8 @@ Library buildDataLibrary(
4154
schemaSource,
4255
typeOverrides,
4356
whenExtensionConfig,
57+
fragmentMap,
58+
dataClassAliasMap,
4459
),
4560
)
4661
.toList();
@@ -54,3 +69,140 @@ Library buildDataLibrary(
5469
]),
5570
);
5671
}
72+
73+
Map<String, SourceSelections> _fragmentMap(SourceNode source) => {
74+
for (var def
75+
in source.document.definitions.whereType<FragmentDefinitionNode>())
76+
def.name.value: SourceSelections(
77+
url: source.url,
78+
selections: def.selectionSet.selections,
79+
),
80+
for (var import in source.imports) ..._fragmentMap(import)
81+
};
82+
83+
Map<String, Reference> _dataClassAliasMap(
84+
SourceNode source, Map<String, SourceSelections> fragmentMap,
85+
[Map<String, Reference>? aliasMap, Set<String>? visitedSource]) {
86+
aliasMap ??= {};
87+
visitedSource ??= {};
88+
89+
source.imports.forEach((s) {
90+
if (!visitedSource!.contains(source.url)) {
91+
visitedSource.add(source.url);
92+
_dataClassAliasMap(s, fragmentMap, aliasMap);
93+
}
94+
});
95+
96+
for (final def
97+
in source.document.definitions.whereType<OperationDefinitionNode>()) {
98+
_dataClassAliasMapDFS(
99+
typeRefPrefix: builtClassName("${def.name!.value}Data"),
100+
getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data",
101+
selections: def.selectionSet.selections,
102+
fragmentMap: fragmentMap,
103+
aliasMap: aliasMap,
104+
);
105+
}
106+
107+
for (final def
108+
in source.document.definitions.whereType<FragmentDefinitionNode>()) {
109+
_dataClassAliasMapDFS(
110+
typeRefPrefix: builtClassName(def.name.value),
111+
getAliasTypeName: builtClassName,
112+
selections: def.selectionSet.selections,
113+
fragmentMap: fragmentMap,
114+
aliasMap: aliasMap,
115+
);
116+
_dataClassAliasMapDFS(
117+
typeRefPrefix: builtClassName("${def.name.value}Data"),
118+
getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data",
119+
selections: def.selectionSet.selections,
120+
fragmentMap: fragmentMap,
121+
aliasMap: aliasMap,
122+
);
123+
}
124+
125+
return aliasMap;
126+
}
127+
128+
void _dataClassAliasMapDFS({
129+
required String typeRefPrefix,
130+
required String Function(String fragmentName) getAliasTypeName,
131+
required List<SelectionNode> selections,
132+
required Map<String, SourceSelections> fragmentMap,
133+
required Map<String, Reference> aliasMap,
134+
}) {
135+
if (selections.isEmpty) return;
136+
137+
// flatten selections to extract untouched fragments while visiting children.
138+
final shrunkenSelections =
139+
shrinkSelections(mergeSelections(selections, fragmentMap), fragmentMap);
140+
141+
// alias single fragment and finish
142+
final selectionsWithoutTypename = shrunkenSelections
143+
.where((s) => !(s is FieldNode && s.name.value == "__typename"));
144+
if (selectionsWithoutTypename.length == 1 &&
145+
selectionsWithoutTypename.first is FragmentSpreadNode) {
146+
final node = selectionsWithoutTypename.first as FragmentSpreadNode;
147+
final fragment = fragmentMap[node.name.value];
148+
final fragmentTypeName = getAliasTypeName(node.name.value);
149+
aliasMap[typeRefPrefix] =
150+
refer(fragmentTypeName, "${fragment!.url ?? ""}#data");
151+
// print("alias $typeRefPrefix => $fragmentTypeName");
152+
return;
153+
}
154+
155+
for (final node in selectionsWithoutTypename) {
156+
if (node is FragmentSpreadNode) {
157+
// exclude redefined selections from each fragment selections
158+
final fragmentSelections = fragmentMap[node.name.value]!.selections;
159+
final exclusiveFragmentSelections =
160+
mergeSelections(fragmentSelections, fragmentMap).where((s1) {
161+
if (s1 is FieldNode) {
162+
final name = (s1.alias ?? s1.name).value;
163+
return selectionsWithoutTypename
164+
.whereType<FieldNode>()
165+
.every((s2) => name != (s2.alias ?? s2.name).value);
166+
} else if (s1 is InlineFragmentNode && s1.typeCondition != null) {
167+
/// TODO: Handle inline fragments without a type condition
168+
final name = s1.typeCondition!.on.name.value;
169+
return selectionsWithoutTypename
170+
.whereType<InlineFragmentNode>()
171+
.every((s2) => name != s2.typeCondition?.on.name.value);
172+
}
173+
return false;
174+
}).toList();
175+
176+
_dataClassAliasMapDFS(
177+
typeRefPrefix: typeRefPrefix,
178+
getAliasTypeName: getAliasTypeName,
179+
selections: exclusiveFragmentSelections,
180+
fragmentMap: fragmentMap,
181+
aliasMap: aliasMap,
182+
);
183+
} else if (node is InlineFragmentNode) {
184+
if (node.typeCondition != null) {
185+
/// TODO: Handle inline fragments without a type condition
186+
_dataClassAliasMapDFS(
187+
typeRefPrefix:
188+
"${typeRefPrefix}__as${node.typeCondition!.on.name.value}",
189+
getAliasTypeName: getAliasTypeName,
190+
selections: [
191+
...selections.where((s) => s != node),
192+
...node.selectionSet.selections,
193+
],
194+
fragmentMap: fragmentMap,
195+
aliasMap: aliasMap,
196+
);
197+
}
198+
} else if (node is FieldNode && node.selectionSet != null) {
199+
_dataClassAliasMapDFS(
200+
typeRefPrefix: "${typeRefPrefix}_${(node.alias ?? node.name).value}",
201+
getAliasTypeName: getAliasTypeName,
202+
selections: node.selectionSet!.selections,
203+
fragmentMap: fragmentMap,
204+
aliasMap: aliasMap,
205+
);
206+
}
207+
}
208+
}

codegen/gql_code_builder/lib/src/built_class.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Class builtClass({
1111
Map<String, Expression>? initializers,
1212
Map<String, SourceSelections> superclassSelections = const {},
1313
List<Method> methods = const [],
14+
Map<String, Reference>? dataClassAliasMap,
1415
}) {
1516
final className = builtClassName(name);
1617
return Class(
@@ -30,12 +31,16 @@ Class builtClass({
3031
],
3132
),
3233
),
33-
...superclassSelections.keys.map<Reference>(
34-
(superName) => refer(
35-
builtClassName(superName),
36-
(superclassSelections[superName]?.url ?? "") + "#data",
37-
),
38-
)
34+
...superclassSelections.keys
35+
.where((superName) =>
36+
dataClassAliasMap?.containsKey(builtClassName(superName)) !=
37+
true)
38+
.map<Reference>(
39+
(superName) => refer(
40+
builtClassName(superName),
41+
(superclassSelections[superName]?.url ?? "") + "#data",
42+
),
43+
)
3944
],
4045
)
4146
..constructors.addAll(

codegen/gql_code_builder/lib/src/common.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Method buildGetter({
138138
required TypeNode typeNode,
139139
required SourceNode schemaSource,
140140
Map<String, Reference> typeOverrides = const {},
141+
Reference? typeRefAlias,
141142
String? typeRefPrefix,
142143
bool built = true,
143144
bool isOverride = false,
@@ -151,7 +152,9 @@ Method buildGetter({
151152

152153
final typeMap = {
153154
...defaultTypeMap,
154-
if (typeRefPrefix != null)
155+
if (typeRefAlias != null)
156+
typeName: typeRefAlias
157+
else if (typeRefPrefix != null)
155158
typeName: refer("${typeRefPrefix}_${nameNode.value}")
156159
else if (typeDef != null)
157160
typeName: refer(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// config for the optimization of data class generation.
2+
class DataClassConfig {
3+
final bool reuseFragments;
4+
5+
const DataClassConfig({
6+
required this.reuseFragments,
7+
});
8+
}

0 commit comments

Comments
 (0)