apollo-codegen-swift
Version:
Swift generator module for Apollo Codegen
990 lines (851 loc) • 27.7 kB
text/typescript
import {
buildSchema,
parse,
GraphQLNonNull,
GraphQLString,
GraphQLEnumType,
GraphQLList,
} from "graphql";
import { loadSchema } from "apollo-codegen-core/lib/loading";
const schema = loadSchema(
require.resolve("../../../../__fixtures__/starwars/schema.json")
);
import {
compileToIR,
CompilerOptions,
CompilerContext,
SelectionSet,
Field,
Argument,
} from "apollo-codegen-core/lib/compiler";
import { SwiftAPIGenerator } from "../codeGeneration";
describe("Swift code generation", () => {
let generator: SwiftAPIGenerator;
beforeEach(() => {
generator = new SwiftAPIGenerator({});
});
function compile(
source: string,
options: CompilerOptions = {
mergeInFieldsFromFragmentSpreads: true,
omitDeprecatedEnumCases: false,
}
): CompilerContext {
const document = parse(source);
const context = compileToIR(schema, document, options);
generator.context = context;
return context;
}
describe("#classDeclarationForOperation()", () => {
it(`should generate a class declaration for a query with variables`, () => {
const { operations } = compile(`
query HeroName($episode: Episode) {
hero(episode: $episode) {
name
}
}
`);
generator.classDeclarationForOperation(
operations["HeroName"],
false,
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a class declaration for a query with fragment spreads`, () => {
const { operations } = compile(`
query Hero {
hero {
...HeroDetails
}
}
fragment HeroDetails on Character {
name
}
`);
generator.classDeclarationForOperation(operations["Hero"], false, false);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a class declaration for a query with conditional fragment spreads`, () => {
const { operations } = compile(`
query Hero {
hero {
...DroidDetails
}
}
fragment DroidDetails on Droid {
primaryFunction
}
`);
generator.classDeclarationForOperation(operations["Hero"], false, false);
expect(generator.output).toMatchSnapshot();
});
it("should correctly escape a mutli-line string literal", () => {
const { operations } = compile(`
mutation CreateReview($episode: Episode) {
createReview(episode: $episode, review: {stars: 5, commentary:
"""
Wow!
I thought
This movie ROCKED!
"""
}) {
stars
commentary
}
}
`);
generator.classDeclarationForOperation(operations["CreateReview"]);
expect(generator.output).toMatchSnapshot();
});
it("should correctly escape a mutli-line string literal with backslashes", () => {
const { operations } = compile(`
mutation CreateReview($episode: Episode) {
createReview(episode: $episode, review: {stars: 5, commentary:
"""
Wow!
I thought
This movie \\ ROCKED!
"""
}) {
stars
commentary
}
}
`);
generator.classDeclarationForOperation(
operations["CreateReview"],
false,
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a class declaration for a query with a fragment spread nested in an inline fragment`, () => {
const { operations } = compile(`
query Hero {
hero {
... on Droid {
...HeroDetails
}
}
}
fragment HeroDetails on Character {
name
}
`);
generator.classDeclarationForOperation(operations["Hero"], false, false);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a class declaration for a mutation with variables`, () => {
const { operations } = compile(`
mutation CreateReview($episode: Episode) {
createReview(episode: $episode, review: { stars: 5, commentary: "Wow!" }) {
stars
commentary
}
}
`);
generator.classDeclarationForOperation(operations["CreateReview"]);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a class declaration with an operationIdentifier property when generateOperationIds is specified`, () => {
const { operations } = compile(
`
query Hero {
hero {
...HeroDetails
}
}
fragment HeroDetails on Character {
name
}
`,
{
generateOperationIds: true,
mergeInFieldsFromFragmentSpreads: true,
omitDeprecatedEnumCases: false,
}
);
generator.classDeclarationForOperation(operations["Hero"], false, false);
expect(generator.output).toMatchSnapshot();
});
});
describe("#initializerDeclarationForProperties()", () => {
it(`should generate initializer for a property`, () => {
generator.initializerDeclarationForProperties([
{ propertyName: "episode", typeName: "Episode" },
]);
expect(generator.output).toMatchSnapshot();
});
it(`should generate initializer for an optional property`, () => {
generator.initializerDeclarationForProperties([
{ propertyName: "episode", typeName: "Episode?", isOptional: true },
]);
expect(generator.output).toMatchSnapshot();
});
it(`should generate initializer for multiple properties`, () => {
generator.initializerDeclarationForProperties([
{ propertyName: "episode", typeName: "Episode?", isOptional: true },
{ propertyName: "scene", typeName: "String?", isOptional: true },
]);
expect(generator.output).toMatchSnapshot();
});
});
describe("#propertyAssignmentForField()", () => {
it("should generate expression for nullable scalar", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: GraphQLString,
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for non-null scalar", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(GraphQLString),
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for nullable list of nullable scalars", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLList(GraphQLString),
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for nullable list of non-null scalars", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLList(new GraphQLNonNull(GraphQLString)),
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for non-null list of nullable scalars", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(new GraphQLList(GraphQLString)),
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for non-null list of non-null scalars", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(GraphQLString))
),
}).source
).toBe('"response_key": propertyName');
});
it("should generate expression for nullable composite", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: schema.getType("Droid"),
}).source
).toBe(
'"response_key": propertyName.flatMap { (value: Droid) -> ResultMap in value.resultMap }'
);
});
it("should generate expression for non-null composite", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(schema.getType("Droid")),
}).source
).toBe('"response_key": propertyName.resultMap');
});
it("should generate expression for nullable list of nullable composites", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLList(schema.getType("Droid")),
}).source
).toBe(
'"response_key": propertyName.flatMap { (value: [Droid?]) -> [ResultMap?] in value.map { (value: Droid?) -> ResultMap? in value.flatMap { (value: Droid) -> ResultMap in value.resultMap } } }'
);
});
it("should generate expression for nullable list of non-null composites", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLList(new GraphQLNonNull(schema.getType("Droid"))),
}).source
).toBe(
'"response_key": propertyName.flatMap { (value: [Droid]) -> [ResultMap] in value.map { (value: Droid) -> ResultMap in value.resultMap } }'
);
});
it("should generate expression for non-null list of nullable composites", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(new GraphQLList(schema.getType("Droid"))),
}).source
).toBe(
'"response_key": propertyName.map { (value: Droid?) -> ResultMap? in value.flatMap { (value: Droid) -> ResultMap in value.resultMap } }'
);
});
it("should generate expression for non-null list of non-null composites", () => {
expect(
generator.propertyAssignmentForField({
responseKey: "response_key",
propertyName: "propertyName",
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(schema.getType("Droid")))
),
}).source
).toBe(
'"response_key": propertyName.map { (value: Droid) -> ResultMap in value.resultMap }'
);
});
});
describe("#propertyDeclarationForField()", () => {
it(`should generate structName as testCTum for key testCTA`, () => {
// The existing schemas don't contain any outputs with fields ending in a series of caps,
// while also being a composite type.
const schema = buildSchema(`
schema {
query: Query
}
type Query {
foo(input: FooInput!): FooOutput
}
input FooInput {
id: ID
}
type FooOutput {
testCTA: Link
}
union Link = InternalLink | ExternalLink
type InternalLink {
path: String
}
type ExternalLink {
url: String
}
`);
const document = parse(`
query Test {
foo(input: {}) {
testCTA {
... on InternalLink {
path
}
... on ExternalLink {
url
}
}
}
}
`);
const context = compileToIR(schema, document);
generator.context = context;
const { operations, typesUsed } = context;
const outputField = operations["Test"].selectionSet
.selections[0] as Field;
generator.propertyDeclarationForField(
outputField.selectionSet.selections[0] as Field & Property
);
expect(generator.output).toMatchSnapshot();
});
});
describe("#structDeclarationForFragment()", () => {
it(`should generate a struct declaration for a fragment with an abstract type condition`, () => {
const { fragments } = compile(`
fragment HeroDetails on Character {
name
appearsIn
}
`);
generator.structDeclarationForFragment(fragments["HeroDetails"], false);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a fragment with a concrete type condition`, () => {
const { fragments } = compile(`
fragment DroidDetails on Droid {
name
primaryFunction
}
`);
generator.structDeclarationForFragment(
fragments["DroidDetails"],
false,
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a fragment with a subselection`, () => {
const { fragments } = compile(`
fragment HeroDetails on Character {
name
friends {
name
}
}
`);
generator.structDeclarationForFragment(
fragments["HeroDetails"],
false,
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a fragment that includes a fragment spread`, () => {
const { fragments } = compile(`
fragment HeroDetails on Character {
name
...MoreHeroDetails
}
fragment MoreHeroDetails on Character {
appearsIn
}
`);
generator.structDeclarationForFragment(
fragments["HeroDetails"],
false,
false
);
expect(generator.output).toMatchSnapshot();
});
});
describe("#structDeclarationForSelectionSet()", () => {
it(`should generate a struct declaration for a selection set`, () => {
const { operations } = compile(`
query Hero {
hero {
name
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should preserve leading and trailing underscores on fields`, () => {
const { operations } = compile(`
query Hero {
hero {
_name: name
_camel_case_id__: id
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should escape reserved keywords in a struct declaration for a selection set`, () => {
const { operations } = compile(`
query Hero {
hero {
private: name
self: friends {
id
}
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should escape init specially in a struct declaration initializer for a selection set`, () => {
const { operations } = compile(`
query Humans {
human(id: 0) {
self: friends {
id
}
}
human(id: 1) {
self: friends {
id
}
_self: name
}
}
`);
const human0 = (operations["Humans"].selectionSet.selections[0] as Field)
.selectionSet as SelectionSet;
const human1 = (operations["Humans"].selectionSet.selections[1] as Field)
.selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Human",
selectionSet: human0,
},
false
);
generator.structDeclarationForSelectionSet(
{
structName: "Human",
selectionSet: human1,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a nested struct declaration for a selection set with subselections`, () => {
const { operations } = compile(`
query Hero {
hero {
friends {
name
}
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a selection set with a fragment spread that matches the parent type`, () => {
const { operations } = compile(`
query Hero {
hero {
name
...HeroDetails
}
}
fragment HeroDetails on Character {
name
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a selection set with a fragment spread with a more specific type condition`, () => {
const { operations } = compile(`
query Hero {
hero {
name
...DroidDetails
}
}
fragment DroidDetails on Droid {
name
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a selection set with an inline fragment`, () => {
const { operations } = compile(`
query Hero {
hero {
name
... on Droid {
primaryFunction
}
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a fragment spread nested in an inline fragment`, () => {
const { operations } = compile(`
query Hero {
hero {
name
... on Droid {
...HeroDetails
}
}
}
fragment HeroDetails on Character {
name
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
it(`should generate a struct declaration for a selection set with a conditional field`, () => {
const { operations } = compile(`
query Hero($includeName: Boolean!) {
hero {
name @include(if: $includeName)
}
}
`);
const selectionSet = (
operations["Hero"].selectionSet.selections[0] as Field
).selectionSet as SelectionSet;
generator.structDeclarationForSelectionSet(
{
structName: "Hero",
selectionSet,
},
false
);
expect(generator.output).toMatchSnapshot();
});
});
describe("#typeDeclarationForGraphQLType()", () => {
it("should generate an enum declaration for a GraphQLEnumType", () => {
generator.typeDeclarationForGraphQLType(schema.getType("Episode"), false);
expect(generator.output).toMatchSnapshot();
});
it("should escape identifiers in cases of enum declaration for a GraphQLEnumType", () => {
const albumPrivaciesEnum = new GraphQLEnumType({
name: "AlbumPrivacies",
values: { PUBLIC: { value: "PUBLIC" }, PRIVATE: { value: "PRIVATE" } },
});
generator.typeDeclarationForGraphQLType(albumPrivaciesEnum, false);
expect(generator.output).toMatchSnapshot();
});
it("should omit deprecated cases from an enum declaration for a GraphQLEnumType", () => {
const { operations } = compile(
`
query Starship {
starship(id: 1) {
length(unit: METER)
}
}
`,
{
generateOperationIds: true,
mergeInFieldsFromFragmentSpreads: true,
omitDeprecatedEnumCases: true,
}
);
let starship = operations["Starship"].selectionSet.selections[0] as Field;
let starshipLength = starship.selectionSet.selections[0] as Field;
let lengthUnitArg = starshipLength.args[0].type;
generator.typeDeclarationForGraphQLType(lengthUnitArg, false);
expect(generator.output).toMatchSnapshot();
});
it("should include deprecated cases in an enum declaration for a GraphQLEnumType", () => {
const { operations } = compile(`
query Starship {
starship(id: 1) {
length(unit: METER)
}
}
`);
let starship = operations["Starship"].selectionSet.selections[0] as Field;
let starshipLength = starship.selectionSet.selections[0] as Field;
let lengthUnitArg = starshipLength.args[0].type;
generator.typeDeclarationForGraphQLType(lengthUnitArg, false);
expect(generator.output).toMatchSnapshot();
});
it("should generate a struct declaration for a GraphQLInputObjectType", () => {
generator.typeDeclarationForGraphQLType(
schema.getType("ReviewInput"),
false
);
expect(generator.output).toMatchSnapshot();
});
});
describe("#dictionaryLiteralForFieldArguments()", () => {
it("should include expressions for input objects with variables", () => {
const { operations } = compile(`
mutation FieldArgumentsWithInputObjects($commentary: String!, $red: Int!) {
createReview(episode: JEDI, review: { stars: 2, commentary: $commentary, favorite_color: { red: $red, blue: 100, green: 50 } }) {
commentary
}
}
`);
const fieldArguments = (
operations["FieldArgumentsWithInputObjects"].selectionSet
.selections[0] as Field
).args as Argument[];
const dictionaryLiteral =
generator.helpers.dictionaryLiteralForFieldArguments(
fieldArguments
).source;
expect(dictionaryLiteral).toBe(
'["episode": "JEDI", "review": ["stars": 2, "commentary": GraphQLVariable("commentary"), "favorite_color": ["red": GraphQLVariable("red"), "blue": 100, "green": 50]]]'
);
});
it("should handle empty input objects", () => {
// The existing schemas don't contain any input objects with all nullable types.
// Extending the schema in a call to `compile` doesn't seem to work.
// So instead we'll just build our own.
const schema = buildSchema(`
schema {
query: Query
}
type Query {
foo(input: FooInput!): Int
}
input FooInput {
id: ID
}
`);
const document = parse(`
query FieldArgumentsWithEmptyInputObject {
foo(input: {}) {
id
}
}
`);
const context = compileToIR(schema, document);
generator.context = context;
const { operations } = context;
const fieldArguments = (
operations["FieldArgumentsWithEmptyInputObject"].selectionSet
.selections[0] as Field
).args as Argument[];
const dictionaryLiteral =
generator.helpers.dictionaryLiteralForFieldArguments(
fieldArguments
).source;
expect(dictionaryLiteral).toBe('["input": [:]]');
});
it("should handle empty input arrays", () => {
// As with the previous test, we need to build our own schema.
const schema = buildSchema(`
schema {
query: Query
}
type Query {
foo(input: [Int!]!): Int
}
`);
const document = parse(`
query FieldArgumentsWithEmptyInputArray {
foo(input: []) {
id
}
}
`);
const context = compileToIR(schema, document);
generator.context = context;
const { operations } = context;
const fieldArguments = (
operations["FieldArgumentsWithEmptyInputArray"].selectionSet
.selections[0] as Field
).args as Argument[];
const dictionaryLiteral =
generator.helpers.dictionaryLiteralForFieldArguments(
fieldArguments
).source;
expect(dictionaryLiteral).toBe('["input": []]');
});
it("should handle input fields of various scalar types including null", () => {
// As with the previous test, we need to build our own schema.
const schema = buildSchema(`
schema {
query: Query
}
type Query {
foo(input: FooInput!): Int
}
input FooInput {
id: ID
id2: ID
name: String
age: Int
rating: Float
bool: Boolean
}
`);
const document = parse(`
query FieldArgumentsWithVariousScalars {
foo(input: { id: null, id2: "4", name: "Anne", age: 27, rating: 4.7, bool: true }) {
id
}
}
`);
const context = compileToIR(schema, document);
generator.context = context;
const { operations } = context;
const fieldArguments = (
operations["FieldArgumentsWithVariousScalars"].selectionSet
.selections[0] as Field
).args as Argument[];
const dictionaryLiteral =
generator.helpers.dictionaryLiteralForFieldArguments(
fieldArguments
).source;
expect(dictionaryLiteral).toBe(
'["input": ["id": nil, "id2": "4", "name": "Anne", "age": 27, "rating": 4.7, "bool": true]]'
);
});
});
});