diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d7da3aca9..6f7baabb25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ - `apollo-codegen-swift` - Fix issue where type names were not being properly escaped [iOS 193](https://github.com/apollographql/apollo-ios/issues/193) - Fix overcorrection on removing redundant modifiers [#1449](https://github.com/apollographql/apollo-tooling/issues/1449) + - Added `CaseIterable` conformance so all known cases can be easily iterated. + - Added comment to `operationDefinition` to show the original query + - Stripped excess whitespace out of `operationDefinition` + - Removed force-unwrap when the thing being unwrapped is a double optional - `apollo-codegen-typescript` - - `apollo-env` diff --git a/packages/apollo-codegen-swift/src/__tests__/__snapshots__/codeGeneration.ts.snap b/packages/apollo-codegen-swift/src/__tests__/__snapshots__/codeGeneration.ts.snap index 47d5942af3..82cf0fa397 100644 --- a/packages/apollo-codegen-swift/src/__tests__/__snapshots__/codeGeneration.ts.snap +++ b/packages/apollo-codegen-swift/src/__tests__/__snapshots__/codeGeneration.ts.snap @@ -2,8 +2,18 @@ exports[`Swift code generation #classDeclarationForOperation() should correctly escape a mutli-line string literal 1`] = ` "public final class CreateReviewMutation: GraphQLMutation { + /// mutation CreateReview($episode: Episode) { + /// createReview(episode: $episode, review: {stars: 5, commentary: \\"\\"\\" + /// Wow! + /// I thought + /// This movie ROCKED! + /// \\"\\"\\"}) { + /// stars + /// commentary + /// } + /// } public let operationDefinition = - \\"mutation CreateReview($episode: Episode) {\\\\n createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"\\\\\\"\\\\\\"\\\\n Wow!\\\\n \\\\n This movie ROCKED!\\\\n \\\\\\"\\\\\\"\\\\\\"}) {\\\\n stars\\\\n commentary\\\\n }\\\\n}\\" + \\"mutation CreateReview($episode: Episode) {\\\\n createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"\\\\\\"\\\\\\"\\\\n Wow!\\\\n I thought\\\\n This movie ROCKED!\\\\n \\\\\\"\\\\\\"\\\\\\"}) {\\\\n stars\\\\n commentary\\\\n }\\\\n}\\" public let operationName = \\"CreateReview\\" @@ -21,7 +31,7 @@ exports[`Swift code generation #classDeclarationForOperation() should correctly public static let possibleTypes = [\\"Mutation\\"] public static let selections: [GraphQLSelection] = [ - GraphQLField(\\"createReview\\", arguments: [\\"episode\\": GraphQLVariable(\\"episode\\"), \\"review\\": [\\"stars\\": 5, \\"commentary\\": \\"Wow!\\\\n\\\\n This movie ROCKED!\\"]], type: .object(CreateReview.selections)), + GraphQLField(\\"createReview\\", arguments: [\\"episode\\": GraphQLVariable(\\"episode\\"), \\"review\\": [\\"stars\\": 5, \\"commentary\\": \\"Wow!\\\\n I thought\\\\n This movie ROCKED!\\"]], type: .object(CreateReview.selections)), ] public private(set) var resultMap: ResultMap @@ -87,8 +97,18 @@ exports[`Swift code generation #classDeclarationForOperation() should correctly exports[`Swift code generation #classDeclarationForOperation() should correctly escape a mutli-line string literal with backslashes 1`] = ` "public final class CreateReviewMutation: GraphQLMutation { + /// mutation CreateReview($episode: Episode) { + /// createReview(episode: $episode, review: {stars: 5, commentary: \\"\\"\\" + /// Wow! + /// I thought + /// This movie \\\\ ROCKED! + /// \\"\\"\\"}) { + /// stars + /// commentary + /// } + /// } public let operationDefinition = - \\"mutation CreateReview($episode: Episode) {\\\\n createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"\\\\\\"\\\\\\"\\\\n Wow!\\\\n \\\\n This movie \\\\ ROCKED!\\\\n \\\\\\"\\\\\\"\\\\\\"}) {\\\\n stars\\\\n commentary\\\\n }\\\\n}\\" + \\"mutation CreateReview($episode: Episode) {\\\\n createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"\\\\\\"\\\\\\"\\\\n Wow!\\\\n I thought\\\\n This movie \\\\ ROCKED!\\\\n \\\\\\"\\\\\\"\\\\\\"}) {\\\\n stars\\\\n commentary\\\\n }\\\\n}\\" public let operationName = \\"CreateReview\\" @@ -106,7 +126,7 @@ exports[`Swift code generation #classDeclarationForOperation() should correctly public static let possibleTypes = [\\"Mutation\\"] public static let selections: [GraphQLSelection] = [ - GraphQLField(\\"createReview\\", arguments: [\\"episode\\": GraphQLVariable(\\"episode\\"), \\"review\\": [\\"stars\\": 5, \\"commentary\\": \\"Wow!\\\\n\\\\n This movie \\\\\\\\ ROCKED!\\"]], type: .object(CreateReview.selections)), + GraphQLField(\\"createReview\\", arguments: [\\"episode\\": GraphQLVariable(\\"episode\\"), \\"review\\": [\\"stars\\": 5, \\"commentary\\": \\"Wow!\\\\n I thought\\\\n This movie \\\\\\\\ ROCKED!\\"]], type: .object(CreateReview.selections)), ] public private(set) var resultMap: ResultMap @@ -172,8 +192,14 @@ exports[`Swift code generation #classDeclarationForOperation() should correctly exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration for a mutation with variables 1`] = ` "public final class CreateReviewMutation: GraphQLMutation { + /// mutation CreateReview($episode: Episode) { + /// createReview(episode: $episode, review: {stars: 5, commentary: \\"Wow!\\"}) { + /// stars + /// commentary + /// } + /// } public let operationDefinition = - \\"mutation CreateReview($episode: Episode) {\\\\n createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"Wow!\\\\\\"}) {\\\\n stars\\\\n commentary\\\\n }\\\\n}\\" + \\"mutation CreateReview($episode: Episode) { createReview(episode: $episode, review: {stars: 5, commentary: \\\\\\"Wow!\\\\\\"}) { stars commentary } }\\" public let operationName = \\"CreateReview\\" @@ -257,8 +283,15 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration for a query with a fragment spread nested in an inline fragment 1`] = ` "public final class HeroQuery: GraphQLQuery { + /// query Hero { + /// hero { + /// ... on Droid { + /// ...HeroDetails + /// } + /// } + /// } public let operationDefinition = - \\"query Hero {\\\\n hero {\\\\n ... on Droid {\\\\n ...HeroDetails\\\\n }\\\\n }\\\\n}\\" + \\"query Hero { hero { ... on Droid { ...HeroDetails } } }\\" public let operationName = \\"Hero\\" @@ -389,8 +422,13 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration for a query with conditional fragment spreads 1`] = ` "public final class HeroQuery: GraphQLQuery { + /// query Hero { + /// hero { + /// ...DroidDetails + /// } + /// } public let operationDefinition = - \\"query Hero {\\\\n hero {\\\\n ...DroidDetails\\\\n }\\\\n}\\" + \\"query Hero { hero { ...DroidDetails } }\\" public let operationName = \\"Hero\\" @@ -549,8 +587,13 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration for a query with fragment spreads 1`] = ` "public final class HeroQuery: GraphQLQuery { + /// query Hero { + /// hero { + /// ...HeroDetails + /// } + /// } public let operationDefinition = - \\"query Hero {\\\\n hero {\\\\n ...HeroDetails\\\\n }\\\\n}\\" + \\"query Hero { hero { ...HeroDetails } }\\" public let operationName = \\"Hero\\" @@ -648,8 +691,13 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration for a query with variables 1`] = ` "public final class HeroNameQuery: GraphQLQuery { + /// query HeroName($episode: Episode) { + /// hero(episode: $episode) { + /// name + /// } + /// } public let operationDefinition = - \\"query HeroName($episode: Episode) {\\\\n hero(episode: $episode) {\\\\n name\\\\n }\\\\n}\\" + \\"query HeroName($episode: Episode) { hero(episode: $episode) { name } }\\" public let operationName = \\"HeroName\\" @@ -726,8 +774,13 @@ exports[`Swift code generation #classDeclarationForOperation() should generate a exports[`Swift code generation #classDeclarationForOperation() should generate a class declaration with an operationIdentifier property when generateOperationIds is specified 1`] = ` "public final class HeroQuery: GraphQLQuery { + /// query Hero { + /// hero { + /// ...HeroDetails + /// } + /// } public let operationDefinition = - \\"query Hero {\\\\n hero {\\\\n ...HeroDetails\\\\n }\\\\n}\\" + \\"query Hero { hero { ...HeroDetails } }\\" public let operationName = \\"Hero\\" @@ -846,8 +899,12 @@ exports[`Swift code generation #initializerDeclarationForProperties() should gen exports[`Swift code generation #structDeclarationForFragment() should generate a struct declaration for a fragment that includes a fragment spread 1`] = ` "public struct HeroDetails: GraphQLFragment { + /// fragment HeroDetails on Character { + /// name + /// ...MoreHeroDetails + /// } public static let fragmentDefinition = - \\"fragment HeroDetails on Character {\\\\n name\\\\n ...MoreHeroDetails\\\\n}\\" + \\"fragment HeroDetails on Character { name ...MoreHeroDetails }\\" public static let possibleTypes = [\\"Human\\", \\"Droid\\"] @@ -920,8 +977,12 @@ exports[`Swift code generation #structDeclarationForFragment() should generate a exports[`Swift code generation #structDeclarationForFragment() should generate a struct declaration for a fragment with a concrete type condition 1`] = ` "public struct DroidDetails: GraphQLFragment { + /// fragment DroidDetails on Droid { + /// name + /// primaryFunction + /// } public static let fragmentDefinition = - \\"fragment DroidDetails on Droid {\\\\n name\\\\n primaryFunction\\\\n}\\" + \\"fragment DroidDetails on Droid { name primaryFunction }\\" public static let possibleTypes = [\\"Droid\\"] @@ -964,8 +1025,14 @@ exports[`Swift code generation #structDeclarationForFragment() should generate a exports[`Swift code generation #structDeclarationForFragment() should generate a struct declaration for a fragment with a subselection 1`] = ` "public struct HeroDetails: GraphQLFragment { + /// fragment HeroDetails on Character { + /// name + /// friends { + /// name + /// } + /// } public static let fragmentDefinition = - \\"fragment HeroDetails on Character {\\\\n name\\\\n friends {\\\\n name\\\\n }\\\\n}\\" + \\"fragment HeroDetails on Character { name friends { name } }\\" public static let possibleTypes = [\\"Human\\", \\"Droid\\"] @@ -1044,8 +1111,12 @@ exports[`Swift code generation #structDeclarationForFragment() should generate a exports[`Swift code generation #structDeclarationForFragment() should generate a struct declaration for a fragment with an abstract type condition 1`] = ` "public struct HeroDetails: GraphQLFragment { + /// fragment HeroDetails on Character { + /// name + /// appearsIn + /// } public static let fragmentDefinition = - \\"fragment HeroDetails on Character {\\\\n name\\\\n appearsIn\\\\n}\\" + \\"fragment HeroDetails on Character { name appearsIn }\\" public static let possibleTypes = [\\"Human\\", \\"Droid\\"] @@ -1649,7 +1720,7 @@ exports[`Swift code generation #structDeclarationForSelectionSet() should genera `; exports[`Swift code generation #typeDeclarationForGraphQLType() should escape identifiers in cases of enum declaration for a GraphQLEnumType 1`] = ` -"public enum AlbumPrivacies: RawRepresentable, Equatable, Hashable, Apollo.JSONDecodable, Apollo.JSONEncodable { +"public enum AlbumPrivacies: RawRepresentable, Equatable, Hashable, CaseIterable, Apollo.JSONDecodable, Apollo.JSONEncodable { public typealias RawValue = String case \`public\` case \`private\` @@ -1680,6 +1751,13 @@ exports[`Swift code generation #typeDeclarationForGraphQLType() should escape id default: return false } } + + public static var allCases: [AlbumPrivacies] { + return [ + .public, + .private, + ] + } }" `; @@ -1705,7 +1783,7 @@ public struct ReviewInput: GraphQLMapConvertible { /// Comment about the movie, optional public var commentary: Swift.Optional { get { - return graphQLMap[\\"commentary\\"] as! Swift.Optional + return graphQLMap[\\"commentary\\"] as? Swift.Optional ?? .none } set { graphQLMap.updateValue(newValue, forKey: \\"commentary\\") @@ -1715,7 +1793,7 @@ public struct ReviewInput: GraphQLMapConvertible { /// Favorite color, optional public var favoriteColor: Swift.Optional { get { - return graphQLMap[\\"favorite_color\\"] as! Swift.Optional + return graphQLMap[\\"favorite_color\\"] as? Swift.Optional ?? .none } set { graphQLMap.updateValue(newValue, forKey: \\"favorite_color\\") @@ -1726,7 +1804,7 @@ public struct ReviewInput: GraphQLMapConvertible { exports[`Swift code generation #typeDeclarationForGraphQLType() should generate an enum declaration for a GraphQLEnumType 1`] = ` "/// The episodes in the Star Wars trilogy -public enum Episode: RawRepresentable, Equatable, Hashable, Apollo.JSONDecodable, Apollo.JSONEncodable { +public enum Episode: RawRepresentable, Equatable, Hashable, CaseIterable, Apollo.JSONDecodable, Apollo.JSONEncodable { public typealias RawValue = String /// Star Wars Episode IV: A New Hope, released in 1977. case newhope @@ -1764,5 +1842,13 @@ public enum Episode: RawRepresentable, Equatable, Hashable, Apollo.JSONDecodable default: return false } } + + public static var allCases: [Episode] { + return [ + .newhope, + .empire, + .jedi, + ] + } }" `; diff --git a/packages/apollo-codegen-swift/src/__tests__/codeGeneration.ts b/packages/apollo-codegen-swift/src/__tests__/codeGeneration.ts index 4540c7d86b..f2362bed05 100644 --- a/packages/apollo-codegen-swift/src/__tests__/codeGeneration.ts +++ b/packages/apollo-codegen-swift/src/__tests__/codeGeneration.ts @@ -96,7 +96,7 @@ describe("Swift code generation", () => { createReview(episode: $episode, review: {stars: 5, commentary: """ Wow! - + I thought This movie ROCKED! """ }) { @@ -117,7 +117,7 @@ describe("Swift code generation", () => { createReview(episode: $episode, review: {stars: 5, commentary: """ Wow! - + I thought This movie \\ ROCKED! """ }) { diff --git a/packages/apollo-codegen-swift/src/codeGeneration.ts b/packages/apollo-codegen-swift/src/codeGeneration.ts index 1ce716dcb2..95d892b29c 100644 --- a/packages/apollo-codegen-swift/src/codeGeneration.ts +++ b/packages/apollo-codegen-swift/src/codeGeneration.ts @@ -202,6 +202,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { }, () => { if (source) { + this.commentWithoutTrimming(source); this.printOnNewline("public let operationDefinition ="); this.withIndent(() => { this.multilineString(source); @@ -315,6 +316,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { outputIndividualFiles, () => { if (source) { + this.commentWithoutTrimming(source); this.printOnNewline("public static let fragmentDefinition ="); this.withIndent(() => { this.multilineString(source); @@ -977,7 +979,7 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printNewlineIfNeeded(); this.comment(description || undefined); this.printOnNewline( - `public enum ${name}: RawRepresentable, Equatable, Hashable, Apollo.JSONDecodable, Apollo.JSONEncodable` + `public enum ${name}: RawRepresentable, Equatable, Hashable, CaseIterable, Apollo.JSONDecodable, Apollo.JSONEncodable` ); this.withinBlock(() => { this.printOnNewline("public typealias RawValue = String"); @@ -1049,6 +1051,21 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.printOnNewline(`default: return false`); }); }); + + this.printNewlineIfNeeded(); + this.printOnNewline(`public static var allCases: [${name}]`); + this.withinBlock(() => { + this.printOnNewline(`return [`); + values.forEach(value => { + const enumDotCaseName = escapeIdentifierIfNeeded( + this.helpers.enumDotCaseName(value.name) + ); + this.withIndent(() => { + this.printOnNewline(`${enumDotCaseName},`); + }); + }); + this.printOnNewline(`]`); + }); }); } @@ -1120,7 +1137,8 @@ export class SwiftAPIGenerator extends SwiftGenerator { name, propertyName, typeName, - description + description, + isOptional } of properties) { this.printNewlineIfNeeded(); this.comment(description || undefined); @@ -1130,9 +1148,15 @@ export class SwiftAPIGenerator extends SwiftGenerator { this.withinBlock(() => { this.printOnNewline("get"); this.withinBlock(() => { - this.printOnNewline( - `return graphQLMap["${name}"] as! ${typeName}` - ); + if (isOptional) { + this.printOnNewline( + `return graphQLMap["${name}"] as? ${typeName} ?? .none` + ); + } else { + this.printOnNewline( + `return graphQLMap["${name}"] as! ${typeName}` + ); + } }); this.printOnNewline("set"); this.withinBlock(() => { diff --git a/packages/apollo-codegen-swift/src/language.ts b/packages/apollo-codegen-swift/src/language.ts index 16924a3aab..3a9c5eb808 100644 --- a/packages/apollo-codegen-swift/src/language.ts +++ b/packages/apollo-codegen-swift/src/language.ts @@ -29,7 +29,18 @@ export interface Property { } export function escapedString(string: string) { - return string.replace(/"/g, '\\"').replace(/\n/g, "\\n"); + if (string.includes('"""')) { + // This includes a multi-line string literal, and we may strip out meaningful + // whitespace if we try to strip whitespace. Don't try. + return string.replace(/"/g, '\\"').replace(/\n/g, "\\n"); + } else { + // Strip unnecessary whitespace. + return string + .split(/\n/g) + .map(line => line.trim()) + .map(line => line.replace(/"/g, '\\"')) + .join(" "); + } } // prettier-ignore @@ -72,6 +83,13 @@ export class SwiftGenerator extends CodeGenerator< }); } + commentWithoutTrimming(comment?: string) { + comment && + comment.split("\n").forEach(line => { + this.printOnNewline(`/// ${line}`); + }); + } + deprecationAttributes( isDeprecated: boolean | undefined, deprecationReason: string | undefined diff --git a/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap b/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap index 2a58bedc66..9d87e0e198 100644 --- a/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap +++ b/packages/apollo/src/commands/client/__tests__/__snapshots__/generate.test.ts.snap @@ -176,8 +176,11 @@ exports[`client:codegen writes swift types from local schema 1`] = ` import Apollo public final class SimpleQueryQuery: GraphQLQuery { + /// query SimpleQuery { + /// hello + /// } public let operationDefinition = - \\"query SimpleQuery {\\\\n hello\\\\n}\\" + \\"query SimpleQuery { hello }\\" public let operationName = \\"SimpleQuery\\" @@ -220,8 +223,11 @@ exports[`client:codegen writes swift types from local schema in a graphql file 1 import Apollo public final class SimpleQueryQuery: GraphQLQuery { + /// query SimpleQuery { + /// hello + /// } public let operationDefinition = - \\"query SimpleQuery {\\\\n hello\\\\n}\\" + \\"query SimpleQuery { hello }\\" public let operationName = \\"SimpleQuery\\"