Apollo-tooling: Swift Codegen, fragments

Created on 27 Dec 2016  Â·  12Comments  Â·  Source: apollographql/apollo-tooling

Question/Feature request regarding the output of the generated code for swift when fragments are used.

We have a fragment called "FormattedText" to encapsulate complex text fields that have nested bold/italic/link content.
And example quest would be

...
...
  headingText {
        ...formattedText
      }
...

The returned data is as follows

"headingText": {
            "spans": [
              {
                "text": "Credit card utilization"
              }
            ]
          },

The issue arrises with the gerenated code:

...
public let headingText: HeadingText?
...


  public struct HeadingText: GraphQLMappable {
                    public let __typename = "FormattedText"

                    public let fragments: Fragments

                    public init(reader: GraphQLResultReader) throws {
                        let formattedText = try FormattedText(reader: reader)
                        fragments = Fragments(formattedText: formattedText)
                    }

                    public struct Fragments {
                        public let formattedText: FormattedText
                    }
                }
...
...
//and finally Fragmented Text

public struct FormattedText: GraphQLNamedFragment {
    public static let fragmentDefinition =
        "fragment formattedText on FormattedText {" +
        "  spans {" +
        "    text" +
        "  }" +
        "}"

    public static let possibleTypes = ["FormattedText"]

    public let __typename = "FormattedText"
    public let spans: [Span?]?

    public init(reader: GraphQLResultReader) throws {
        spans = try reader.optionalList(for: Field(responseName: "spans"))
    }

    public struct Span: GraphQLMappable {
        public let __typename = "FormattedSpan"
        public let text: String?

        public init(reader: GraphQLResultReader) throws {
            text = try reader.optionalValue(for: Field(responseName: "text"))
        }
    }
}

In order to access this object you would write:

data.headingText?.fragments.formattedText

The feedback is the fragments contatiner necessary? Should the HeadingText class itself not be a FormattedText or more closely implement the FormattedText fragment. This indirection strikes me as unnecessary.

Even the headingText?.__typename == "FormattedText" so this type has been fully specified.

eg:

headingText CKGraphQL.TestCreditFactorsQuery.Data.CreditFactor.HeadingText? some
    __typename  String  "FormattedText"         
    fragments   CKGraphQL.TestCreditFactorsQuery.Data.CreditFactor.HeadingText.Fragments    
        formattedText   CKGraphQL.FormattedText 
            __typename  String  "FormattedText" 
            spans   [CKGraphQL.FormattedText.Span?]?    1 value some
                [0] CKGraphQL.FormattedText.Span?   some
                    __typename  String  "FormattedSpan" 
                    text    String? "Credit card utilization"   some

So my suggestion is, can we make HeadingText more directly inherit from FormattedText?

 public struct HeadingText: FormattedText {

What are the conditions under which this fragment structure becomes more useful?

Most helpful comment

Just came across this, sounds like it might solve our issues if it lands!

https://github.com/apple/swift/pull/8718

All 12 comments

I actually started out with the approach you're suggesting, generating objects with fields from fragments merged in, and conforming to a generated protocol for each fragment.

This broke down because of limitations of the Swift type system, and then I realized there were also positive consequences to keeping fragments as separate objects.

To start with the typing limitations, the main issue is that properties of protocols are invariant in the current version of Swift. So you cannot conform to a protocol with a property of a more specific type, even if the property is read only.

To take a simple example, given this GraphQL query and fragment:

query Hero {
  hero {
    ...HeroDetails
    friends {
      appearsIn
    }
  }
}

fragment HeroAndFriendsNames on Character {
  name
  friends {
    name
  }
}

You would expect to be able to generate something like this (leaving out a lot of code):

class HeroQuery {
  struct Data {
    struct Hero: HeroAndFriendsNames {
      let name: String
      let friends: [Friend]
      struct Friend: HeroAndFriendsNames_Friend {
        let name: String
        let appearsIn: [Episode]
      }
    }
  }
}

protocol HeroAndFriendsNames {
  var name: String { get }
  var friends: [HeroAndFriendsNames_Friend] { get }
}

protocol HeroAndFriendsNames_Friend {
  var name: String { get }
}

But this code doesn't compile because let friends: [Friend] does not conform to var friends: [HeroAndFriendsNames_Friend] { get }, even though Friend conforms to HeroAndFriendsNames_Friend.

It gets even more complicated when you take type conditions into account, because now the type hierarchy starts to diverge based on the actual type of the object.

Generating fragments as separate objects is a way to overcome these issues. But the more positive way of looking at it is that it enables the kind of data masking Relay also advocates.

Child components (e.g. table view cells) can define fragments with their exact data needs. And parent components (e.g. a table view controller) only have to know the name of the fragment and pass it on without inadvertently depending on data requested by the child (or the other way around). See the Frontpage iOS app for an example of this structure.

I see what you're describing.
The two suggestions I can make that somewhat alleviate the uglyness are

a) Add an "as" operator similar to "asDroid/asHuman" downcasting.

public var asHeroAndFriends : HeroAndFriendsNames {
       get {
            return self.fragments.heroAndFriendsNames
       }
}

b) Add an init extension to HeroAndFriendsNames

extension HeroAndFriendsNames{
    init(_ hero : HeroAndFriendsNamesQuery.Data.Hero) {
        self = hero.fragments.heroAndFriendsNames;
    }
}

for usage like

let heroAndFriends = HeroAndFriendsNames(data.hero!)

I may have gotten too used to it by now, and I remember thinking it was ugly before, but making fragments explicit doesn't actually seem too bad to me.

The upside is that you make it very clear you're accessing data from a fragment, and you get convenient code completion with just the fragment names, so it's easy to see what fragments are there.

I think the switch I had to make is to see fragments as not just an implementation construct, but as a way of defining view models that clearly ties data needs to specific UI components.

I'm definitely open to better solutions (of those two, I think I would prefer the first, because you still get code completion), but I'm not yet convinced they are needed.

Hmm, @martijnwalraven in the example above, just using an associated type should correctly constrain things and make it more ergonomic. The following compiles correctly:

public enum Episode: String {
  case newhope = "NEWHOPE"
  case empire = "EMPIRE"
  case jedi = "JEDI"
}

class HeroQuery {
  struct Data {
    let hero: Hero
    struct Hero: HeroAndFriendsNames  {
      let name: String
      let friends: [Friend]

      struct Friend: HeroAndFriendsNames_Friend {
        let name: String
        let appearsIn: [Episode]
      }
    }
  }
}

protocol HeroAndFriendsNames {
  associatedtype Friend: HeroAndFriendsNames_Friend

  var name: String { get }
  var friends: [Friend] { get }
}

protocol HeroAndFriendsNames_Friend {
  var name: String { get }
}

Used like so:

let x = HeroQuery.Data(
  hero: HeroQuery.Data.Hero(
    name: "Foo",
    friends: [
      HeroQuery.Data.Hero.Friend(
        name: "Bar",
        appearsIn: [.newhope]
      )
    ]
  )
)
print(x)
print(x.hero.friends[0].appearsIn[0])

// Types are correctly constrained (and can be genericized for fragments):
func hiFromHeroAndFriendsFragment<H: HeroAndFriendsNames>(hero: H) -> String {
  let friends = hero.friends.map { $0.name }.joined(separator: ", ")
  return "\(hero.name) says, 'Hi, \(friends)!"
}

// Can be specific to the concrete structure if preferred:
func hiFromHeroQueryHero(hero: HeroQuery.Data.Hero) -> String {
  let friends = hero.friends.map { $0.name }.joined(separator: ", ")
  return "\(hero.name) says, 'Hi, \(friends)!"
}

print(hiFromHeroAndFriendsFragment(hero: x.hero))
print(hiFromHeroQueryHero(hero: x.hero))

@erydo: Using associated types is an interesting approach, but the biggest drawback is that you can no longer store fragments.

In this example, it's impossible to have a variable of type HeroAndFriendsNames. As you mentioned, you can use fragments as type constraints in a generic function, and that is useful, but I think it is important we can still treat fragments as stand-alone types as well.

I'd argue that fragments that can be spread _are not_ necessarily standalone types, which is even emphasized by their name. They're essentially structural constraints. The fields returned in a selection set form a union of each of the fields listed and each of the fields from their (spreadable/non-upcasting) fragments.

Fragments implemented as above can be stored, either passed abstractly as "any kind of result that includes that fragment" (hiFromHeroAndFriendsFragment above) or concretely (hiFromHeroQueryHero).

For cases where it's really necessary to create a common struct type for a fragment, it would still be straightforward to make that available. You could generate a struct implementation of that fragment protocol with only the fragment members—similar to what apollo-codegen does now—and a single toXXXFragmentStruct implementation to get that from any fragment implementor.

Excuse the poor naming in the following, I'm roughly simulating what the generated code might look like. The following added to code in the earlier comment:

struct HeroAndFriendsNamesStruct: HeroAndFriendsNames {
  let name: String
  let friends: [Friend]
  typealias Friend = HeroAndFriendsNames_FriendStruct
}

extension HeroAndFriendsNames {
  typealias FragmentStruct = HeroAndFriendsNamesStruct

  // Converts any selection set with this fragment to a common struct type.
  var toHeroAndFriendsNamesStruct: FragmentStruct {
    return FragmentStruct(
      name: self.name,
      friends: self.friends.map { (f: Friend) in f.toHeroAndFriendsNames_FriendStruct }
    )
  }
}

struct HeroAndFriendsNames_FriendStruct: HeroAndFriendsNames_Friend {
  let name: String
}

extension HeroAndFriendsNames_Friend {
  typealias FragmentStruct = HeroAndFriendsNames_FriendStruct

  // Converts any selection set with this fragment to a common struct type.
  var toHeroAndFriendsNames_FriendStruct: FragmentStruct {
    return FragmentStruct(name: self.name)
  }
}

That would allow you to use a concrete type if using the protocol is undesirable:

// Generic for any query results that include that Fragment:
func hiGeneric<H: HeroAndFriendsNames>(hero: H) -> String {
  let friends = hero.friends.map { $0.name }.joined(separator: ", ")
  return "\(hero.name) says, 'Hi, \(friends)!"
}

// Results specifically from one query which includes that fragment:
func hiFromQuery(hero: HeroQuery.Data.Hero) -> String {
  let friends = hero.friends.map { $0.name }.joined(separator: ", ")
  return "\(hero.name) says, 'Hi, \(friends)!"
}

// If you can't use generics or otherwise want a concrete struct:
func hiOnlyFragmentStruct(hero: HeroAndFriendsNames.FragmentStruct) -> String {
  let friends = hero.friends.map { $0.name }.joined(separator: ", ")
  return "\(hero.name) says, 'Hi, \(friends)!"
}

print(hiFromQuery(hero: x.hero))
print(hiGeneric(hero: x.hero))
print(hiGeneric(hero: x.hero.toHeroAndFriendsNamesStruct))
print(hiOnlyFragmentStruct(hero: x.hero.toHeroAndFriendsNamesStruct))

// Will not work because x.hero isn't exactly the fragment type, and
// hiOnlyFragmentStruct isn't generic.
// print(hiOnlyFragmentStruct(hero: x.hero))

// Will not work because hiFromQuery expects a type greater than the limited
// fragment provides:
// print(hiFromQuery(hero: x.hero.toFragmentStruct))

That approach covers both cases: It allows fragments on results to be treated generically and ergonomically, but still allows them to be isolated out if for some reason they need to be.

Another benefit is that factoring a query's fields into a fragment no longer forces all code using that query to be refactored, which is intuitive, because the actual structural guarantees of that query wouldn't have changed.

On that note, it's useful to consider upcasting fragments separately.
The ...HeroDetails example above is a guaranteed type match (all of its fields could be inlined), but that same fragment used as an upcast on a node() query wouldn't be—you wouldn't be able to inline the fragment's fields. For those cases I think apollo-codegen's current approach of putting those as nullable members is good.

But many (perhaps most?) instances of fragments are not upcasts in that way, but are simply spreads of common fields.

Fragments implemented as above can be stored, either passed abstractly as "any kind of result that includes that fragment" (hiFromHeroAndFriendsFragment above) or concretely (hiFromHeroQueryHero).

That depends on what you mean by stored. They can not be stored as instance variables, which is something most people would want to do be able to do with a fragment.

The current restrictions on protocols with associated types, until we get something like generalized existentials in the language, mean many legitimate uses will end up with a Protocol ‘HeroAndFriendsNames' can only be used as a generic constraint because it has Self or associated type requirements error.

This severely limits the usefulness of the approach, and I don't think being able to use the protocol as a generic constraint justifies the added complexity of generating both protocols and structs for every selection set for every fragment.

Protocols with associated types and generic methods are pretty advanced concepts that are hard to grasp for many developers. I don't think we can ask people to make every method that deals with a fragment generic.

I'd argue that fragments that can be spread _are not_ necessarily standalone types, which is even emphasized by their name. They're essentially structural constraints. The fields returned in a selection set form a union of each of the fields listed and each of the fields from their (spreadable/non-upcasting) fragments.

In practice, fragments are as much a run time as they are a typing construct. We often have a need to deal with the data from a fragment independent of the query it came from, or even independent of any query.

This is even more important as we're working on an imperative store API similar to the one recently introduced in Apollo JavaScript. This means we should be able to read/write fragments without an associated query, so we do need the ability to instantiate a fragment as a stand-alone type.

I think we might be talking past each other slightly, I apologize…

My second comment specifically showed storing instance variables by allowing a fairly trivial struct to be created from a generic protocol implementor. That struct is similar to apollo-codegen's current output, and is always constructible from the protocols, so it's _certainly_ not more limited. You wouldn't be losing any capability that's already there, you'd only be gaining the ability to have more refined types available.

In practice, fragments are as much a run time as they are a typing construct. We often have a need to deal with the data from a fragment independent of the query it came from, or even independent of any query.

The proposal above does not add any restrictions to that. It only only removes them. Right now, for example, the structs generated for inline spreads (AsFoo) are completely unrelated in the type system of the generated swift code. Structs can't be inherited, and there are currently no protocols to conform to, so you're SOL if you want to do anything generic with GraphQL interfaces.

I might've been unclear in what I was proposing be generated, which is, for each fragment:

  • A protocol definition (with associated types for sub-fragments)
  • A convenience struct definition that minimally satisfies that fragment protocol.
  • A default toXXXStruct member for implementors of that fragment protocol to produce a concrete struct. Or the struct definition could just have a constructor.

This means that you still always have access to the general struct form (which is the only form currently generated by apollo-codegen), but are additionally able to have more specifically refined types, which is very useful. Additionally, you aren't restricted to a strictly descending taxonomy, which means the result of a selection set with multiple fragment spreads can just implement the protocols for each of those fragments.

Here's a motivating example, adapted (and contrived a bit) from a real schema and query:
Schema:

interface ProfileAttribute {
  id: ID!
  isVerified: Boolean!
  isMutable: Boolean!
}

type EmailAttribute implements ProfileAttribute {
  id: ID!
  isVerified: Boolean!
  isMutable: Boolean!
  emailAddress: String!
  normalizedEmailAddress: String!
}

type PhoneAttribute implements ProfileAttribute {
  id: ID!
  isVerified: Boolean!
  isMutable: Boolean!
  numberE164: String!
}

# …
# There are several more common fields
# and several more attribute types omitted.

type Profile {
  id: ID!
  primaryEmailAttribute: EmailAttribute!
  primaryPhoneAttribute: PhoneAttribute
  otherProfileAttributes: [ProfileAttribute!]
  # …
}

type Query {
  profile: Profile!
}

Then the query:

fragment ProfileAttributeFragment on ProfileAttribute {
  id
  isVerified
  ...on EmailAttribute {
    emailAddress
  }
  ...on PhoneAttribute {
    numberE164
  }
  # …
}

# This is slightly contrived, but helpful for illustration.
fragment MutabilityInfoFragment on ProfileAttributeFragment {
  isMutable
}

fragment ProfileFragment on Profile {
  id
  primaryEmailAttribute {
    ...ProfileAttributeFragment
    normalizedEmailAddress
  }
  primaryPhoneAttribute {
    ...ProfileAttributeFragment
  }
  otherProfileAttributes {
    ...ProfileAttributeFragment
  }
  # …
}

query GetFullProfile {
  profile {
    ...ProfileFragment
    primaryEmailAttribute {
      ...MutabilityInfoFragment
    }
  }
}

In the above, a few things to notice:

  • The profile {…} selection:

    • The selection set is guaranteed to match the on Profile type condition.

    • The selection set includes all of the fields from ProfileFragment.

    • (If we were to move the id field from ProfileFragment into the query itself, it still would, but would just have an additional field).

  • The primaryEmailAttribute {…} selection in ProfileFragment:

    • The selection set is guaranteed to match the on ProfileAttribute type condition.

    • Further, it's guaranteed to match the ...on EmailAttribute type condition within the ProfileAttributeFragment.

    • That means that the result, if any, is guaranteed to have a non-null .emailAddress property.

    • We also add the normalizedEmailAddress in addition to the fragment.

  • The primaryEmailAttribute fragment in GetFullProfile

    • We also add the fields from MutabilityInfoFragment

We could generate protocol definitions for this, as well as a general struct form. For any given use of a fragment, we can also return a type with additional guarantees based on its parentage in the schema, while still conforming to the very generic fragment protocol.

The following compiles fine and runs—sorry for the length…

//////// Fragment protocols {

protocol ProfileAttributeFragment {
  var id: String { get }
  var isVerified: Bool { get }
}
protocol ProfileAttributeFragment_AsEmailAttribute: ProfileAttributeFragment {
  var emailAddress: String { get }
}
protocol ProfileAttributeFragment_AsPhoneAttribute: ProfileAttributeFragment {
  var numberE164: String { get }
}

protocol ProfileFragment {
  var id: String { get }

  var primaryEmailAttribute: PrimaryEmailAttribute { get }
  var primaryPhoneAttribute: PrimaryPhoneAttribute? { get }
  var otherProfileAttributes: [OtherProfileAttributes] { get }

  associatedtype PrimaryEmailAttribute: ProfileAttributeFragment_AsEmailAttribute
  associatedtype PrimaryPhoneAttribute: ProfileAttributeFragment_AsPhoneAttribute
  associatedtype OtherProfileAttributes: ProfileAttributeFragment
}

protocol MutabilityInfoFragment {
  var isMutable: Bool { get }
}

//////// }

//////// Fragment structs (most general, least refined representation of fragments) {
//
// These structs conform to the protocols, but only consider the fields from
// the particular fragment, and require separate structs with duplicated data
// if there is more than one fragment spread into a selection set in a query.
// This is similar to the current apollo-codegen output.
//
// apollo-codegen currently uses structs for everything, which is fine if
// you have protocols for conformance checking, but it makes it impossible to
// subtype. In apollo-codegen, a ProfileAttributeFragment and
// ProfileAttributeFragment.AsEmailAttribute are not related in the type system
// _at all_. The following is similar, but the use of protocols allows them
// to be used with generic functions.
//

struct ProfileAttributeFragmentStruct: ProfileAttributeFragment {
  let id: String
  let isVerified: Bool

  let asEmailAttribute: AsEmailAttribute?
  let asPhoneAttribute: AsPhoneAttribute?

  // Note that ProfileAttributeFragmentStruct.AsEmailAttribute is not a subtype
  // of ProfileAttributeFragmentStruct (same restriction as current
  // apollo-codegen). It does, however, conform to both the
  // ProfileAttributeFragment and ProfileAttributeFragment_AsEmailAttribute
  // protocols.

  struct AsEmailAttribute: ProfileAttributeFragment_AsEmailAttribute {
    let id: String
    let isVerified: Bool
    let emailAddress: String

    var asEmailAttribute: AsEmailAttribute { return self }
    var asPhoneAttribute: AsPhoneAttribute? { return nil }
  }
  struct AsPhoneAttribute: ProfileAttributeFragment_AsPhoneAttribute {
    let id: String
    let isVerified: Bool
    let numberE164: String

    var asEmailAttribute: AsEmailAttribute? { return nil }
    var asPhoneAttribute: AsPhoneAttribute { return self }
  }
}

struct ProfileFragmentStruct: ProfileFragment {
  let id: String

  let primaryEmailAttribute: ProfileAttributeFragmentStruct.AsEmailAttribute
  let primaryPhoneAttribute: ProfileAttributeFragmentStruct.AsPhoneAttribute?
  let otherProfileAttributes: [ProfileAttributeFragmentStruct]

  typealias OtherProfileAttributes = ProfileAttributeFragmentStruct
}

struct MutabilityInfoFragmentStruct: MutabilityInfoFragment {
  let isMutable: Bool
}

//////// }

//////// Conversions from any selection set with a fragment to bare structs for that fragment:
//////// This is probably more verbose than it needs to be, but it would be generated anyway.

extension ProfileAttributeFragmentStruct {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct {
    return self
  }
}
extension ProfileAttributeFragment {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct {
    return ProfileAttributeFragmentStruct(
      id: self.id,
      isVerified: self.isVerified,
      asEmailAttribute: nil,
      asPhoneAttribute: nil
    )
  }
}
extension ProfileAttributeFragmentStruct.AsEmailAttribute {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct.AsEmailAttribute {
    return self
  }
}
extension ProfileAttributeFragment_AsEmailAttribute {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct.AsEmailAttribute {
    return ProfileAttributeFragmentStruct.AsEmailAttribute(
      id: self.id,
      isVerified: self.isVerified,
      emailAddress: self.emailAddress)
  }
}
extension ProfileAttributeFragmentStruct.AsPhoneAttribute {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct.AsPhoneAttribute {
    return self
  }
}
extension ProfileAttributeFragment_AsPhoneAttribute {
  var toProfileAttributeFragmentStruct: ProfileAttributeFragmentStruct.AsPhoneAttribute {
    return ProfileAttributeFragmentStruct.AsPhoneAttribute(
      id: self.id,
      isVerified: self.isVerified,
      numberE164: self.numberE164)
  }
}
extension ProfileFragmentStruct {
  var toProfileFragmentStruct: ProfileFragmentStruct {
    return self
  }
}
extension ProfileFragment {
  var toProfileFragmentStruct: ProfileFragmentStruct {
    return ProfileFragmentStruct(
      id: self.id,
      primaryEmailAttribute: self.primaryEmailAttribute.toProfileAttributeFragmentStruct,
      primaryPhoneAttribute: self.primaryPhoneAttribute?.toProfileAttributeFragmentStruct,
      otherProfileAttributes: self.otherProfileAttributes.map { $0.toProfileAttributeFragmentStruct }
    )
  }
}
extension MutabilityInfoFragmentStruct {
  var toMutabilityInfoFragmentStruct: MutabilityInfoFragmentStruct {
    return self
  }
}
extension MutabilityInfoFragment {
  var toMutabilityInfoFragmentStruct: MutabilityInfoFragmentStruct {
    return MutabilityInfoFragmentStruct(isMutable: self.isMutable)
  }
}

//////// }

//////// Query types (includes fragments as protocols, implemented with maximum information) {
// Notice that PrimaryEmailAttribute as the additional fields from the query.
// It has two fragments and one additional field (normalizedEmailAddress).

class GetFullProfile {
  struct Data {
    let profile: Profile
    struct Profile: ProfileFragment {
      let id: String
      let primaryEmailAttribute: PrimaryEmailAttribute
      let primaryPhoneAttribute: PrimaryPhoneAttribute?
      let otherProfileAttributes: [OtherProfileAttributes]

      // Implements ProfileAttributeFragment and ProfileAttributeFragment_AsEmailAttribute,
      // adds the additional fields (`normalizedEmailAddress`)
      // Can always call .toProfileAttributeFragmentStruct
      // or .toProfileAttributeFragmentStruct to get only that fragment field.
      struct PrimaryEmailAttribute: ProfileAttributeFragment_AsEmailAttribute, MutabilityInfoFragment {
        let id: String
        let isMutable: Bool
        let isVerified: Bool
        let emailAddress: String
        let normalizedEmailAddress: String
      }

      // Single spread fragment and no additional fields. In this case, could
      // just typealias directly to that minimal type.
      // Alternatively, this could replicate the structs explicitly here­they'd
      // still conform to the relevant protocols and would be convertible.
      typealias PrimaryPhoneAttribute = ProfileAttributeFragmentStruct.AsPhoneAttribute
      typealias OtherProfileAttributes = ProfileAttributeFragmentStruct
    }
  }
}
//////// }

There are a lot of benefits to that.

When accessing fields directly, you get the full type/nullability guarantees that the schema actually provides for that specific query. You can have structs that are still related via protocol conformance (so you don't have to duplicate code when dealing with inline spreads).

And you still have access to fragment-only struct forms.

let x = GetFullProfile.Data(
  profile: GetFullProfile.Data.Profile(
    id: "14",
    primaryEmailAttribute: GetFullProfile.Data.Profile.PrimaryEmailAttribute(
      id: "email-123",
      isMutable: true,
      isVerified: false,
      emailAddress: "[email protected]",
      normalizedEmailAddress: "[email protected]"
    ),
    primaryPhoneAttribute: GetFullProfile.Data.Profile.PrimaryPhoneAttribute(
      id: "phone-321",
      isVerified: false,
      numberE164: "+15551234567"
    ),
    otherProfileAttributes: []
  )
)
print(x)

// You can still use concrete types, you just have to do toXXXFragmentStruct.
// This is the same as the current requirement of accessing through
// apollo-codegen's `.fragments` field.
func printConcreteAttributeInfo(_ a: ProfileAttributeFragmentStruct) {
  print(a.id)
}
func printConcreteProfile(_ a: ProfileFragmentStruct) {
  print(a.id)
  print(a.primaryEmailAttribute.emailAddress)
    if let phone = a.primaryPhoneAttribute {
    print(phone.numberE164)
  }
}
printConcreteProfile(x.profile.toProfileFragmentStruct)
printConcreteAttributeInfo(x.profile.primaryEmailAttribute.toProfileAttributeFragmentStruct)
if let phone = x.profile.primaryPhoneAttribute {
  printConcreteAttributeInfo(phone.toProfileAttributeFragmentStruct)
}

// Generics now supported, useful for fragments used in multiple places, or for
// type-refined sub-fragments maintaining an is-a relationship via protocols:
func printGenericProfile<F: ProfileFragment>(_ a: F) {
  print(a.id)
  print(a.primaryEmailAttribute.emailAddress)
    if let phone = a.primaryPhoneAttribute {
    print(phone.numberE164)
  }
}
func printAttributeInfo<F: ProfileAttributeFragment>(_ a: F) {
  print(a.id)
}
func printSomethingWithMutabilityInfo<F: MutabilityInfoFragment>(_ a: F) {
  print(a.isMutable)
}

printGenericProfile(x.profile)
printAttributeInfo(x.profile.primaryEmailAttribute)
printAttributeInfo(x.profile.primaryEmailAttribute.toProfileAttributeFragmentStruct)

// Multiple fragments/protocols supported in a selection set:

printAttributeInfo(x.profile.primaryEmailAttribute)
printSomethingWithMutabilityInfo(x.profile.primaryEmailAttribute)
print(x.profile.primaryEmailAttribute.isMutable)

// Can access full fields directly without having to deal with every single fragment:
print(x.profile.primaryEmailAttribute.emailAddress)
print(x.profile.primaryEmailAttribute.toProfileAttributeFragmentStruct.emailAddress)
if let phone = x.profile.primaryPhoneAttribute {
  print(phone.toProfileAttributeFragmentStruct.numberE164)
  print(phone.numberE164)
}

I think we might be talking past each other slightly, I apologize…

I did get what you were proposing, but I should have commented on the second part explicitly to make that clear, sorry.

You wouldn't be losing any capability that's already there, you'd only be gaining the ability to have more refined types available.
You can still use concrete types, you just have to do toXXXFragmentStruct. This is the same as the current requirement of accessing through apollo-codegen's .fragments field.

This isn't really about losing a capability, but about adding complexity for users. I think asking people to use toXXXFragmentStruct every time they want to store a fragment in an instance variable, and to remember they'll have to type the variable as XXXFragmentStruct instead of XXXFragment, is too much of a burden and will make the code hard to understand.

Using .fragments has the benefit of offering a clear conceptual model; it is more than a workaround. If you see fragments not just as a way of reusing selections but as an abstraction mechanism, there is a benefit to Relay-like data masking, which hides data defined in fragments from the parent. This way, child components define their own data dependencies, and if parents need the same data they'll have to ask for it themselves.

Looking at the queries you used as an example, I can see how keeping fragments separate and accessing them through .fragments is a pain. But I would suggest simplifying them by getting rid of ProfileAttributeFragment and MutabilityInfoFragment, and by inlining selections:

fragment ProfileFragment on Profile {
  id
  primaryEmailAttribute {
    id
    isVerified
    emailAddress
    normalizedEmailAddress
  }
  primaryPhoneAttribute {
    id
    isVerified
    numberE164
  }
  otherProfileAttributes {
    id
    isVerified
  }
}

query GetFullProfile {
  profile {
    ...ProfileFragment
    primaryEmailAttribute {
      isMutable
    }
  }
}

I would argue the resulting query and fragment are a lot clearer and easier to deal with.

If you need to treat primaryEmailAttribute, primaryPhoneAttribute, and the individual otherProfileAttributes as instances of a shared type, you can still define a protocol and make them conform to it manually. I think that is perfectly acceptable; not everything needs to be solved at the GraphQL level.

Just came across this, sounds like it might solve our issues if it lands!

https://github.com/apple/swift/pull/8718

I think asking people to use toXXXFragmentStruct every time they want to store a fragment in an instance variable, and to remember they'll have to type the variable as XXXFragmentStruct instead of XXXFragment, is too much of a burden and will make the code hard to understand.

I believe the current API could be replicated by making a getter for .fragments.profileFragment which does the same operation as .toProfileFragmentStruct in the example above. This, I think, would allow all or nearly-all current code to behave the same.

I believe that the API could be designed to avoid requiring any user-facing complexity beyond the current generated code, but maybe I'm wrong.

As for refactoring the GraphQL query itself, I know that can be done—but it's undesirable for two reasons:

  1. It's inconvenient that pure-refactors of the GraphQL query (i.e. refactors that do not alter the guarantees of the returned structure) forces the user-facing code to also be refactored.
  2. In the real-life case of the example above, there are 6+ "ProfileAttribute" types, which are used either concretely or through the interface in nearly a dozen queries. (Sometimes directly, sometimes as children of another fragment). Being required to inline them is error-prone and completely negates the benefits of named fragments.

It does look like covariant protocol conformance would help here…but I'm not entirely certain. Structs need to have known sizes to be stored, so if I understand correctly, covariance protocol conformance would still require generics to be passed around. I might be misunderstanding that, though.

I believe the current API could be replicated by making a getter for .fragments.profileFragment which does the same operation as .toProfileFragmentStruct in the example above. This, I think, would allow all or nearly-all current code to behave the same.

Well, you'd still need to type the variable as XXXFragmentStruct instead of XXXFragment if you're storing the result or passing it around.

It does look like covariant protocol conformance would help here…but I'm not entirely certain. Structs need to have known sizes to be stored, so if I understand correctly, covariance protocol conformance would still require generics to be passed around. I might be misunderstanding that, though.

If you're referencing a struct through a protocol, that always requires an existential container, and covariance doesn't change that. There should be no need to make the type generic.

See here for a nice writeup of the difference between the use of an existential container and generic methods.

Was this page helpful?
0 / 5 - 0 ratings