This issue will serve as the official Request for Comment for the Swift Codegen project.
This project is to take our existing code generation, which is done in Typescript, and move it to Swift.
Note that parsing of the Schema and parsing/validation of queries will still be taking place using our Typescript tooling - these are presently too tied to JS to be able to be replaced without a very, very large amount of work.
Moving the actual generation of code to Swift will allow Swift developers to more easily understand how code is being generated, and more easily contribute fixes and improvements to the code generation process.
This project will take place in several phases, which are outlined below.
Phase 0: Generate code with swift Script instead of bash
Swift scripts can be run directly from the command line, and these scripts can also import frameworks.
Everything we are currently using bash for will be updated to use Swift wrappers in a framework which will be included with the core Apollo library. This includes:
- Downloading and validating the JS tooling (abstracted away from the user, but still tested)
- Running a command with the Apollo CLI
- Downloading a schema
- Running code generation
This will be considerably easier to test than using random bash scripts, and those tests will help ensure that any changes are non-breaking.
The initial framework and tests are already up as a draft PR if you're interested in looking at the details.
The other thing this will do is make it considerably easier to add more flags in the future, specifically the flag to enable the Swift-based code generation rather than the Typescript-based code generation.
This phase will require significant updates to documentation since everything assumes the use of bash right now.
Phase 1: New Codegen
This is going to involve a few sub-phases.
Getting the Abstract Syntax Tree out of our existing codegen in JSON format
This is the part that depends most on our tooling repo. This should basically be done, but this could potentially be a blocker if there are pieces that we need to generate Swift code that's clearer and harder to fail at.
Parsing the AST into Swift Objects for code generation
In theory, this part should be the most straightforward, and will mostly involve parsing everything in the JSON into Codable objects which will ultimately be used by the code generation engine.
Generating code
This will involve a number of new goodies:
Codable conformance instead of custom JSON parsing
- Automatic
Equatable and Hashable conformance
- Addition of an option to automatically add
Identifiable conformance when a GraphQLID is one of the returned fields.
- Union types will be represented by
String-backed enums to make it easier to switch on the underlying type and to pull out the types being generated
- All generated code will have initializers to allow for easier testing.
- Fragments will, wherever possible, be represented as protocols, to allow for much easier access to their contents.
- Killing off double-optionals in input objects in favor of a
GraphQLOptional type which better represents the potential states of items which can be sent to the server.
I've stubbed all this out manually using our Star Wars test harness, so I wanted to share a few examples of what the differences in generated code will be (Hidden behind flippy triangles because it's loooooong).
This code is provided for feedback purposes and is not meant to represent the final version of things.
Basic query
Before
public final class HeroNameWithIdQuery: GraphQLQuery {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
query HeroNameWithID($episode: Episode) {
hero(episode: $episode) {
__typename
id
name
}
}
"""
public let operationName = "HeroNameWithID"
public let operationIdentifier: String? = "83c03f612c46fca72f6cb902df267c57bffc9209bc44dd87d2524fb2b34f6f18"
public var episode: Episode?
public init(episode: Episode? = nil) {
self.episode = episode
}
public var variables: GraphQLMap? {
return ["episode": episode]
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = ["Query"]
public static let selections: [GraphQLSelection] = [
GraphQLField("hero", arguments: ["episode": GraphQLVariable("episode")], type: .object(Hero.selections)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(hero: Hero? = nil) {
self.init(unsafeResultMap: ["__typename": "Query", "hero": hero.flatMap { (value: Hero) -> ResultMap in value.resultMap }])
}
public var hero: Hero? {
get {
return (resultMap["hero"] as? ResultMap).flatMap { Hero(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "hero")
}
}
public struct Hero: GraphQLSelectionSet {
public static let possibleTypes = ["Human", "Droid"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("id", type: .nonNull(.scalar(GraphQLID.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public static func makeHuman(id: GraphQLID, name: String) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Human", "id": id, "name": name])
}
public static func makeDroid(id: GraphQLID, name: String) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Droid", "id": id, "name": name])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// The ID of the character
public var id: GraphQLID {
get {
return resultMap["id"]! as! GraphQLID
}
set {
resultMap.updateValue(newValue, forKey: "id")
}
}
/// The name of the character
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
}
}
}
After
public final class HeroNameWithIdQueryMk2: GraphQLQueryMk2, Codable {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
query HeroNameWithID($episode: Episode) {
hero(episode: $episode) {
__typename
id
name
}
}
"""
public let operationName = "HeroNameWithID"
public let operationIdentifier: String? = "83c03f612c46fca72f6cb902df267c57bffc9209bc44dd87d2524fb2b34f6f18"
public var episode: GraphQLOptional<EpisodeMk2>
public init(episode: GraphQLOptional<EpisodeMk2>) {
self.episode = episode
}
public enum CodingKeys: String, CodingKey {
case episode
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeGraphQLOptional(self.episode, forKey: .episode)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.episode = try container.decodeGraphQLOptional(forKey: .episode)
}
public struct ResponseData: Codable, Equatable, Hashable {
public let hero: Hero
public init(hero: Hero) {
self.hero = hero
}
public struct Hero: Codable, Equatable, Hashable {
public let __typename: CharacterType
public let id: GraphQLID
public let name: String
public init(__typename: CharacterType,
id: GraphQLID,
name: String) {
self.__typename = __typename
self.id = id
self.name = name
}
}
}
}
Query with a fragment
Before
Fragment:
public struct CharacterName: GraphQLFragment {
/// The raw GraphQL definition of this fragment.
public static let fragmentDefinition =
"""
fragment CharacterName on Character {
__typename
name
}
"""
public static let possibleTypes = ["Human", "Droid"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public static func makeHuman(name: String) -> CharacterName {
return CharacterName(unsafeResultMap: ["__typename": "Human", "name": name])
}
public static func makeDroid(name: String) -> CharacterName {
return CharacterName(unsafeResultMap: ["__typename": "Droid", "name": name])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// The name of the character
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
}
Query:
public final class HeroNameWithFragmentQuery: GraphQLQuery {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
query HeroNameWithFragment($episode: Episode) {
hero(episode: $episode) {
__typename
...CharacterName
}
}
"""
public let operationName = "HeroNameWithFragment"
public let operationIdentifier: String? = "b952f0054915a32ec524ac0dde0244bcda246649debe149f9e32e303e21c8266"
public var queryDocument: String { return operationDefinition.appending(CharacterName.fragmentDefinition) }
public var episode: Episode?
public init(episode: Episode? = nil) {
self.episode = episode
}
public var variables: GraphQLMap? {
return ["episode": episode]
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = ["Query"]
public static let selections: [GraphQLSelection] = [
GraphQLField("hero", arguments: ["episode": GraphQLVariable("episode")], type: .object(Hero.selections)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(hero: Hero? = nil) {
self.init(unsafeResultMap: ["__typename": "Query", "hero": hero.flatMap { (value: Hero) -> ResultMap in value.resultMap }])
}
public var hero: Hero? {
get {
return (resultMap["hero"] as? ResultMap).flatMap { Hero(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "hero")
}
}
public struct Hero: GraphQLSelectionSet {
public static let possibleTypes = ["Human", "Droid"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public static func makeHuman(name: String) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Human", "name": name])
}
public static func makeDroid(name: String) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Droid", "name": name])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// The name of the character
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
public var fragments: Fragments {
get {
return Fragments(unsafeResultMap: resultMap)
}
set {
resultMap += newValue.resultMap
}
}
public struct Fragments {
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public var characterName: CharacterName {
get {
return CharacterName(unsafeResultMap: resultMap)
}
set {
resultMap += newValue.resultMap
}
}
}
}
}
}
After
Fragment:
public protocol CharacterNameMk2: GraphQLFragmentMk2 {
var __typename: CharacterType { get }
var name: String { get }
}
public extension CharacterNameMk2 {
static var fragmentDefinition: String {
return """
fragment CharacterName on Character {
__typename
name
}
"""
}
}
Query:
public final class HeroNameWithFragmentQueryMk2: GraphQLQueryMk2, Codable {
public let operationDefinition =
"""
query HeroNameWithFragment($episode: Episode) {
hero(episode: $episode) {
__typename
...CharacterName
}
}
"""
public let operationName = "HeroNameWithFragment"
public var queryDocument: String { return operationDefinition.appending(ResponseData.Hero.fragmentDefinition) }
public var episode: GraphQLOptional<EpisodeMk2>
public init(episode: GraphQLOptional<EpisodeMk2>) {
self.episode = episode
}
public enum CodingKeys: String, CodingKey {
case episode
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeGraphQLOptional(self.episode, forKey: .episode)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.episode = try container.decodeGraphQLOptional(forKey: .episode)
}
public struct ResponseData: Codable, Equatable, Hashable {
public let hero: Hero
public init(hero: Hero) {
self.hero = hero
}
public struct Hero: Codable, Equatable, Hashable, CharacterNameMk2 {
public let __typename: CharacterType
public let name: String
public init(__typename: CharacterType,
name: String) {
self.__typename = __typename
self.name = name
}
}
}
}
Query with Dependent types
Before
public final class HeroDetailsQuery: GraphQLQuery {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
query HeroDetails($episode: Episode) {
hero(episode: $episode) {
__typename
name
... on Human {
height
}
... on Droid {
primaryFunction
}
}
}
"""
public let operationName = "HeroDetails"
public let operationIdentifier: String? = "2b67111fd3a1c6b2ac7d1ef7764e5cefa41d3f4218e1d60cb67c22feafbd43ec"
public var episode: Episode?
public init(episode: Episode? = nil) {
self.episode = episode
}
public var variables: GraphQLMap? {
return ["episode": episode]
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = ["Query"]
public static let selections: [GraphQLSelection] = [
GraphQLField("hero", arguments: ["episode": GraphQLVariable("episode")], type: .object(Hero.selections)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(hero: Hero? = nil) {
self.init(unsafeResultMap: ["__typename": "Query", "hero": hero.flatMap { (value: Hero) -> ResultMap in value.resultMap }])
}
public var hero: Hero? {
get {
return (resultMap["hero"] as? ResultMap).flatMap { Hero(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "hero")
}
}
public struct Hero: GraphQLSelectionSet {
public static let possibleTypes = ["Human", "Droid"]
public static let selections: [GraphQLSelection] = [
GraphQLTypeCase(
variants: ["Human": AsHuman.selections, "Droid": AsDroid.selections],
default: [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
]
)
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public static func makeHuman(name: String, height: Double? = nil) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Human", "name": name, "height": height])
}
public static func makeDroid(name: String, primaryFunction: String? = nil) -> Hero {
return Hero(unsafeResultMap: ["__typename": "Droid", "name": name, "primaryFunction": primaryFunction])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// The name of the character
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
public var asHuman: AsHuman? {
get {
if !AsHuman.possibleTypes.contains(__typename) { return nil }
return AsHuman(unsafeResultMap: resultMap)
}
set {
guard let newValue = newValue else { return }
resultMap = newValue.resultMap
}
}
public struct AsHuman: GraphQLSelectionSet {
public static let possibleTypes = ["Human"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
GraphQLField("height", type: .scalar(Double.self)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(name: String, height: Double? = nil) {
self.init(unsafeResultMap: ["__typename": "Human", "name": name, "height": height])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// What this human calls themselves
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
/// Height in the preferred unit, default is meters
public var height: Double? {
get {
return resultMap["height"] as? Double
}
set {
resultMap.updateValue(newValue, forKey: "height")
}
}
}
public var asDroid: AsDroid? {
get {
if !AsDroid.possibleTypes.contains(__typename) { return nil }
return AsDroid(unsafeResultMap: resultMap)
}
set {
guard let newValue = newValue else { return }
resultMap = newValue.resultMap
}
}
public struct AsDroid: GraphQLSelectionSet {
public static let possibleTypes = ["Droid"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("name", type: .nonNull(.scalar(String.self))),
GraphQLField("primaryFunction", type: .scalar(String.self)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(name: String, primaryFunction: String? = nil) {
self.init(unsafeResultMap: ["__typename": "Droid", "name": name, "primaryFunction": primaryFunction])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// What others call this droid
public var name: String {
get {
return resultMap["name"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "name")
}
}
/// This droid's primary function
public var primaryFunction: String? {
get {
return resultMap["primaryFunction"] as? String
}
set {
resultMap.updateValue(newValue, forKey: "primaryFunction")
}
}
}
}
}
}
After
Union type enum:
public enum CharacterType: RawRepresentable, Codable, CaseIterable, Equatable, Hashable {
public typealias RawValue = String
case Human
case Droid
case __unknown(String)
public static var allCases: [CharacterType] {
return [
.Human,
.Droid
]
}
public var rawValue: String {
switch self {
case .Human: return "Human"
case .Droid: return "Droid"
case .__unknown(let value): return value
}
}
public init(rawValue: String) {
switch rawValue {
case "Human": self = .Human
case "Droid": self = .Droid
default: self = .__unknown(rawValue)
}
}
}
Query:
public final class HeroDetailsQueryMk2: GraphQLQueryMk2, Codable {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
query HeroDetails($episode: Episode) {
hero(episode: $episode) {
__typename
name
... on Human {
height
}
... on Droid {
primaryFunction
}
}
}
"""
public let operationName = "HeroDetails"
public var episode: GraphQLOptional<EpisodeMk2>
public init(episode: GraphQLOptional<EpisodeMk2>) {
self.episode = episode
}
public enum CodingKeys: String, CodingKey {
case episode
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeGraphQLOptional(self.episode, forKey: .episode)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.episode = try container.decodeGraphQLOptional(forKey: .episode)
}
public struct ResponseData: Codable, Equatable, Hashable {
public let hero: Hero
public init(hero: Hero) {
self.hero = hero
}
public struct Hero: Codable, Equatable, Hashable {
public let __typename: CharacterType
public let name: String
public let height: Double?
public let primaryFunction: String?
public init(__typename: CharacterType,
name: String,
height: Double?,
primaryFunction: String?) {
self.__typename = __typename
self.name = name
self.height = height
self.primaryFunction = primaryFunction
}
public var asDroid: DroidHero? {
guard self.__typename == .Droid else {
return nil
}
return DroidHero(name: self.name,
primaryFunction: self.primaryFunction!)
}
public var asHuman: HumanHero? {
guard self.__typename == .Human else {
return nil
}
return HumanHero(name: self.name,
height: self.height!)
}
public struct DroidHero: Equatable, Hashable {
public let name: String
public let primaryFunction: String
public init(name: String,
primaryFunction: String) {
self.name = name
self.primaryFunction = primaryFunction
}
}
public struct HumanHero: Equatable, Hashable {
public let name: String
public let height: Double
public init(name: String,
height: Double) {
self.name = name
self.height = height
}
}
}
}
}
Basic mutation
Before
Input Object:
/// The input object sent when someone is creating a new review
public struct ReviewInput: GraphQLMapConvertible {
public var graphQLMap: GraphQLMap
public init(stars: Int, commentary: Swift.Optional<String?> = nil, favoriteColor: Swift.Optional<ColorInput?> = nil) {
graphQLMap = ["stars": stars, "commentary": commentary, "favorite_color": favoriteColor]
}
/// 0-5 stars
public var stars: Int {
get {
return graphQLMap["stars"] as! Int
}
set {
graphQLMap.updateValue(newValue, forKey: "stars")
}
}
/// Comment about the movie, optional
public var commentary: Swift.Optional<String?> {
get {
return graphQLMap["commentary"] as? Swift.Optional<String?> ?? Swift.Optional<String?>.none
}
set {
graphQLMap.updateValue(newValue, forKey: "commentary")
}
}
/// Favorite color, optional
public var favoriteColor: Swift.Optional<ColorInput?> {
get {
return graphQLMap["favorite_color"] as? Swift.Optional<ColorInput?> ?? Swift.Optional<ColorInput?>.none
}
set {
graphQLMap.updateValue(newValue, forKey: "favorite_color")
}
}
}
Mutation:
public final class CreateReviewForEpisodeMutation: GraphQLMutation {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
__typename
stars
commentary
}
}
"""
public let operationName = "CreateReviewForEpisode"
public let operationIdentifier: String? = "9bbf5b4074d0635fb19d17c621b7b04ebfb1920d468a94266819e149841e7d5d"
public var episode: Episode
public var review: ReviewInput
public init(episode: Episode, review: ReviewInput) {
self.episode = episode
self.review = review
}
public var variables: GraphQLMap? {
return ["episode": episode, "review": review]
}
public struct Data: GraphQLSelectionSet {
public static let possibleTypes = ["Mutation"]
public static let selections: [GraphQLSelection] = [
GraphQLField("createReview", arguments: ["episode": GraphQLVariable("episode"), "review": GraphQLVariable("review")], type: .object(CreateReview.selections)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(createReview: CreateReview? = nil) {
self.init(unsafeResultMap: ["__typename": "Mutation", "createReview": createReview.flatMap { (value: CreateReview) -> ResultMap in value.resultMap }])
}
public var createReview: CreateReview? {
get {
return (resultMap["createReview"] as? ResultMap).flatMap { CreateReview(unsafeResultMap: $0) }
}
set {
resultMap.updateValue(newValue?.resultMap, forKey: "createReview")
}
}
public struct CreateReview: GraphQLSelectionSet {
public static let possibleTypes = ["Review"]
public static let selections: [GraphQLSelection] = [
GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
GraphQLField("stars", type: .nonNull(.scalar(Int.self))),
GraphQLField("commentary", type: .scalar(String.self)),
]
public private(set) var resultMap: ResultMap
public init(unsafeResultMap: ResultMap) {
self.resultMap = unsafeResultMap
}
public init(stars: Int, commentary: String? = nil) {
self.init(unsafeResultMap: ["__typename": "Review", "stars": stars, "commentary": commentary])
}
public var __typename: String {
get {
return resultMap["__typename"]! as! String
}
set {
resultMap.updateValue(newValue, forKey: "__typename")
}
}
/// The number of stars this review gave, 1-5
public var stars: Int {
get {
return resultMap["stars"]! as! Int
}
set {
resultMap.updateValue(newValue, forKey: "stars")
}
}
/// Comment about the movie
public var commentary: String? {
get {
return resultMap["commentary"] as? String
}
set {
resultMap.updateValue(newValue, forKey: "commentary")
}
}
}
}
}
After
Input object:
/// The input object sent when someone is creating a new review
public struct ReviewInputMk2 {
/// 0-5 stars
public let stars: Int
/// Comment about the movie, optional
public let commentary: GraphQLOptional<String>
/// Favorite color, optional
public let favoriteColor: GraphQLOptional<ColorInputMk2>
public enum CodingKeys: String, CodingKey {
case stars
case commentary
case favoriteColor
}
public init(stars: Int,
commentary: GraphQLOptional<String>,
favoriteColor: GraphQLOptional<ColorInputMk2>) {
self.stars = stars
self.commentary = commentary
self.favoriteColor = favoriteColor
}
}
extension ReviewInputMk2: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ReviewInputMk2.CodingKeys.self)
try container.encode(self.stars, forKey: .stars)
try container.encodeGraphQLOptional(self.commentary, forKey: .commentary)
try container.encodeGraphQLOptional(self.favoriteColor, forKey: .favoriteColor)
}
}
extension ReviewInputMk2: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ReviewInputMk2.CodingKeys.self)
self.stars = try container.decode(Int.self, forKey: .stars)
self.commentary = try container.decodeGraphQLOptional(forKey: .commentary)
self.favoriteColor = try container.decodeGraphQLOptional(forKey: .favoriteColor)
}
}
Mutation:
public final class CreateAwesomeReviewMutationMk2: GraphQLMutationMk2 {
/// The raw GraphQL definition of this operation.
public let operationDefinition =
"""
mutation CreateReviewForEpisode($episode: Episode!, $review: ReviewInput!) {
createReview(episode: $episode, review: $review) {
__typename
stars
commentary
}
}
"""
public let operationName = "CreateAwesomeReview"
public var variableData: Data? {
return nil
}
public init() {
}
public struct ResponseData: Codable, Equatable, Hashable {
public let createReview: CreateReview
public init(createReview: CreateReview) {
self.createReview = createReview
}
public struct CreateReview: Codable, Equatable, Hashable {
public let __typename: String
public let stars: Int
public let commentary: String?
public init(__typename: String,
stars: Int,
commentary: String?) {
self.__typename = __typename
self.stars = stars
self.commentary = commentary
}
}
}
}
Adding a handler for parsing code from new codegen
Right now all our parsing code is heavily tied to our current caching mechanism. This first phase will not include any caching, and thus will need to have somewhat different code for handling parsing the code.
This will be made available through an option on the CodegenOptions added in phase 0, which will default to false.
Updating documentation
Documentation for the new code generation will likely need to live side-by-side with the documentation for the old code generation until the old codegen is deprecated.
Clear documentation for each of the two options will be critical.
Phase 2: Add Immutable caching
For this phase, a caching layer which cannot be mutated directly by developers and which is compatible with the new system will be added.
This has the benefit of giving access to basic caching quicker than would be possible if we tried to add mutable caching at the same time.
This caching layer will need to be compatible with both In-Memory and SQLite backing stores.
For In-Memory stores, migration is not necessary since the store is destroyed every time your application is terminated.
Right now there is not a plan to include any official migration from older versions of the cache for SQLite-based caching.
The architecture will be sufficiently different from the existing architecture that, in my opinion, trying to write a general enough migrator to move everything over would be a disproportionate time sink, and ultimately not worth it. I would greatly appreciate alternative opinions on this point in the comments.
TBD is whether read-only direct cache access will be supported at this point, or punted to the addition of mutable caching.
The CodegenOptions to use the new code generation will remain defaulted to false at this phase, although this should be considerably more usable for the majority of users.
Phase 3: Add Mutable caching
This part is still somewhat ill-defined, and I may send out a separate RFC on this part before I start adding the immutable caching, but I wanted to outline some of the issues at play.
Since our current caching system relies backing dictionaries generated by our current codegen, this presents a challenge when attempting to update a cached value:
- How do you access the value that needs to be updated?
- How do you update it, since in new codegen objects are immutable? Do you update the value in the underlying cache with a key path? Do you make a copy of the existing object, changing only the properties you wish to change?
- How does this change if you're using
Identifiable conformance to uniquely identify your object?
This will all probably take some figuring out, but looking forward to digging in to this.
The CodegenOptions to use the new code generation will default to true after this phase, and old code generation will be formally deprecated.
Phase 4: Removal of old codegen
Once phase 3 has been shipped and been determined to be reasonably stable, support for code from the old codegen will be removed.
And I will do a happy dance. 🙃
Request for comment
Please add your comments below. Anything I change in response to comments, I'll try to add footnotes to in order to indicate where the change came from.
Thank you for reading all this!
This issue will serve as the official Request for Comment for the Swift Codegen project.
This project is to take our existing code generation, which is done in Typescript, and move it to Swift.
Note that parsing of the Schema and parsing/validation of queries will still be taking place using our Typescript tooling - these are presently too tied to JS to be able to be replaced without a very, very large amount of work.
Moving the actual generation of code to Swift will allow Swift developers to more easily understand how code is being generated, and more easily contribute fixes and improvements to the code generation process.
This project will take place in several phases, which are outlined below.
Phase 0: Generate code with swift Script instead of bash
Swift scripts can be run directly from the command line, and these scripts can also import frameworks.
Everything we are currently using bash for will be updated to use Swift wrappers in a framework which will be included with the core Apollo library. This includes:
This will be considerably easier to test than using random bash scripts, and those tests will help ensure that any changes are non-breaking.
The initial framework and tests are already up as a draft PR if you're interested in looking at the details.
The other thing this will do is make it considerably easier to add more flags in the future, specifically the flag to enable the Swift-based code generation rather than the Typescript-based code generation.
This phase will require significant updates to documentation since everything assumes the use of bash right now.
Phase 1: New Codegen
This is going to involve a few sub-phases.
Getting the Abstract Syntax Tree out of our existing codegen in JSON format
This is the part that depends most on our tooling repo. This should basically be done, but this could potentially be a blocker if there are pieces that we need to generate Swift code that's clearer and harder to fail at.
Parsing the AST into Swift Objects for code generation
In theory, this part should be the most straightforward, and will mostly involve parsing everything in the JSON into
Codableobjects which will ultimately be used by the code generation engine.Generating code
This will involve a number of new goodies:
Codableconformance instead of custom JSON parsingEquatableandHashableconformanceIdentifiableconformance when aGraphQLIDis one of the returned fields.String-backed enums to make it easier toswitchon the underlying type and to pull out the types being generatedGraphQLOptionaltype which better represents the potential states of items which can be sent to the server.I've stubbed all this out manually using our Star Wars test harness, so I wanted to share a few examples of what the differences in generated code will be (Hidden behind flippy triangles because it's loooooong).
This code is provided for feedback purposes and is not meant to represent the final version of things.
Basic query
Before
After
Query with a fragment
Before
Fragment:
Query:
After
Fragment:
Query:
Query with Dependent types
Before
After
Union type enum:
Query:
Basic mutation
Before
Input Object:
Mutation:
After
Input object:
Mutation:
Adding a handler for parsing code from new codegen
Right now all our parsing code is heavily tied to our current caching mechanism. This first phase will not include any caching, and thus will need to have somewhat different code for handling parsing the code.
This will be made available through an option on the
CodegenOptionsadded in phase 0, which will default tofalse.Updating documentation
Documentation for the new code generation will likely need to live side-by-side with the documentation for the old code generation until the old codegen is deprecated.
Clear documentation for each of the two options will be critical.
Phase 2: Add Immutable caching
For this phase, a caching layer which cannot be mutated directly by developers and which is compatible with the new system will be added.
This has the benefit of giving access to basic caching quicker than would be possible if we tried to add mutable caching at the same time.
This caching layer will need to be compatible with both In-Memory and SQLite backing stores.
For In-Memory stores, migration is not necessary since the store is destroyed every time your application is terminated.
Right now there is not a plan to include any official migration from older versions of the cache for SQLite-based caching.
The architecture will be sufficiently different from the existing architecture that, in my opinion, trying to write a general enough migrator to move everything over would be a disproportionate time sink, and ultimately not worth it. I would greatly appreciate alternative opinions on this point in the comments.
TBD is whether read-only direct cache access will be supported at this point, or punted to the addition of mutable caching.
The
CodegenOptionsto use the new code generation will remain defaulted tofalseat this phase, although this should be considerably more usable for the majority of users.Phase 3: Add Mutable caching
This part is still somewhat ill-defined, and I may send out a separate RFC on this part before I start adding the immutable caching, but I wanted to outline some of the issues at play.
Since our current caching system relies backing dictionaries generated by our current codegen, this presents a challenge when attempting to update a cached value:
Identifiableconformance to uniquely identify your object?This will all probably take some figuring out, but looking forward to digging in to this.
The
CodegenOptionsto use the new code generation will default totrueafter this phase, and old code generation will be formally deprecated.Phase 4: Removal of old codegen
Once phase 3 has been shipped and been determined to be reasonably stable, support for code from the old codegen will be removed.
And I will do a happy dance. 🙃
Request for comment
Please add your comments below. Anything I change in response to comments, I'll try to add footnotes to in order to indicate where the change came from.
Thank you for reading all this!