Closed designatednerd closed 4 years ago
This is wonderful news! Thanks for all the great updates and thoughtfulness you have put into the sdk over the last few months
Thanks! Now I just gotta put my money where my mouth is
Thanks for putting this together! A few comments/questions:
For handling union types, I noticed in your sample generated code that you have the fields which are specific to a given specialization (e.g., DroidHero
) as optionals on the generic version (e.g., public let height: Double?
on Hero
). Won't this break if, for instance DroidHero
had a height: Double
field defined, but HumanHero
had a height: String
field? Or do you have a way around that planned?
Also, it looks like as you have it, union types still use the as<Specific Type>
pattern. Have you considered adding associated values to the cases in the __typename
enum so we'd be able to use switch
-case let
? Something along the lines of:
enum CharacterType {
case Human(HumanHero)
case Droid(DroidHero)
case __unknown(String)
}
switch myHero.typename { case let .Droid(droidHero): // handle droid case let .Human(humanHero): // handle human case let .unknown(unrecognizedTypeName): // handle unrecognized character type }
3. Saw a `GraphQLOptional` type floating around in the sample code- is that some special type that's distinct from `Swift.Optional`? (And perhaps related: what are the `encodeGraphQLOptional`/`decodeGraphQLOptional` functions meant to be doing, as distinct from the built-in encode/decode functions for swift optionals?)
Those questions aside though, overall this is looking great to me!
- For handling union types, I noticed in your sample generated code that you have the fields which are specific to a given specialization (e.g., DroidHero) as optionals on the generic version (e.g., public let height: Double? on Hero). Won't this break if, for instance DroidHero had a height: Double field defined, but HumanHero had a height: String field? Or do you have a way around that planned?
Yes, but I believe this will also break under the current setup. You should be able to avoid this by aliasing one of the fields in the request, which will change the name of the property on the Swift side.
.Have you considered adding associated values to the cases in the
__typename
enum so we'd be able to useswitch
-case let
?
Unfortunately that won't work super-well with this setup, since those associated types will be different per query, and I want that union type to work with all queries.
Otherwise we'd have to regenerate the union type for everything, and that would be a ton of extra code compared to the as blah
version, especially for APIs that have huge union types like Github pull request timeline items.
Saw a GraphQLOptional type floating around in the sample code- is that some special type that's distinct from Swift.Optional?
Yes, that's the replacement for double-optionals in input types to make it considerably more explicit what's being sent. There's been a ton of confusion on how to send a null value vs. not sending anything. Here's the code for those (including the extensions on encoding and decoding containers):
public enum GraphQLOptional<T> {
case notPresent
case nullValue
case value(T)
}
extension GraphQLOptional: Equatable where T: Equatable {
public static func ==(lhs: GraphQLOptional, rhs: GraphQLOptional) -> Bool {
switch (lhs, rhs) {
case (.notPresent, .notPresent),
(.nullValue, .nullValue):
return true
case (.value(let lhsValue), .value(let rhsValue)):
return lhsValue == rhsValue
default:
return false
}
}
}
public extension KeyedEncodingContainer {
mutating func encodeGraphQLOptional<T: Codable>(_ optional: GraphQLOptional<T>, forKey key: K) throws {
switch optional {
case .notPresent:
break
case .nullValue:
try self.encodeNil(forKey: key)
case .value(let value):
try self.encode(value, forKey: key)
}
}
}
public extension KeyedDecodingContainer {
func decodeGraphQLOptional<T: Codable>(forKey key: K) throws -> GraphQLOptional<T> {
if self.contains(key) {
if let value = try? self.decode(T.self, forKey: key) {
return .value(value)
} else {
return .nullValue
}
} else {
return .notPresent
}
}
}
Gonna leave this open for further comment over the weekend and then add some of this stuff to the ROADMAP.md
document (probably not in this much detail) and close this issue out.
I stumbled upon this while trying out appolo with swiftui and getting problems parsing objects in lists if they are not implementing Identifiable
even if they have an id field. Great that this is already in progress of being fixed as part of the rewrite :-)
That one's also a choice that's going to be opt-in: Some users don't want the id
field to be the unique identifier, since there's also potentially things like a pre-existing GUID or an ISBN that people may need to use for unique identifiers.
A few thoughts from having worked with a modified version of Apollo codegen:
1/ Codable I love the move to encoder / decoder. Do you think this would let people use other format than json?
2/ Nested protocols Maybe you could lay out how protocols work with nested objects selection in the fragment. I had to generate some ugly boilerplate there to make swift happy as I didn't want to have associated types. For instance what would this convert to?
fragment Frag on Character {
friends {
name
}
}
3/ Opt in features With a large codebase, it's valuable to have Equatable, Hashable, Identifiable etc be opt in (or have a configurable default that developers can intentionally diverge from). The reason is that those few words have a large impact on binary size and code scales better with conservative defaults. To do so, I've used GraphQL directives that are directed to the codegen. This allow developers to easily control the codegen on a per query basis. I also made response types immutable by default.
4/ Lean structs Also related to app size, complex structs unfortunately have a large impact, quite larger than classes (4MB for me, which was a blocker). The current codegen uses lean structs as their only stored attribute is a dictionary. I moved to a struct + class design inspired by https://developer.apple.com/videos/play/wwdc2016/416/?time=2361 that is similar to the current solution with stronger type safety. This does add a bunch of code you don't really want to be reading though...
5/ Protocol initializers For testing, it would be nice to have a stub implementation of each protocol that one can instantiate without depending on a specific type.
6/ File modularization It would be great to let folks write .graphql
files anywhere in their codebase and generate .swift
files alongside them. This helps with modularization. Also because queries can then be more easily internal and not public this might reduce app size through more optimized compilation. I'm not sure how you'd make the workspace aware of the new files though.
7/ Abstract types For the abstract/union type representation, you also could consider using an enum type which doesn't expose the non common properties:
public enum Hero: Codable, Equatable, Hashable {
case droid(let droid: DroidHero)
case human(let human: HumanHero)
public var __typename: CharacterType {
switch self {
case .droid(let droid):
return droid.__typename
case .human(let human):
return human.__typename
}
}
public var name: CharacterType {
switch self {
case .droid(let droid):
return droid.name
case .human(let human):
return human.name
}
}
}
8/ The enum representation will be great!
9/ I think killing double optional is great as well.
10/ It's not clear to me what the need for a new caching solution is. I would love to understand this part better! 🤔
Apologies for taking a couple days to get back to you @gsabran, I'm a bit under the weather. Trying to reply point by point to your questions:
1/ ... Do you think this would let people use other format than json?
Ideally you should be able to pass in encoder/decoder so that's supported. I'm not sure if this will be part of the very first version but given the way Codable works it shouldn't be too difficult to work with longer term.
2/ Nested protocols Maybe you could lay out how protocols work with nested objects selection in the fragment. I had to generate some ugly boilerplate there to make swift happy as I didn't want to have associated types...
This is set up as associated types at the moment:
Can you point to specific problems you've had with associated types? I know they can be a little finicky but they are way better than a number of alternatives I explored.
3/ Opt-in...
I will probably make these opt-out rather than opt-in. Most people are not working with super-huge codebases, and the bloat would be worth the simplicity. That said, you're right that it does add some bloat if you're in
4/ ....I moved to a struct + class design inspired by https://developer.apple.com/videos/play/wwdc2016/416/?time=2361...
I'll have to watch this video at a point where my brain is working properly, but thank you for the link!
5/ Protocol initializers For testing, it would be nice to have a stub implementation of each protocol that one can instantiate without depending on a specific type.
This sounds like it'd cause some code bloat issues similar to what you were concerned about in point 3, though. What's an example scenario where a base implementation would give you a better testbed than a specific implementation that already exists?
6/ File modularization It would be great to let folks write .graphql files anywhere in their codebase and generate .swift files alongside them.
This sounds great but unfortunately I think this would require very significant decoupling from our typescript code generation. I'll keep this in mind as an idea as I work through this, but I gotta be honest, it's highly likely this is not going to be possible without massive, massive changes well beyond the scope of this project.
7/ ...consider using an enum type which doesn't expose the non common properties:...
I'll take a look at this suggestion in the context of what I've already got going. Thanks!
10/ It's not clear to me what the need for a new caching solution is. I would love to understand this part better! 🤔
The current caching solution is completely tied to everything being backed by untyped dictionaries. Moving to strongly typed objects and properties essentially breaks the hell out of that.
Thanks!
2/ Can you point to specific problems you've had with associated types?
I think mainly when you are working with a collection of elements of varying concrete type, in which case you have to do type erasure. Also you can't cast to an associated type.
5/ This sounds like it'd cause some code bloat issues
Yes, you'll definitively want to wrap this in #if DEBUG ... #endif
What's an example scenario where a base implementation would give you a better testbed than a specific implementation that already exists?
You could imagine that you have a small fragment that only appears in a large selection. Testing a function that uses only the output of the fragment will require some cumbersome initialization. I agree it is not as important as the other things.
Testing a function that uses only the output of the fragment will require some cumbersome initialization.
Agreed, but can't you just make a dummy struct in your tests that conforms to that protocol?
For sure, it would be less work for the developer to get this for free from the codegen :)
it would be less work for the developer
* -
developers who are not me 😆
Agreed that this is a cool idea but I think it probably won't go into the first round of changes just because I want to try and keep the scope limited where I can in hopes of shipping this, ever.
Totally agreed for the first / second rounds. Just saying!
Great work - looking forward to seeing this land! I came here with the express purpose of writing a feature request for the ability to split the generated swift file into multiple files, but I see that @gsabran mentioned it above in 6/ ... it's a shame that it sounds like something that's going to be hard to implement, because it's becoming quite an annoyance for us in using Apollo.
Using the current version of Apollo codegen, our (prisma-generated) schema results in a massively unwieldy 60k line swift file... Cthulhu-save any unwitting dev that opens that file or Ctrl-Cmd-Clicks on a generated type to jump to the definition... there is a good 30-60sec of block main-queue in Xcode before the file opens, every time :/
So if there is anyway this can be given some attention it'd be very much appreciated 😄
@lozhuf The issue is with putting multiple files the same place as the query files - there's a bunch of info I'm going to lose on the folder structure as stuff gets output to JSON that makes that harder.
If you want to output multiple files into one directory instead of doing one giant file you can already do that with current codegen, and I'm planning to make that considerably more obvious with the swift scripting codegen.
Ahh, THANKYOU! 👏 turns out, the correct answer, as always, is RTFM 😉
Thank you all for your comments, I've now updated the ROADMAP
document with a high-level summary of the issues here. I'll be using the Swift Codegen Project to beef up a list of what needs to be done on everything, so please feel free to follow that project for further updates.
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
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 parsingEquatable
andHashable
conformanceIdentifiable
conformance when aGraphQLID
is one of the returned fields.String
-backed enums to make it easier toswitch
on the underlying type and to pull out the types being generatedGraphQLOptional
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** ```swift 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** ```swift 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: GraphQLOptionalQuery with a fragment
**Before** Fragment: ```swift 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: ```swift 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: ```swift 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: ```swift 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: GraphQLOptionalQuery with Dependent types
**Before** ```swift 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: ```swift 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: ```swift 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: GraphQLOptionalBasic mutation
**Before** Input Object: ```swift /// 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.OptionalAdding 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 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
CodegenOptions
to use the new code generation will remain defaulted tofalse
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:
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 totrue
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!