UNPKG

10.2 kBPlain TextView Raw
1/**
2 * Copyright (c) 2016, John Hewson
3 * All rights reserved.
4 */
5
6/// <reference path="../typings/graphql-types.d.ts" />
7/// <reference path="../typings/graphql-language.d.ts" />
8/// <reference path="../typings/graphql-utilities.d.ts" />
9
10import {
11 OperationDefinition,
12 FragmentDefinition,
13 FragmentSpread,
14 InlineFragment,
15 SelectionSet,
16 Field,
17 Document,
18 parse
19} from "graphql/language";
20
21import {
22 ElmFieldDecl,
23 ElmDecl,
24 ElmTypeDecl,
25 ElmParameterDecl,
26 ElmExpr,
27 moduleToString,
28 typeToString
29} from './elm-ast';
30
31import {
32 GraphQLSchema,
33 GraphQLNonNull,
34 GraphQLList,
35 GraphQLScalarType,
36 GraphQLEnumType,
37 GraphQLType,
38 GraphQLInputType,
39 GraphQLUnionType
40} from 'graphql/type';
41
42import {
43 TypeInfo,
44 buildClientSchema,
45 introspectionQuery,
46 typeFromAST,
47} from 'graphql/utilities';
48
49import {
50 FragmentDefinitionMap,
51 GraphQLEnumMap,
52 elmSafeName,
53 typeToElm,
54 getRootType,
55} from './query-to-elm';
56
57export function decoderForQuery(def: OperationDefinition, info: TypeInfo,
58 schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
59 seenFragments: FragmentDefinitionMap): ElmExpr {
60 return decoderFor(def, info, schema, fragmentDefinitionMap, seenFragments);
61}
62
63export function decoderForFragment(def: FragmentDefinition, info: TypeInfo,
64 schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
65 seenFragments: FragmentDefinitionMap): ElmExpr {
66 return decoderFor(def, info, schema, fragmentDefinitionMap, seenFragments);
67}
68
69export function decoderFor(def: OperationDefinition | FragmentDefinition, info: TypeInfo,
70 schema: GraphQLSchema, fragmentDefinitionMap: FragmentDefinitionMap,
71 seenFragments: FragmentDefinitionMap): ElmExpr {
72
73 function walkDefinition(def: OperationDefinition | FragmentDefinition, info: TypeInfo) {
74 if (def.kind == 'OperationDefinition') {
75 return walkOperationDefinition(<OperationDefinition>def, info);
76 } else if (def.kind == 'FragmentDefinition') {
77 return walkFragmentDefinition(<FragmentDefinition>def, info);
78 }
79 }
80
81 function walkOperationDefinition(def: OperationDefinition, info: TypeInfo): ElmExpr {
82 info.enter(def);
83 if (def.operation == 'query' || def.operation == 'mutation') {
84 let decls: Array<ElmDecl> = [];
85 // Name
86 let name: string;
87 if (def.name) {
88 name = def.name.value;
89 } else {
90 name = 'AnonymousQuery';
91 }
92 let resultType = name[0].toUpperCase() + name.substr(1);
93 // todo: Directives
94 // SelectionSet
95 let expr = walkSelectionSet(def.selectionSet, info);
96 // VariableDefinition
97 let parameters: Array<ElmParameterDecl> = [];
98 if (def.variableDefinitions) {
99 for (let varDef of def.variableDefinitions) {
100 let name = varDef.variable.name.value;
101
102 let type = typeToString(typeToElm(typeFromAST(schema, varDef.type)), 0);
103 // todo: default value
104 parameters.push({ name, type });
105 }
106 }
107 info.leave(def);
108
109 return { expr: 'map ' + resultType + ' ' + expr.expr };
110 }
111 }
112
113 function walkFragmentDefinition(def: FragmentDefinition, info: TypeInfo): ElmExpr {
114 info.enter(def);
115
116 let name = def.name.value;
117
118 let decls: Array<ElmDecl> = [];
119 let resultType = name[0].toUpperCase() + name.substr(1);
120
121 // todo: Directives
122
123 // SelectionSet
124 let fields = walkSelectionSet(def.selectionSet, info);
125
126 let fieldNames = getSelectionSetFields(def.selectionSet, info);
127 let shape = `(\\${fieldNames.join(' ')} -> { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
128
129 info.leave(def);
130 return { expr: 'map ' + shape + ' ' + fields.expr };
131 }
132
133 function walkSelectionSet(selSet: SelectionSet, info: TypeInfo, seenFields: Array<string> = []): ElmExpr {
134 info.enter(selSet);
135 let fields: Array<ElmExpr> = [];
136 for (let sel of selSet.selections) {
137 if (sel.kind == 'Field') {
138 let field = <Field>sel;
139 if (seenFields.indexOf(field.name.value) == -1) {
140 fields.push(walkField(field, info));
141 seenFields.push(field.name.value);
142 }
143 } else if (sel.kind == 'FragmentSpread') {
144 // expand out all fragment spreads
145 let spreadName = (<FragmentSpread>sel).name.value;
146 let def = fragmentDefinitionMap[spreadName];
147 fields.push(walkSelectionSet(def.selectionSet, info, seenFields));
148 } else if (sel.kind == 'InlineFragment') {
149 throw new Error('Should not happen');
150 }
151 }
152 info.leave(selSet);
153 return { expr: fields.map(f => f.expr).filter(e => e.length > 0).join('\n |: ') }
154 }
155
156 function getSelectionSetFields(selSet: SelectionSet, info: TypeInfo): Array<string> {
157 info.enter(selSet);
158 let fields: Array<string> = [];
159 for (let sel of selSet.selections) {
160 if (sel.kind == 'Field') {
161 let field = <Field>sel;
162 let name = elmSafeName(field.name.value);
163 if (field.alias) {
164 name = elmSafeName(field.alias.value);
165 }
166 if (fields.indexOf(name) == -1) {
167 fields.push(name);
168 }
169 } else if (sel.kind == 'FragmentSpread') {
170 // expand out all fragment spreads
171 let spreadName = (<FragmentSpread>sel).name.value;
172 let def = fragmentDefinitionMap[spreadName];
173 for (let name of getSelectionSetFields(def.selectionSet, info)) {
174 if (fields.indexOf(name) == -1) {
175 fields.push(name);
176 }
177 }
178 } else if (sel.kind == 'InlineFragment') {
179 throw new Error('Should not happen');
180 }
181 }
182 info.leave(selSet);
183 return fields;
184 }
185
186 function walkField(field: Field, info: TypeInfo): ElmExpr {
187 info.enter(field);
188 // Name
189 let name = elmSafeName(field.name.value);
190 let originalName = field.name.value;
191
192 let info_type = info.getType()
193 let isMaybe = false
194 if (info_type instanceof GraphQLNonNull) {
195 info_type = info_type['ofType'];
196 } else {
197 isMaybe = true;
198 }
199 // Alias
200 if (field.alias) {
201 name = elmSafeName(field.alias.value);
202 originalName = field.alias.value;
203 }
204
205 // Arguments (opt)
206 let args = field.arguments; // e.g. id: "1000"
207
208 // todo: Directives
209
210 if (getRootType(info_type) instanceof GraphQLUnionType) {
211 // Union
212 return walkUnion(originalName, field, info);
213 } else {
214 // SelectionSet
215 if (field.selectionSet) {
216 let prefix = '';
217 if (info_type instanceof GraphQLList) {
218 prefix = 'list ';
219 }
220
221 let fields = walkSelectionSet(field.selectionSet, info);
222 info.leave(field);
223 let fieldNames = getSelectionSetFields(field.selectionSet, info);
224 let shape = `(\\${fieldNames.join(' ')} -> { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
225 let left = '(field "' + originalName + '" ';
226 let right = '(map ' + shape + ' ' + fields.expr + '))';
227 let indent = ' ';
228 if (prefix) {
229 right = '(' + prefix + right + ')';
230 }
231 if (isMaybe) {
232 right = '(' + 'maybe ' + right + ')';
233 }
234
235 return { expr: left + indent + right };
236 } else {
237 let decoder = leafTypeToDecoder(info_type);
238 info.leave(field);
239 let expr = { expr: '(field "' + originalName + '" (' + decoder +'))' };
240 if (isMaybe) {
241 expr = { expr: '(maybe ' + expr.expr + ')' };
242 }
243 return expr;
244 }
245 }
246 }
247
248 function walkUnion(originalName: string, field: Field, info: TypeInfo): ElmExpr {
249 let decoder = '\n (\\typename -> case typename of';
250 let indent = ' ';
251 let info_type = info.getType()
252
253 let isMaybe = false
254 if (info_type instanceof GraphQLNonNull) {
255 info_type = info_type['ofType']
256 } else {
257 isMaybe = true
258 }
259
260 let prefix = '';
261 if (info_type instanceof GraphQLList) {
262 prefix = 'list ';
263 }
264
265 for (let sel of field.selectionSet.selections) {
266 if (sel.kind == 'InlineFragment') {
267 let inlineFragment = <InlineFragment> sel;
268 decoder += `\n${indent}"${inlineFragment.typeCondition.name.value}" -> `;
269
270 info.enter(inlineFragment);
271 let fields = walkSelectionSet(inlineFragment.selectionSet, info);
272 info.leave(inlineFragment);
273 let fieldNames = getSelectionSetFields(inlineFragment.selectionSet, info);
274 let ctor = elmSafeName(inlineFragment.typeCondition.name.value);
275 let shape = `(\\${fieldNames.join(' ')} -> ${ctor} { ${fieldNames.map(f => f + ' = ' + f).join(', ')} })`;
276 let right = '(map ' + shape + ' ' + fields.expr + ')';
277 if (prefix) {
278 right = '(' + prefix + right + ')';
279 }
280 if (isMaybe) {
281 right = '(' + 'maybe ' + right + ')';
282 }
283 decoder += right;
284
285
286 } else if (sel.kind == 'Field') {
287 let field = <Field>sel;
288 if (field.name.value != '__typename') {
289 throw new Error('Unexpected field: ' + field.name.value);
290 }
291 } else {
292 throw new Error('Unexpected: ' + sel.kind);
293 }
294 }
295
296 decoder += `\n${indent}_ -> fail "Unexpected union type")`;
297
298 decoder = '((field "__typename" string) |> andThen ' + decoder + ')';
299 return { expr: '(field "' + originalName + '" ' + decoder +')' };
300 }
301
302 function leafTypeToDecoder(type: GraphQLType): string {
303 let prefix = '';
304
305 if (type instanceof GraphQLList) {
306 prefix = 'list ';
307 type = type['ofType'];
308 }
309
310 // leaf types only
311 if (type instanceof GraphQLScalarType) {
312 switch (type.name) {
313 case 'Int': return prefix + 'int';
314 case 'Float': return prefix + 'float';
315 case 'Boolean': return prefix + 'bool';
316 case 'ID':
317 case 'String': return prefix + 'string';
318 }
319 } else if (type instanceof GraphQLEnumType) {
320 return prefix + type.name.toLowerCase() + 'Decoder';
321 } else {
322 throw new Error('not a leaf type: ' + (<any>type).name);
323 }
324 }
325
326 return walkDefinition(def, info);
327}