import { GraphQLType, GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID, GraphQLScalarType, isCompositeType, getNamedType, GraphQLInputField, isNonNullType, isListType, isScalarType, isEnumType } from "graphql"; import { camelCase as _camelCase, pascalCase as _pascalCase } from "change-case"; import * as Inflector from "inflected"; import { join, wrap } from "apollo-codegen-core/lib/utilities/printing"; import { Property, Struct, SwiftSource, swift } from "./language"; import { CompilerOptions, SelectionSet, Field, FragmentSpread, Argument } from "apollo-codegen-core/lib/compiler"; import { isMetaFieldName } from "apollo-codegen-core/lib/utilities/graphql"; import { Variant } from "apollo-codegen-core/lib/compiler/visitors/typeCase"; import { collectAndMergeFields } from "apollo-codegen-core/lib/compiler/visitors/collectAndMergeFields"; // In this file, most functions work with strings, but anything that takes or receives an // expression uses `SwiftSource`. This way types and names stay represented as strings for as long as // possible. const builtInScalarMap = { [GraphQLString.name]: "String", [GraphQLInt.name]: "Int", [GraphQLFloat.name]: "Double", [GraphQLBoolean.name]: "Bool", [GraphQLID.name]: "GraphQLID" }; export class Helpers { constructor(public options: CompilerOptions) {} // Types typeNameFromGraphQLType( type: GraphQLType, unmodifiedTypeName?: string, isOptional?: boolean ): string { if (isNonNullType(type)) { return this.typeNameFromGraphQLType( type.ofType, unmodifiedTypeName, false ); } else if (isOptional === undefined) { isOptional = true; } let typeName; if (isListType(type)) { typeName = "[" + this.typeNameFromGraphQLType(type.ofType, unmodifiedTypeName) + "]"; } else if (isScalarType(type)) { typeName = this.typeNameForScalarType(type); } else { typeName = unmodifiedTypeName || type.name; } return isOptional ? typeName + "?" : typeName; } typeNameForScalarType(type: GraphQLScalarType): string { return ( builtInScalarMap[type.name] || (this.options.passthroughCustomScalars ? this.options.customScalarsPrefix + type.name : GraphQLString.name) ); } fieldTypeEnum(type: GraphQLType, structName: string): SwiftSource { if (isNonNullType(type)) { return swift`.nonNull(${this.fieldTypeEnum(type.ofType, structName)})`; } else if (isListType(type)) { return swift`.list(${this.fieldTypeEnum(type.ofType, structName)})`; } else if (isScalarType(type)) { return swift`.scalar(${this.typeNameForScalarType(type)}.self)`; } else if (isEnumType(type)) { return swift`.scalar(${type.name}.self)`; } else if (isCompositeType(type)) { return swift`.object(${structName}.selections)`; } else { throw new Error(`Unknown field type: ${type}`); } } // Names enumCaseName(name: string) { return camelCase(name); } enumDotCaseName(name: string): SwiftSource { return swift`.${SwiftSource.memberName(camelCase(name))}`; } operationClassName(name: string) { return pascalCase(name); } structNameForPropertyName(propertyName: string) { return pascalCase(Inflector.singularize(propertyName)); } structNameForFragmentName(fragmentName: string) { return pascalCase(fragmentName); } structNameForVariant(variant: SelectionSet) { return ( "As" + variant.possibleTypes.map(type => pascalCase(type.name)).join("Or") ); } /** * Returns the internal parameter name for a given property name. * * If the property name is valid to use, it's returned directly. Otherwise it's prefixed with an * underscore and modified until it's unique among the given property set. * @param propertyName The name of the property. * @param properties A list of properties that should be consulted when producing a unique name. * @returns The name to use for the internal parameter name for the property. */ internalParameterName( propertyName: string, properties: { propertyName: string }[] ): string { return SwiftSource.isValidParameterName(propertyName) ? propertyName : makeUniqueName(`_${propertyName}`, properties); } // Properties propertyFromField( field: Field, namespace?: string ): Field & Property & Struct { const { responseKey, isConditional } = field; const propertyName = isMetaFieldName(responseKey) ? responseKey : camelCase(responseKey); const structName = join( [namespace, this.structNameForPropertyName(responseKey)], "." ); let type = field.type; if (isConditional && isNonNullType(type)) { type = type.ofType; } const isOptional = !isNonNullType(type); const unmodifiedType = getNamedType(field.type); const unmodifiedTypeName = isCompositeType(unmodifiedType) ? structName : unmodifiedType.name; const typeName = this.typeNameFromGraphQLType(type, unmodifiedTypeName); return Object.assign({}, field, { responseKey, propertyName, typeName, structName, isOptional }); } propertyFromVariant(variant: Variant): Variant & Property & Struct { const structName = this.structNameForVariant(variant); return Object.assign(variant, { propertyName: camelCase(structName), typeName: structName + "?", structName }); } propertyFromFragmentSpread( fragmentSpread: FragmentSpread, isConditional: boolean ): FragmentSpread & Property & Struct { const structName = this.structNameForFragmentName( fragmentSpread.fragmentName ); return Object.assign({}, fragmentSpread, { propertyName: camelCase(fragmentSpread.fragmentName), typeName: isConditional ? structName + "?" : structName, structName, isConditional }); } propertyFromInputField(field: GraphQLInputField) { return Object.assign({}, field, { propertyName: camelCase(field.name), typeName: this.typeNameFromGraphQLType(field.type), isOptional: !isNonNullType(field.type) }); } propertiesForSelectionSet( selectionSet: SelectionSet, namespace?: string ): (Field & Property & Struct)[] | undefined { const properties = collectAndMergeFields(selectionSet, true) .filter(field => field.name !== "__typename") .map(field => this.propertyFromField(field, namespace)); // If we're not merging in fields from fragment spreads, there is no guarantee there will a generated // type for a composite field, so to avoid compiler errors we skip the initializer for now. if ( selectionSet.selections.some( selection => selection.kind === "FragmentSpread" ) && properties.some(property => isCompositeType(getNamedType(property.type))) ) { return undefined; } return properties; } // Expressions dictionaryLiteralForFieldArguments(args: Argument[]): SwiftSource { function expressionFromValue(value: any): SwiftSource { if (value === null) { return swift`nil`; } else if (value.kind === "Variable") { return swift`GraphQLVariable(${SwiftSource.string( value.variableName )})`; } else if (Array.isArray(value)) { return ( SwiftSource.wrap( swift`[`, SwiftSource.join(value.map(expressionFromValue), ", "), swift`]` ) || swift`[]` ); } else if (typeof value === "object") { return ( SwiftSource.wrap( swift`[`, SwiftSource.join( Object.entries(value).map(([key, value]) => { return swift`${SwiftSource.string(key)}: ${expressionFromValue( value )}`; }), ", " ), swift`]` ) || swift`[:]` ); } else if (typeof value === "string") { return SwiftSource.string(value); } else { return new SwiftSource(JSON.stringify(value)); } } return ( SwiftSource.wrap( swift`[`, SwiftSource.join( args.map(arg => { return swift`${SwiftSource.string(arg.name)}: ${expressionFromValue( arg.value )}`; }), ", " ), swift`]` ) || swift`[:]` ); } mapExpressionForType( type: GraphQLType, isConditional: boolean = false, makeExpression: (expression: SwiftSource) => SwiftSource, expression: SwiftSource, inputTypeName: string, outputTypeName: string ): SwiftSource { let isOptional; if (isNonNullType(type)) { isOptional = !!isConditional; type = type.ofType; } else { isOptional = true; } if (isListType(type)) { const elementType = type.ofType; if (isOptional) { return swift`${expression}.flatMap { ${makeClosureSignature( this.typeNameFromGraphQLType(type, inputTypeName, false), this.typeNameFromGraphQLType(type, outputTypeName, false) )} value.map { ${makeClosureSignature( this.typeNameFromGraphQLType(elementType, inputTypeName), this.typeNameFromGraphQLType(elementType, outputTypeName) )} ${this.mapExpressionForType( elementType, undefined, makeExpression, swift`value`, inputTypeName, outputTypeName )} } }`; } else { return swift`${expression}.map { ${makeClosureSignature( this.typeNameFromGraphQLType(elementType, inputTypeName), this.typeNameFromGraphQLType(elementType, outputTypeName) )} ${this.mapExpressionForType( elementType, undefined, makeExpression, swift`value`, inputTypeName, outputTypeName )} }`; } } else if (isOptional) { return swift`${expression}.flatMap { ${makeClosureSignature( this.typeNameFromGraphQLType(type, inputTypeName, false), this.typeNameFromGraphQLType(type, outputTypeName, false) )} ${makeExpression(swift`value`)} }`; } else { return makeExpression(expression); } } } function makeClosureSignature( parameterTypeName: string, returnTypeName?: string ): SwiftSource { let closureSignature = swift`(value: ${parameterTypeName})`; if (returnTypeName) { closureSignature.append(swift` -> ${returnTypeName}`); } closureSignature.append(swift` in`); return closureSignature; } /** * Takes a proposed name and modifies it to be unique given a list of properties. * @param proposedName The proposed name that shouldn't conflict with any property. * @param properties A list of properties the name shouldn't conflict with. * @returns A name based on `proposedName` that doesn't match any existing property name. */ function makeUniqueName( proposedName: string, properties: { propertyName: string }[] ): string { // Assume conflicts are very rare and property lists are short, and just do a linear search. If // we find a conflict, start over with the modified name. for (let name = proposedName; ; name += "_") { if (properties.every(prop => prop.propertyName != name)) { return name; } } } /** * Converts a value from "underscore_case" to "camelCase". * * This preserves any leading/trailing underscores. */ function camelCase(value: string): string { const [_, prefix, middle, suffix] = value.match(/^(_*)(.*?)(_*)$/) || [ "", "", value, "" ]; return `${prefix}${_camelCase(middle)}${suffix}`; } /** * Converts a value from "underscore_case" to "PascalCase". * * This preserves any leading/trailing underscores. */ function pascalCase(value: string): string { const [_, prefix, middle, suffix] = value.match(/^(_*)(.*?)(_*)$/) || [ "", "", value, "" ]; return `${prefix}${_pascalCase(middle)}${suffix}`; }