Skip to content

RFC: Swift Codegen Rewrite #939

@designatednerd

Description

@designatednerd

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    discussionRequests for comment or other discussionsfeatureNew addition or enhancement to existing solutions

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions