UNPKG

12.2 kBPlain TextView Raw
1import {
2 GraphQLType,
3 GraphQLString,
4 GraphQLInt,
5 GraphQLFloat,
6 GraphQLBoolean,
7 GraphQLID,
8 GraphQLScalarType,
9 isCompositeType,
10 getNamedType,
11 GraphQLInputField,
12 isNonNullType,
13 isListType,
14 isScalarType,
15 isEnumType,
16} from "graphql";
17
18import {
19 camelCase as _camelCase,
20 pascalCase as _pascalCase,
21} from "change-case";
22import * as Inflector from "inflected";
23import { join, wrap } from "apollo-codegen-core/lib/utilities/printing";
24
25import { Property, Struct, SwiftSource, swift } from "./language";
26
27import {
28 CompilerOptions,
29 SelectionSet,
30 Field,
31 FragmentSpread,
32 Argument,
33} from "apollo-codegen-core/lib/compiler";
34import { isMetaFieldName } from "apollo-codegen-core/lib/utilities/graphql";
35import { Variant } from "apollo-codegen-core/lib/compiler/visitors/typeCase";
36import { collectAndMergeFields } from "apollo-codegen-core/lib/compiler/visitors/collectAndMergeFields";
37
38// In this file, most functions work with strings, but anything that takes or receives an
39// expression uses `SwiftSource`. This way types and names stay represented as strings for as long as
40// possible.
41
42const builtInScalarMap = {
43 [GraphQLString.name]: "String",
44 [GraphQLInt.name]: "Int",
45 [GraphQLFloat.name]: "Double",
46 [GraphQLBoolean.name]: "Bool",
47 [GraphQLID.name]: "GraphQLID",
48};
49
50export class Helpers {
51 constructor(public options: CompilerOptions) {}
52
53 // Types
54
55 typeNameFromGraphQLType(
56 type: GraphQLType,
57 unmodifiedTypeName?: string,
58 isOptional?: boolean
59 ): string {
60 if (isNonNullType(type)) {
61 return this.typeNameFromGraphQLType(
62 type.ofType,
63 unmodifiedTypeName,
64 false
65 );
66 } else if (isOptional === undefined) {
67 isOptional = true;
68 }
69
70 let typeName;
71 if (isListType(type)) {
72 typeName =
73 "[" +
74 this.typeNameFromGraphQLType(type.ofType, unmodifiedTypeName) +
75 "]";
76 } else if (isScalarType(type)) {
77 typeName = this.typeNameForScalarType(type);
78 } else {
79 typeName = unmodifiedTypeName || type.name;
80 }
81
82 return isOptional ? typeName + "?" : typeName;
83 }
84
85 typeNameForScalarType(type: GraphQLScalarType): string {
86 return (
87 builtInScalarMap[type.name] ||
88 (this.options.passthroughCustomScalars
89 ? this.options.customScalarsPrefix + type.name
90 : GraphQLString.name)
91 );
92 }
93
94 fieldTypeEnum(type: GraphQLType, structName: string): SwiftSource {
95 if (isNonNullType(type)) {
96 return swift`.nonNull(${this.fieldTypeEnum(type.ofType, structName)})`;
97 } else if (isListType(type)) {
98 return swift`.list(${this.fieldTypeEnum(type.ofType, structName)})`;
99 } else if (isScalarType(type)) {
100 return swift`.scalar(${this.typeNameForScalarType(type)}.self)`;
101 } else if (isEnumType(type)) {
102 return swift`.scalar(${type.name}.self)`;
103 } else if (isCompositeType(type)) {
104 return swift`.object(${structName}.selections)`;
105 } else {
106 throw new Error(`Unknown field type: ${type}`);
107 }
108 }
109
110 // Names
111
112 enumCaseName(name: string) {
113 return camelCase(name);
114 }
115
116 enumDotCaseName(name: string): SwiftSource {
117 return swift`.${SwiftSource.memberName(camelCase(name))}`;
118 }
119
120 operationClassName(name: string) {
121 return pascalCase(name);
122 }
123
124 structNameForPropertyName(propertyName: string) {
125 return pascalCase(Inflector.singularize(propertyName));
126 }
127
128 structNameForFragmentName(fragmentName: string) {
129 return pascalCase(fragmentName);
130 }
131
132 structNameForVariant(variant: SelectionSet) {
133 return (
134 "As" +
135 variant.possibleTypes.map((type) => pascalCase(type.name)).join("Or")
136 );
137 }
138
139 /**
140 * Returns the internal parameter name for a given property name.
141 *
142 * If the property name is valid to use, it's returned directly. Otherwise it's prefixed with an
143 * underscore and modified until it's unique among the given property set.
144 * @param propertyName The name of the property.
145 * @param properties A list of properties that should be consulted when producing a unique name.
146 * @returns The name to use for the internal parameter name for the property.
147 */
148 internalParameterName(
149 propertyName: string,
150 properties: { propertyName: string }[]
151 ): string {
152 return SwiftSource.isValidParameterName(propertyName)
153 ? propertyName
154 : makeUniqueName(`_${propertyName}`, properties);
155 }
156
157 // Properties
158
159 propertyFromField(
160 field: Field,
161 namespace?: string
162 ): Field & Property & Struct {
163 const { responseKey, isConditional } = field;
164
165 const propertyName = isMetaFieldName(responseKey)
166 ? responseKey
167 : camelCase(responseKey);
168
169 const structName = join(
170 [namespace, this.structNameForPropertyName(responseKey)],
171 "."
172 );
173
174 let type = field.type;
175
176 if (isConditional && isNonNullType(type)) {
177 type = type.ofType;
178 }
179
180 const isOptional = !isNonNullType(type);
181
182 const unmodifiedType = getNamedType(field.type);
183
184 const unmodifiedTypeName = isCompositeType(unmodifiedType)
185 ? structName
186 : unmodifiedType.name;
187
188 const typeName = this.typeNameFromGraphQLType(type, unmodifiedTypeName);
189
190 return Object.assign({}, field, {
191 responseKey,
192 propertyName,
193 typeName,
194 structName,
195 isOptional,
196 });
197 }
198
199 propertyFromVariant(variant: Variant): Variant & Property & Struct {
200 const structName = this.structNameForVariant(variant);
201
202 return Object.assign(variant, {
203 propertyName: camelCase(structName),
204 typeName: structName + "?",
205 structName,
206 });
207 }
208
209 propertyFromFragmentSpread(
210 fragmentSpread: FragmentSpread,
211 isConditional: boolean
212 ): FragmentSpread & Property & Struct {
213 const structName = this.structNameForFragmentName(
214 fragmentSpread.fragmentName
215 );
216
217 return Object.assign({}, fragmentSpread, {
218 propertyName: camelCase(fragmentSpread.fragmentName),
219 typeName: isConditional ? structName + "?" : structName,
220 structName,
221 isConditional,
222 });
223 }
224
225 propertyFromInputField(field: GraphQLInputField) {
226 return Object.assign({}, field, {
227 propertyName: camelCase(field.name),
228 typeName: this.typeNameFromGraphQLType(field.type),
229 isOptional: !isNonNullType(field.type),
230 });
231 }
232
233 propertiesForSelectionSet(
234 selectionSet: SelectionSet,
235 namespace?: string
236 ): (Field & Property & Struct)[] | undefined {
237 const properties = collectAndMergeFields(selectionSet, true)
238 .filter((field) => field.name !== "__typename")
239 .map((field) => this.propertyFromField(field, namespace));
240
241 // If we're not merging in fields from fragment spreads, there is no guarantee there will a generated
242 // type for a composite field, so to avoid compiler errors we skip the initializer for now.
243 if (
244 selectionSet.selections.some(
245 (selection) => selection.kind === "FragmentSpread"
246 ) &&
247 properties.some((property) =>
248 isCompositeType(getNamedType(property.type))
249 )
250 ) {
251 return undefined;
252 }
253
254 return properties;
255 }
256
257 // Expressions
258
259 dictionaryLiteralForFieldArguments(args: Argument[]): SwiftSource {
260 function expressionFromValue(value: any): SwiftSource {
261 if (value === null) {
262 return swift`nil`;
263 } else if (value.kind === "Variable") {
264 return swift`GraphQLVariable(${SwiftSource.string(
265 value.variableName
266 )})`;
267 } else if (Array.isArray(value)) {
268 return (
269 SwiftSource.wrap(
270 swift`[`,
271 SwiftSource.join(value.map(expressionFromValue), ", "),
272 swift`]`
273 ) || swift`[]`
274 );
275 } else if (typeof value === "object") {
276 return (
277 SwiftSource.wrap(
278 swift`[`,
279 SwiftSource.join(
280 Object.entries(value).map(([key, value]) => {
281 return swift`${SwiftSource.string(key)}: ${expressionFromValue(
282 value
283 )}`;
284 }),
285 ", "
286 ),
287 swift`]`
288 ) || swift`[:]`
289 );
290 } else if (typeof value === "string") {
291 return SwiftSource.string(value);
292 } else {
293 return new SwiftSource(JSON.stringify(value));
294 }
295 }
296
297 return (
298 SwiftSource.wrap(
299 swift`[`,
300 SwiftSource.join(
301 args.map((arg) => {
302 return swift`${SwiftSource.string(arg.name)}: ${expressionFromValue(
303 arg.value
304 )}`;
305 }),
306 ", "
307 ),
308 swift`]`
309 ) || swift`[:]`
310 );
311 }
312
313 mapExpressionForType(
314 type: GraphQLType,
315 isConditional: boolean = false,
316 makeExpression: (expression: SwiftSource) => SwiftSource,
317 expression: SwiftSource,
318 inputTypeName: string,
319 outputTypeName: string
320 ): SwiftSource {
321 let isOptional;
322 if (isNonNullType(type)) {
323 isOptional = !!isConditional;
324 type = type.ofType;
325 } else {
326 isOptional = true;
327 }
328
329 if (isListType(type)) {
330 const elementType = type.ofType;
331 if (isOptional) {
332 return swift`${expression}.flatMap { ${makeClosureSignature(
333 this.typeNameFromGraphQLType(type, inputTypeName, false),
334 this.typeNameFromGraphQLType(type, outputTypeName, false)
335 )} value.map { ${makeClosureSignature(
336 this.typeNameFromGraphQLType(elementType, inputTypeName),
337 this.typeNameFromGraphQLType(elementType, outputTypeName)
338 )} ${this.mapExpressionForType(
339 elementType,
340 undefined,
341 makeExpression,
342 swift`value`,
343 inputTypeName,
344 outputTypeName
345 )} } }`;
346 } else {
347 return swift`${expression}.map { ${makeClosureSignature(
348 this.typeNameFromGraphQLType(elementType, inputTypeName),
349 this.typeNameFromGraphQLType(elementType, outputTypeName)
350 )} ${this.mapExpressionForType(
351 elementType,
352 undefined,
353 makeExpression,
354 swift`value`,
355 inputTypeName,
356 outputTypeName
357 )} }`;
358 }
359 } else if (isOptional) {
360 return swift`${expression}.flatMap { ${makeClosureSignature(
361 this.typeNameFromGraphQLType(type, inputTypeName, false),
362 this.typeNameFromGraphQLType(type, outputTypeName, false)
363 )} ${makeExpression(swift`value`)} }`;
364 } else {
365 return makeExpression(expression);
366 }
367 }
368}
369
370function makeClosureSignature(
371 parameterTypeName: string,
372 returnTypeName?: string
373): SwiftSource {
374 let closureSignature = swift`(value: ${parameterTypeName})`;
375
376 if (returnTypeName) {
377 closureSignature.append(swift` -> ${returnTypeName}`);
378 }
379 closureSignature.append(swift` in`);
380 return closureSignature;
381}
382
383/**
384 * Takes a proposed name and modifies it to be unique given a list of properties.
385 * @param proposedName The proposed name that shouldn't conflict with any property.
386 * @param properties A list of properties the name shouldn't conflict with.
387 * @returns A name based on `proposedName` that doesn't match any existing property name.
388 */
389function makeUniqueName(
390 proposedName: string,
391 properties: { propertyName: string }[]
392): string {
393 // Assume conflicts are very rare and property lists are short, and just do a linear search. If
394 // we find a conflict, start over with the modified name.
395 for (let name = proposedName; ; name += "_") {
396 if (properties.every((prop) => prop.propertyName != name)) {
397 return name;
398 }
399 }
400}
401
402/**
403 * Converts a value from "underscore_case" to "camelCase".
404 *
405 * This preserves any leading/trailing underscores.
406 */
407function camelCase(value: string): string {
408 const [_, prefix, middle, suffix] = value.match(/^(_*)(.*?)(_*)$/) || [
409 "",
410 "",
411 value,
412 "",
413 ];
414 return `${prefix}${_camelCase(middle)}${suffix}`;
415}
416
417/**
418 * Converts a value from "underscore_case" to "PascalCase".
419 *
420 * This preserves any leading/trailing underscores.
421 */
422function pascalCase(value: string): string {
423 const [_, prefix, middle, suffix] = value.match(/^(_*)(.*?)(_*)$/) || [
424 "",
425 "",
426 value,
427 "",
428 ];
429 return `${prefix}${_pascalCase(middle)}${suffix}`;
430}