From c26256f585ed17b698b68cf81f1939a61cd297aa Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Tue, 12 Aug 2025 12:00:03 -0700 Subject: [PATCH 1/4] add type validation for when deserializing --- test-codegen/Sources/testComplexQuery.graphql | 20 + test-codegen/Sources/testQuery.graphql | 12 + test-codegen/SwapiSchema/Package.swift | 28 + .../Sources/ApolloAPIExtensions.swift | 75 ++ .../Fragments/FilmFragment.graphql.swift | 30 + .../Fragments/NodeFragment.graphql.swift | 92 ++ .../Fragments/PlanetInfo.graphql.swift | 31 + .../Queries/TestComplexQuery.graphql.swift | 115 ++ .../Queries/TestQuery.graphql.swift | 115 ++ .../Sources/Schema/CustomScalars/ID.swift | 11 + .../Schema/Interfaces/Node.graphql.swift | 20 + .../Sources/Schema/Objects/Film.graphql.swift | 13 + .../Objects/FilmsConnection.graphql.swift | 13 + .../Schema/Objects/Person.graphql.swift | 13 + .../Schema/Objects/Planet.graphql.swift | 14 + .../Sources/Schema/Objects/Root.graphql.swift | 12 + .../Schema/Objects/Species.graphql.swift | 13 + .../Schema/Objects/Starship.graphql.swift | 13 + .../Schema/Objects/Vehicle.graphql.swift | 13 + .../Sources/Schema/SchemaConfiguration.swift | 15 + .../Schema/SchemaMetadata.graphql.swift | 38 + test-codegen/TestPackage/Package.resolved | 14 + test-codegen/TestPackage/Package.swift | 28 + .../TestPackage/Sources/TestPackage.swift | 6 + test-codegen/apollo-codegen-config.json | 29 + test-codegen/schema.graphqls | 1167 +++++++++++++++++ 26 files changed, 1950 insertions(+) create mode 100644 test-codegen/Sources/testComplexQuery.graphql create mode 100644 test-codegen/Sources/testQuery.graphql create mode 100644 test-codegen/SwapiSchema/Package.swift create mode 100644 test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift create mode 100644 test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift create mode 100644 test-codegen/TestPackage/Package.resolved create mode 100644 test-codegen/TestPackage/Package.swift create mode 100644 test-codegen/TestPackage/Sources/TestPackage.swift create mode 100644 test-codegen/apollo-codegen-config.json create mode 100644 test-codegen/schema.graphqls diff --git a/test-codegen/Sources/testComplexQuery.graphql b/test-codegen/Sources/testComplexQuery.graphql new file mode 100644 index 000000000..1b13b800c --- /dev/null +++ b/test-codegen/Sources/testComplexQuery.graphql @@ -0,0 +1,20 @@ +query TestComplexQuery($after: String, $before: String, $first: Int, $last: Int) { + allFilms(after: $after, before: $before, first: $first, last: $last) { + films { + ...FilmFragment + } + } +} + +fragment NodeFragment on Node { + id + ...on Person { + name + } + ...PlanetInfo +} + +fragment PlanetInfo on Planet { + name + orbitalPeriod +} \ No newline at end of file diff --git a/test-codegen/Sources/testQuery.graphql b/test-codegen/Sources/testQuery.graphql new file mode 100644 index 000000000..35d568c2f --- /dev/null +++ b/test-codegen/Sources/testQuery.graphql @@ -0,0 +1,12 @@ +query TestQuery($after: String, $before: String, $first: Int, $last: Int) { + allFilms(after: $after, before: $before, first: $first, last: $last) { + films { + ...FilmFragment + } + } +} + +fragment FilmFragment on Film { + director + episodeID +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Package.swift b/test-codegen/SwapiSchema/Package.swift new file mode 100644 index 000000000..2e820f0d5 --- /dev/null +++ b/test-codegen/SwapiSchema/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let package = Package( + name: "SwapiSchema", + platforms: [ + .iOS(.v12), + .macOS(.v10_14), + .tvOS(.v12), + .watchOS(.v5), + ], + products: [ + .library(name: "SwapiSchema", targets: ["SwapiSchema"]), + ], + dependencies: [ + .package(url: "https://github.com/apollographql/apollo-ios", exact: "1.23.0"), + ], + targets: [ + .target( + name: "SwapiSchema", + dependencies: [ + .product(name: "ApolloAPI", package: "apollo-ios"), + ], + path: "./Sources" + ), + ] +) diff --git a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift new file mode 100644 index 000000000..3dfac7fed --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift @@ -0,0 +1,75 @@ +import ApolloAPI + +public protocol Validatable { + static func validate(value: Self?) throws +} + +public enum ValidationError: Error { + case dataIsNil + case dataCorrupted +} + +extension String: Validatable { + public static func validate(value: String?) throws { + guard let value = value else { + throw ValidationError.dataIsNil + } + + guard let _ = value as? String else { + throw ValidationError.dataCorrupted + } + } +} + +extension Int: Validatable { + public static func validate(value: Int?) throws { + guard let value = value else { + throw ValidationError.dataIsNil + } + + guard let _ = value as? Int else { + throw ValidationError.dataCorrupted + } + } +} + + +extension Optional: Validatable where Wrapped: Validatable { + public static func validate(value: Wrapped??) throws { + switch value { + case .some(let value): + try Wrapped.validate(value: value) + case .none: + break + } + } +} + +extension Array: Validatable where Element: Validatable { + public static func validate(value: [Element]?) throws { + guard let value = value else { + throw ValidationError.dataIsNil + } + + for element in value { + try Element.validate(value: element) + } + } +} + +extension SelectionSet { + public func validate(_: T.Type, for key: String) throws { + let value: T? = self.__data[key] + try T.validate(value: value) + } + public func validate(_: T.Type, for key: String) throws { + let value: T? = self.__data[key] + try T.validate(value: value) + } +} + +extension Validatable { + public func validate() throws { + try Self.validate(value: self) + } +} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift new file mode 100644 index 000000000..5cb4bffbd --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift @@ -0,0 +1,30 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct FilmFragment: SwapiSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + #"fragment FilmFragment on Film { __typename director episodeID }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "director") + try value.validate(Int?.self, for: "episodeID") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("director", String?.self), + .field("episodeID", Int?.self), + ] } + + /// The name of the director of this film. + public var director: String? { __data["director"] } + /// The episode number of this film. + public var episodeID: Int? { __data["episodeID"] } +} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift new file mode 100644 index 000000000..9fd556d26 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift @@ -0,0 +1,92 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + #"fragment NodeFragment on Node { __typename id ... on Person { name } ...PlanetInfo }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(SwapiSchema.ID.self, for: "id") + try asPerson?.validate() + try asPlanet?.validate() + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Interfaces.Node } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("id", SwapiSchema.ID.self), + .inlineFragment(AsPerson.self), + .inlineFragment(AsPlanet.self), + ] } + + /// The id of the object. + public var id: SwapiSchema.ID { __data["id"] } + + public var asPerson: AsPerson? { _asInlineFragment() } + public var asPlanet: AsPlanet? { _asInlineFragment() } + + /// AsPerson + /// + /// Parent Type: `Person` + public struct AsPerson: SwapiSchema.InlineFragment, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "name") + try value.validate(SwapiSchema.ID.self, for: "id") + } + + public typealias RootEntityType = NodeFragment + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Person } + public static var __selections: [ApolloAPI.Selection] { [ + .field("name", String?.self), + ] } + + /// The name of this person. + public var name: String? { __data["name"] } + /// The id of the object. + public var id: SwapiSchema.ID { __data["id"] } + } + + /// AsPlanet + /// + /// Parent Type: `Planet` + public struct AsPlanet: SwapiSchema.InlineFragment, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(SwapiSchema.ID.self, for: "id") + try value.validate(String?.self, for: "name") + try value.validate(Int?.self, for: "orbitalPeriod") + } + + public typealias RootEntityType = NodeFragment + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .fragment(PlanetInfo.self), + ] } + + /// The id of the object. + public var id: SwapiSchema.ID { __data["id"] } + /// The name of this planet. + public var name: String? { __data["name"] } + /// The number of standard days it takes for this planet to complete a single orbit + /// of its local star. + public var orbitalPeriod: Int? { __data["orbitalPeriod"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var planetInfo: PlanetInfo { _toFragment() } + } + } +} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift new file mode 100644 index 000000000..e20f37163 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift @@ -0,0 +1,31 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct PlanetInfo: SwapiSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + #"fragment PlanetInfo on Planet { __typename name orbitalPeriod }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "name") + try value.validate(Int?.self, for: "orbitalPeriod") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("name", String?.self), + .field("orbitalPeriod", Int?.self), + ] } + + /// The name of this planet. + public var name: String? { __data["name"] } + /// The number of standard days it takes for this planet to complete a single orbit + /// of its local star. + public var orbitalPeriod: Int? { __data["orbitalPeriod"] } +} diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift new file mode 100644 index 000000000..51189a0ba --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift @@ -0,0 +1,115 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class TestComplexQuery: GraphQLQuery { + public static let operationName: String = "TestComplexQuery" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query TestComplexQuery($after: String, $before: String, $first: Int, $last: Int) { allFilms(after: $after, before: $before, first: $first, last: $last) { __typename films { __typename ...FilmFragment } } }"#, + fragments: [FilmFragment.self] + )) + + public var after: GraphQLNullable + public var before: GraphQLNullable + public var first: GraphQLNullable + public var last: GraphQLNullable + + public init( + after: GraphQLNullable, + before: GraphQLNullable, + first: GraphQLNullable, + last: GraphQLNullable + ) { + self.after = after + self.before = before + self.first = first + self.last = last + } + + public var __variables: Variables? { [ + "after": after, + "before": before, + "first": first, + "last": last + ] } + + public struct Data: SwapiSchema.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(AllFilms?.self, for: "allFilms") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } + public static var __selections: [ApolloAPI.Selection] { [ + .field("allFilms", AllFilms?.self, arguments: [ + "after": .variable("after"), + "before": .variable("before"), + "first": .variable("first"), + "last": .variable("last") + ]), + ] } + + public var allFilms: AllFilms? { __data["allFilms"] } + + /// AllFilms + /// + /// Parent Type: `FilmsConnection` + public struct AllFilms: SwapiSchema.SelectionSet, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate([Film?]?.self, for: "films") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("films", [Film?]?.self), + ] } + + /// A list of all of the objects returned in the connection. This is a convenience + /// field provided for quickly exploring the API; rather than querying for + /// "{ edges { node } }" when no edge data is needed, this field can be be used + /// instead. Note that when clients like Relay need to fetch the "cursor" field on + /// the edge to enable efficient pagination, this shortcut cannot be used, and the + /// full "{ edges { node } }" version should be used instead. + public var films: [Film?]? { __data["films"] } + + /// AllFilms.Film + /// + /// Parent Type: `Film` + public struct Film: SwapiSchema.SelectionSet, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "director") + try value.validate(Int?.self, for: "episodeID") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(FilmFragment.self), + ] } + + /// The name of the director of this film. + public var director: String? { __data["director"] } + /// The episode number of this film. + public var episodeID: Int? { __data["episodeID"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var filmFragment: FilmFragment { _toFragment() } + } + } + } + } +} diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift new file mode 100644 index 000000000..879c8053c --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift @@ -0,0 +1,115 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public class TestQuery: GraphQLQuery { + public static let operationName: String = "TestQuery" + public static let operationDocument: ApolloAPI.OperationDocument = .init( + definition: .init( + #"query TestQuery($after: String, $before: String, $first: Int, $last: Int) { allFilms(after: $after, before: $before, first: $first, last: $last) { __typename films { __typename ...FilmFragment } } }"#, + fragments: [FilmFragment.self] + )) + + public var after: GraphQLNullable + public var before: GraphQLNullable + public var first: GraphQLNullable + public var last: GraphQLNullable + + public init( + after: GraphQLNullable, + before: GraphQLNullable, + first: GraphQLNullable, + last: GraphQLNullable + ) { + self.after = after + self.before = before + self.first = first + self.last = last + } + + public var __variables: Variables? { [ + "after": after, + "before": before, + "first": first, + "last": last + ] } + + public struct Data: SwapiSchema.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(AllFilms?.self, for: "allFilms") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } + public static var __selections: [ApolloAPI.Selection] { [ + .field("allFilms", AllFilms?.self, arguments: [ + "after": .variable("after"), + "before": .variable("before"), + "first": .variable("first"), + "last": .variable("last") + ]), + ] } + + public var allFilms: AllFilms? { __data["allFilms"] } + + /// AllFilms + /// + /// Parent Type: `FilmsConnection` + public struct AllFilms: SwapiSchema.SelectionSet, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate([Film?]?.self, for: "films") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("films", [Film?]?.self), + ] } + + /// A list of all of the objects returned in the connection. This is a convenience + /// field provided for quickly exploring the API; rather than querying for + /// "{ edges { node } }" when no edge data is needed, this field can be be used + /// instead. Note that when clients like Relay need to fetch the "cursor" field on + /// the edge to enable efficient pagination, this shortcut cannot be used, and the + /// full "{ edges { node } }" version should be used instead. + public var films: [Film?]? { __data["films"] } + + /// AllFilms.Film + /// + /// Parent Type: `Film` + public struct Film: SwapiSchema.SelectionSet, Validatable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "director") + try value.validate(Int?.self, for: "episodeID") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .fragment(FilmFragment.self), + ] } + + /// The name of the director of this film. + public var director: String? { __data["director"] } + /// The episode number of this film. + public var episodeID: Int? { __data["episodeID"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var filmFragment: FilmFragment { _toFragment() } + } + } + } + } +} diff --git a/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift b/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift new file mode 100644 index 000000000..18e23c4cc --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift @@ -0,0 +1,11 @@ +// @generated +// This file was automatically generated and can be edited to +// implement advanced custom scalar functionality. +// +// Any changes to this file will not be overwritten by future +// code generation execution. + +import ApolloAPI + +/// The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. +public typealias ID = String diff --git a/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift new file mode 100644 index 000000000..229d73dc0 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift @@ -0,0 +1,20 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Interfaces { + /// An object with an ID + static let Node = ApolloAPI.Interface( + name: "Node", + keyFields: nil, + implementingObjects: [ + "Film", + "Person", + "Planet", + "Species", + "Starship", + "Vehicle" + ] + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift new file mode 100644 index 000000000..75a0c1c11 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A single film. + static let Film = ApolloAPI.Object( + typename: "Film", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift new file mode 100644 index 000000000..3ec6933c6 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A connection to a list of items. + static let FilmsConnection = ApolloAPI.Object( + typename: "FilmsConnection", + implementedInterfaces: [], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift new file mode 100644 index 000000000..8916b98da --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// An individual person or character within the Star Wars universe. + static let Person = ApolloAPI.Object( + typename: "Person", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift new file mode 100644 index 000000000..0a9b9dd64 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift @@ -0,0 +1,14 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A large mass, planet or planetoid in the Star Wars Universe, at the time of + /// 0 ABY. + static let Planet = ApolloAPI.Object( + typename: "Planet", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift new file mode 100644 index 000000000..23657f671 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift @@ -0,0 +1,12 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + static let Root = ApolloAPI.Object( + typename: "Root", + implementedInterfaces: [], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift new file mode 100644 index 000000000..a6e8d6dee --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A type of person or character within the Star Wars Universe. + static let Species = ApolloAPI.Object( + typename: "Species", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift new file mode 100644 index 000000000..9d3cc70ab --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A single transport craft that has hyperdrive capability. + static let Starship = ApolloAPI.Object( + typename: "Starship", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift new file mode 100644 index 000000000..d8b92ebfa --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift @@ -0,0 +1,13 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Objects { + /// A single transport craft that does not have hyperdrive capability + static let Vehicle = ApolloAPI.Object( + typename: "Vehicle", + implementedInterfaces: [Interfaces.Node.self], + keyFields: nil + ) +} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift b/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift new file mode 100644 index 000000000..87235012c --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift @@ -0,0 +1,15 @@ +// @generated +// This file was automatically generated and can be edited to +// provide custom configuration for a generated GraphQL schema. +// +// Any changes to this file will not be overwritten by future +// code generation execution. + +import ApolloAPI + +public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { + public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? { + // Implement this function to configure cache key resolution for your schema types. + return nil + } +} diff --git a/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift new file mode 100644 index 000000000..3ee5ac312 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift @@ -0,0 +1,38 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet +where Schema == SwapiSchema.SchemaMetadata {} + +public protocol InlineFragment: ApolloAPI.SelectionSet & ApolloAPI.InlineFragment +where Schema == SwapiSchema.SchemaMetadata {} + +public protocol MutableSelectionSet: ApolloAPI.MutableRootSelectionSet +where Schema == SwapiSchema.SchemaMetadata {} + +public protocol MutableInlineFragment: ApolloAPI.MutableSelectionSet & ApolloAPI.InlineFragment +where Schema == SwapiSchema.SchemaMetadata {} + +public enum SchemaMetadata: ApolloAPI.SchemaMetadata { + public static let configuration: any ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self + + public static func objectType(forTypename typename: String) -> ApolloAPI.Object? { + switch typename { + case "Film": return SwapiSchema.Objects.Film + case "FilmsConnection": return SwapiSchema.Objects.FilmsConnection + case "Person": return SwapiSchema.Objects.Person + case "Planet": return SwapiSchema.Objects.Planet + case "Root": return SwapiSchema.Objects.Root + case "Species": return SwapiSchema.Objects.Species + case "Starship": return SwapiSchema.Objects.Starship + case "Vehicle": return SwapiSchema.Objects.Vehicle + default: return nil + } + } +} + +public enum Objects {} +public enum Interfaces {} +public enum Unions {} diff --git a/test-codegen/TestPackage/Package.resolved b/test-codegen/TestPackage/Package.resolved new file mode 100644 index 000000000..48e3df3ed --- /dev/null +++ b/test-codegen/TestPackage/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "apollo-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apollographql/apollo-ios", + "state" : { + "revision" : "d591c1dd55824867877cff4f6551fe32983d7f51", + "version" : "1.23.0" + } + } + ], + "version" : 2 +} diff --git a/test-codegen/TestPackage/Package.swift b/test-codegen/TestPackage/Package.swift new file mode 100644 index 000000000..8c88fa88e --- /dev/null +++ b/test-codegen/TestPackage/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version:5.9 + +import PackageDescription + +let package = Package( + name: "TestPackage", + platforms: [ + .iOS(.v12), + .macOS(.v10_14), + .tvOS(.v12), + .watchOS(.v5), + ], + products: [ + .library(name: "TestPackage", targets: ["TestPackage"]), + ], + dependencies: [ + .package(path: "../SwapiSchema") + ], + targets: [ + .target( + name: "TestPackage", + dependencies: [ + .product(name: "SwapiSchema", package: "SwapiSchema"), + ], + path: "./Sources" + ), + ] +) diff --git a/test-codegen/TestPackage/Sources/TestPackage.swift b/test-codegen/TestPackage/Sources/TestPackage.swift new file mode 100644 index 000000000..d24165e11 --- /dev/null +++ b/test-codegen/TestPackage/Sources/TestPackage.swift @@ -0,0 +1,6 @@ + +import SwapiSchema + +func test(data: TestQuery.Data) { + data.allFilms?.films?.count +} diff --git a/test-codegen/apollo-codegen-config.json b/test-codegen/apollo-codegen-config.json new file mode 100644 index 000000000..ac4a60018 --- /dev/null +++ b/test-codegen/apollo-codegen-config.json @@ -0,0 +1,29 @@ +{ + "schemaNamespace" : "SwapiSchema", + "input" : { + "operationSearchPaths" : [ + "**/*.graphql" + ], + "schemaSearchPaths" : [ + "**/*.graphqls" + ] + }, + "output" : { + "testMocks" : { + "none" : { + } + }, + "schemaTypes" : { + "path" : "./SwapiSchema", + "moduleType" : { + "swiftPackageManager" : { + } + } + }, + "operations" : { + "inSchemaModule" : { + } + }, + "generateTypeValidation": true + } +} \ No newline at end of file diff --git a/test-codegen/schema.graphqls b/test-codegen/schema.graphqls new file mode 100644 index 000000000..680ad9fa5 --- /dev/null +++ b/test-codegen/schema.graphqls @@ -0,0 +1,1167 @@ +schema { + query: Root +} + +"""A single film.""" +type Film implements Node { + characterConnection(after: String, before: String, first: Int, last: Int): FilmCharactersConnection + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The name of the director of this film.""" + director: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """The episode number of this film.""" + episodeID: Int + + """The ID of an object""" + id: ID! + + """The opening paragraphs at the beginning of this film.""" + openingCrawl: String + planetConnection(after: String, before: String, first: Int, last: Int): FilmPlanetsConnection + + """The name(s) of the producer(s) of this film.""" + producers: [String] + + """The ISO 8601 date format of film release at original creator country.""" + releaseDate: String + speciesConnection(after: String, before: String, first: Int, last: Int): FilmSpeciesConnection + starshipConnection(after: String, before: String, first: Int, last: Int): FilmStarshipsConnection + + """The title of this film.""" + title: String + vehicleConnection(after: String, before: String, first: Int, last: Int): FilmVehiclesConnection +} + +"""A connection to a list of items.""" +type FilmCharactersConnection { + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + characters: [Person] + + """A list of edges.""" + edges: [FilmCharactersEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type FilmCharactersEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""A connection to a list of items.""" +type FilmPlanetsConnection { + """A list of edges.""" + edges: [FilmPlanetsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type FilmPlanetsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Planet +} + +"""A connection to a list of items.""" +type FilmSpeciesConnection { + """A list of edges.""" + edges: [FilmSpeciesEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type FilmSpeciesEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Species +} + +"""A connection to a list of items.""" +type FilmStarshipsConnection { + """A list of edges.""" + edges: [FilmStarshipsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type FilmStarshipsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Starship +} + +"""A connection to a list of items.""" +type FilmVehiclesConnection { + """A list of edges.""" + edges: [FilmVehiclesEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] +} + +"""An edge in a connection.""" +type FilmVehiclesEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Vehicle +} + +"""A connection to a list of items.""" +type FilmsConnection { + """A list of edges.""" + edges: [FilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type FilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""An object with an ID""" +interface Node { + """The id of the object.""" + id: ID! +} + +"""Information about pagination in a connection.""" +type PageInfo { + """When paginating forwards, the cursor to continue.""" + endCursor: String + + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String +} + +"""A connection to a list of items.""" +type PeopleConnection { + """A list of edges.""" + edges: [PeopleEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PeopleEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""An individual person or character within the Star Wars universe.""" +type Person implements Node { + """ + The birth year of the person, using the in-universe standard of BBY or ABY - + Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is + a battle that occurs at the end of Star Wars episode IV: A New Hope. + """ + birthYear: String + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """ + The eye color of this person. Will be "unknown" if not known or "n/a" if the + person does not have an eye. + """ + eyeColor: String + filmConnection(after: String, before: String, first: Int, last: Int): PersonFilmsConnection + + """ + The gender of this person. Either "Male", "Female" or "unknown", + "n/a" if the person does not have a gender. + """ + gender: String + + """ + The hair color of this person. Will be "unknown" if not known or "n/a" if the + person does not have hair. + """ + hairColor: String + + """The height of the person in centimeters.""" + height: Int + + """A planet that this person was born on or inhabits.""" + homeworld: Planet + + """The ID of an object""" + id: ID! + + """The mass of the person in kilograms.""" + mass: Float + + """The name of this person.""" + name: String + + """The skin color of this person.""" + skinColor: String + + """The species that this person belongs to, or null if unknown.""" + species: Species + starshipConnection(after: String, before: String, first: Int, last: Int): PersonStarshipsConnection + vehicleConnection(after: String, before: String, first: Int, last: Int): PersonVehiclesConnection +} + +"""A connection to a list of items.""" +type PersonFilmsConnection { + """A list of edges.""" + edges: [PersonFilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PersonFilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""A connection to a list of items.""" +type PersonStarshipsConnection { + """A list of edges.""" + edges: [PersonStarshipsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PersonStarshipsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Starship +} + +"""A connection to a list of items.""" +type PersonVehiclesConnection { + """A list of edges.""" + edges: [PersonVehiclesEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] +} + +"""An edge in a connection.""" +type PersonVehiclesEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Vehicle +} + +""" +A large mass, planet or planetoid in the Star Wars Universe, at the time of +0 ABY. +""" +type Planet implements Node { + """The climates of this planet.""" + climates: [String] + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The diameter of this planet in kilometers.""" + diameter: Int + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + filmConnection(after: String, before: String, first: Int, last: Int): PlanetFilmsConnection + + """ + A number denoting the gravity of this planet, where "1" is normal or 1 standard + G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. + """ + gravity: String + + """The ID of an object""" + id: ID! + + """The name of this planet.""" + name: String + + """ + The number of standard days it takes for this planet to complete a single orbit + of its local star. + """ + orbitalPeriod: Int + + """The average population of sentient beings inhabiting this planet.""" + population: Float + residentConnection(after: String, before: String, first: Int, last: Int): PlanetResidentsConnection + + """ + The number of standard hours it takes for this planet to complete a single + rotation on its axis. + """ + rotationPeriod: Int + + """ + The percentage of the planet surface that is naturally occuring water or bodies + of water. + """ + surfaceWater: Float + + """The terrains of this planet.""" + terrains: [String] +} + +"""A connection to a list of items.""" +type PlanetFilmsConnection { + """A list of edges.""" + edges: [PlanetFilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PlanetFilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""A connection to a list of items.""" +type PlanetResidentsConnection { + """A list of edges.""" + edges: [PlanetResidentsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + residents: [Person] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PlanetResidentsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""A connection to a list of items.""" +type PlanetsConnection { + """A list of edges.""" + edges: [PlanetsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + planets: [Planet] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type PlanetsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Planet +} + +type Root { + allFilms(after: String, before: String, first: Int, last: Int): FilmsConnection + allPeople(after: String, before: String, first: Int, last: Int): PeopleConnection + allPlanets(after: String, before: String, first: Int, last: Int): PlanetsConnection + allSpecies(after: String, before: String, first: Int, last: Int): SpeciesConnection + allStarships(after: String, before: String, first: Int, last: Int): StarshipsConnection + allVehicles(after: String, before: String, first: Int, last: Int): VehiclesConnection + film(filmID: ID, id: ID): Film + + """Fetches an object given its ID""" + node( + """The ID of an object""" + id: ID! + ): Node + person(id: ID, personID: ID): Person + planet(id: ID, planetID: ID): Planet + species(id: ID, speciesID: ID): Species + starship(id: ID, starshipID: ID): Starship + vehicle(id: ID, vehicleID: ID): Vehicle +} + +"""A type of person or character within the Star Wars Universe.""" +type Species implements Node { + """The average height of this species in centimeters.""" + averageHeight: Float + + """The average lifespan of this species in years, null if unknown.""" + averageLifespan: Int + + """The classification of this species, such as "mammal" or "reptile".""" + classification: String + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The designation of this species, such as "sentient".""" + designation: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + + """ + Common eye colors for this species, null if this species does not typically + have eyes. + """ + eyeColors: [String] + filmConnection(after: String, before: String, first: Int, last: Int): SpeciesFilmsConnection + + """ + Common hair colors for this species, null if this species does not typically + have hair. + """ + hairColors: [String] + + """A planet that this species originates from.""" + homeworld: Planet + + """The ID of an object""" + id: ID! + + """The language commonly spoken by this species.""" + language: String + + """The name of this species.""" + name: String + personConnection(after: String, before: String, first: Int, last: Int): SpeciesPeopleConnection + + """ + Common skin colors for this species, null if this species does not typically + have skin. + """ + skinColors: [String] +} + +"""A connection to a list of items.""" +type SpeciesConnection { + """A list of edges.""" + edges: [SpeciesEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + species: [Species] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type SpeciesEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Species +} + +"""A connection to a list of items.""" +type SpeciesFilmsConnection { + """A list of edges.""" + edges: [SpeciesFilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type SpeciesFilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""A connection to a list of items.""" +type SpeciesPeopleConnection { + """A list of edges.""" + edges: [SpeciesPeopleEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + people: [Person] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type SpeciesPeopleEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""A single transport craft that has hyperdrive capability.""" +type Starship implements Node { + """ + The Maximum number of Megalights this starship can travel in a standard hour. + A "Megalight" is a standard unit of distance and has never been defined before + within the Star Wars universe. This figure is only really useful for measuring + the difference in speed of starships. We can assume it is similar to AU, the + distance between our Sun (Sol) and Earth. + """ + MGLT: Int + + """The maximum number of kilograms that this starship can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this starship can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + + """The cost of this starship new, in galactic credits.""" + costInCredits: Float + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The number of personnel needed to run or pilot this starship.""" + crew: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + filmConnection(after: String, before: String, first: Int, last: Int): StarshipFilmsConnection + + """The class of this starships hyperdrive.""" + hyperdriveRating: Float + + """The ID of an object""" + id: ID! + + """The length of this starship in meters.""" + length: Float + + """The manufacturers of this starship.""" + manufacturers: [String] + + """ + The maximum speed of this starship in atmosphere. null if this starship is + incapable of atmosphering flight. + """ + maxAtmospheringSpeed: Int + + """ + The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 + Orbital Battle Station". + """ + model: String + + """The name of this starship. The common name, such as "Death Star".""" + name: String + + """The number of non-essential people this starship can transport.""" + passengers: String + pilotConnection(after: String, before: String, first: Int, last: Int): StarshipPilotsConnection + + """ + The class of this starship, such as "Starfighter" or "Deep Space Mobile + Battlestation" + """ + starshipClass: String +} + +"""A connection to a list of items.""" +type StarshipFilmsConnection { + """A list of edges.""" + edges: [StarshipFilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type StarshipFilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""A connection to a list of items.""" +type StarshipPilotsConnection { + """A list of edges.""" + edges: [StarshipPilotsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type StarshipPilotsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""A connection to a list of items.""" +type StarshipsConnection { + """A list of edges.""" + edges: [StarshipsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + starships: [Starship] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type StarshipsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Starship +} + +"""A single transport craft that does not have hyperdrive capability""" +type Vehicle implements Node { + """The maximum number of kilograms that this vehicle can transport.""" + cargoCapacity: Float + + """ + The maximum length of time that this vehicle can provide consumables for its + entire crew without having to resupply. + """ + consumables: String + + """The cost of this vehicle new, in Galactic Credits.""" + costInCredits: Float + + """The ISO 8601 date format of the time that this resource was created.""" + created: String + + """The number of personnel needed to run or pilot this vehicle.""" + crew: String + + """The ISO 8601 date format of the time that this resource was edited.""" + edited: String + filmConnection(after: String, before: String, first: Int, last: Int): VehicleFilmsConnection + + """The ID of an object""" + id: ID! + + """The length of this vehicle in meters.""" + length: Float + + """The manufacturers of this vehicle.""" + manufacturers: [String] + + """The maximum speed of this vehicle in atmosphere.""" + maxAtmospheringSpeed: Int + + """ + The model or official name of this vehicle. Such as "All-Terrain Attack + Transport". + """ + model: String + + """ + The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder + bike". + """ + name: String + + """The number of non-essential people this vehicle can transport.""" + passengers: String + pilotConnection(after: String, before: String, first: Int, last: Int): VehiclePilotsConnection + + """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" + vehicleClass: String +} + +"""A connection to a list of items.""" +type VehicleFilmsConnection { + """A list of edges.""" + edges: [VehicleFilmsEdge] + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + films: [Film] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type VehicleFilmsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Film +} + +"""A connection to a list of items.""" +type VehiclePilotsConnection { + """A list of edges.""" + edges: [VehiclePilotsEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + pilots: [Person] + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int +} + +"""An edge in a connection.""" +type VehiclePilotsEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Person +} + +"""A connection to a list of items.""" +type VehiclesConnection { + """A list of edges.""" + edges: [VehiclesEdge] + + """Information to aid in pagination.""" + pageInfo: PageInfo! + + """ + A count of the total number of objects in this connection, ignoring pagination. + This allows a client to fetch the first five objects by passing "5" as the + argument to "first", then fetch the total count so it could display "5 of 83", + for example. + """ + totalCount: Int + + """ + A list of all of the objects returned in the connection. This is a convenience + field provided for quickly exploring the API; rather than querying for + "{ edges { node } }" when no edge data is needed, this field can be be used + instead. Note that when clients like Relay need to fetch the "cursor" field on + the edge to enable efficient pagination, this shortcut cannot be used, and the + full "{ edges { node } }" version should be used instead. + """ + vehicles: [Vehicle] +} + +"""An edge in a connection.""" +type VehiclesEdge { + """A cursor for use in pagination""" + cursor: String! + + """The item at the end of the edge""" + node: Vehicle +} From 2719419a84ab28c1b4b1c1d08e91864f54a1f587 Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Tue, 12 Aug 2025 12:19:29 -0700 Subject: [PATCH 2/4] fix --- test-codegen/Sources/testComplexQuery.graphql | 1 + .../Sources/ApolloAPIExtensions.swift | 28 ++++++------------- .../Fragments/NodeFragment.graphql.swift | 10 +++++-- .../Schema/Enums/GoodOrBad.graphql.swift | 9 ++++++ test-codegen/schema.graphqls | 7 +++++ 5 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift diff --git a/test-codegen/Sources/testComplexQuery.graphql b/test-codegen/Sources/testComplexQuery.graphql index 1b13b800c..97f2bb491 100644 --- a/test-codegen/Sources/testComplexQuery.graphql +++ b/test-codegen/Sources/testComplexQuery.graphql @@ -10,6 +10,7 @@ fragment NodeFragment on Node { id ...on Person { name + goodOrBad } ...PlanetInfo } diff --git a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift index 3dfac7fed..262caca2f 100644 --- a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift +++ b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift @@ -8,31 +8,21 @@ public enum ValidationError: Error { case dataIsNil case dataCorrupted } - -extension String: Validatable { - public static func validate(value: String?) throws { - guard let value = value else { - throw ValidationError.dataIsNil - } - - guard let _ = value as? String else { - throw ValidationError.dataCorrupted - } - } -} -extension Int: Validatable { - public static func validate(value: Int?) throws { +extension AnyScalarType { + public static func validate(value: Self?) throws { guard let value = value else { throw ValidationError.dataIsNil } - - guard let _ = value as? Int else { - throw ValidationError.dataCorrupted - } } } +extension String: Validatable {} +extension Int: Validatable {} +extension Bool: Validatable {} +extension Float: Validatable {} +extension Double: Validatable {} +extension GraphQLEnum: Validatable {} extension Optional: Validatable where Wrapped: Validatable { public static func validate(value: Wrapped??) throws { @@ -57,7 +47,7 @@ extension Array: Validatable where Element: Validatable { } } -extension SelectionSet { +extension ApolloAPI.SelectionSet { public func validate(_: T.Type, for key: String) throws { let value: T? = self.__data[key] try T.validate(value: value) diff --git a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift index 9fd556d26..331950618 100644 --- a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift @@ -5,7 +5,7 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public static var fragmentDefinition: StaticString { - #"fragment NodeFragment on Node { __typename id ... on Person { name } ...PlanetInfo }"# + #"fragment NodeFragment on Node { __typename id ... on Person { name goodOrBad } ...PlanetInfo }"# } public let __data: DataDict @@ -13,8 +13,8 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public static func validate(value: Self?) throws { guard let value else { throw ValidationError.dataIsNil } try value.validate(SwapiSchema.ID.self, for: "id") - try asPerson?.validate() - try asPlanet?.validate() + try value.asPerson?.validate() + try value.asPlanet?.validate() } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Interfaces.Node } @@ -40,6 +40,7 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public static func validate(value: Self?) throws { guard let value else { throw ValidationError.dataIsNil } try value.validate(String?.self, for: "name") + try value.validate(GraphQLEnum?.self, for: "goodOrBad") try value.validate(SwapiSchema.ID.self, for: "id") } @@ -47,10 +48,13 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Person } public static var __selections: [ApolloAPI.Selection] { [ .field("name", String?.self), + .field("goodOrBad", GraphQLEnum?.self), ] } /// The name of this person. public var name: String? { __data["name"] } + /// Whether this is a good person or a bad one + public var goodOrBad: GraphQLEnum? { __data["goodOrBad"] } /// The id of the object. public var id: SwapiSchema.ID { __data["id"] } } diff --git a/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift new file mode 100644 index 000000000..6c83ad658 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift @@ -0,0 +1,9 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public enum GoodOrBad: String, EnumType { + case good = "GOOD" + case bad = "BAD" +} diff --git a/test-codegen/schema.graphqls b/test-codegen/schema.graphqls index 680ad9fa5..97c4cb547 100644 --- a/test-codegen/schema.graphqls +++ b/test-codegen/schema.graphqls @@ -367,6 +367,8 @@ type Person implements Node { species: Species starshipConnection(after: String, before: String, first: Int, last: Int): PersonStarshipsConnection vehicleConnection(after: String, before: String, first: Int, last: Int): PersonVehiclesConnection + """Whether this is a good person or a bad one""" + goodOrBad: GoodOrBad } """A connection to a list of items.""" @@ -1165,3 +1167,8 @@ type VehiclesEdge { """The item at the end of the edge""" node: Vehicle } + +enum GoodOrBad { + GOOD + BAD +} \ No newline at end of file From 18cd657e8502cf25e4626ff077eafc480db5d166 Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Tue, 12 Aug 2025 19:30:45 -0700 Subject: [PATCH 3/4] wip validate / decode --- .../SelectionSetTemplateTests.swift | 13 + ...ctionSetTemplate_ErrorHandling_Tests.swift | 2 + ...ectionSetTemplate_FieldMerging_Tests.swift | 1 + ...ectionSetTemplate_Initializers_Tests.swift | 2 + ...SetTemplate_LocalCacheMutation_Tests.swift | 1 + ...plateString_DeprecationMessage_Tests.swift | 2 + .../ApolloCodegenConfiguration.swift | 17 +- .../Templates/FragmentTemplate.swift | 2 + ...LocalCacheMutationDefinitionTemplate.swift | 1 + .../OperationDefinitionTemplate.swift | 1 + .../Templates/SelectionSetTemplate.swift | 96 ++++- apollo-ios/Sources/ApolloAPI/DataDict.swift | 81 +++- .../Sources/ApolloAPI/SelectionSet.swift | 4 + test-codegen/Sources/testComplexQuery.graphql | 20 + .../Sources/ApolloAPIExtensions.swift | 39 +- .../Fragments/FilmFragment.graphql.swift | 18 +- .../Fragments/NodeFragment.graphql.swift | 183 +++++++- .../PersonOrPlanetInfo.graphql.swift | 200 +++++++++ .../Fragments/PlanetInfo.graphql.swift | 18 +- test-codegen/SwapiSchema/Sources/JSON.swift | 396 ++++++++++++++++++ .../Queries/TestComplexQuery.graphql.swift | 48 ++- .../Queries/TestQuery.graphql.swift | 48 ++- .../Unions/PersonOrPlanet.graphql.swift | 14 + test-codegen/TestPackage/Package.resolved | 14 - test-codegen/TestPackage/Package.swift | 11 +- .../TestPackage/Tests/CodableTests.swift | 152 +++++++ .../TestPackage/Tests/JSON+helpers.swift | 106 +++++ test-codegen/apollo-codegen-config.json | 2 +- test-codegen/schema.graphqls | 7 +- 29 files changed, 1468 insertions(+), 31 deletions(-) create mode 100644 test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift create mode 100644 test-codegen/SwapiSchema/Sources/JSON.swift create mode 100644 test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift delete mode 100644 test-codegen/TestPackage/Package.resolved create mode 100644 test-codegen/TestPackage/Tests/CodableTests.swift create mode 100644 test-codegen/TestPackage/Tests/JSON+helpers.swift diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 9677a3f8b..4cedfc3da 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -58,6 +58,7 @@ class SelectionSetTemplateTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, + generateTypeValidation: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -3333,6 +3334,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3411,6 +3413,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3499,6 +3502,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3587,6 +3591,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3675,6 +3680,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3768,6 +3774,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3867,6 +3874,7 @@ class SelectionSetTemplateTests: XCTestCase { let basicFragmentSubject = SelectionSetTemplate( definition: allAnimals_basicFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3959,6 +3967,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -4051,6 +4060,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9684,6 +9694,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9769,6 +9780,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -11940,6 +11952,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentTemplate = SelectionSetTemplate( definition: detailsFragment.fragment, generateInitializers: false, + generateTypeValidation: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift index f7a549c91..7fc9b1986 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift @@ -56,6 +56,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, + generateTypeValidation: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -79,6 +80,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, + generateTypeValidation: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift index d381d0c64..c242e2464 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift @@ -67,6 +67,7 @@ class SelectionSetTemplate_FieldMerging_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: selectionSetInitializers, + generateTypeValidation: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift index aeda3eb5b..d7edd442e 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift @@ -49,6 +49,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, + generateTypeValidation: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -77,6 +78,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, + generateTypeValidation: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift index 255cb4c4d..c3b6b4b86 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift @@ -50,6 +50,7 @@ class SelectionSetTemplate_LocalCacheMutationTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, + generateTypeValidation: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift index 2931c09cd..d0efbdbea 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift @@ -273,6 +273,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, + generateTypeValidation: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() @@ -403,6 +404,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, + generateTypeValidation: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift index 9810e006f..0772c9444 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift @@ -165,6 +165,8 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public let operations: OperationsFileOutput /// The local path structure for the test mock operation object files. public let testMocks: TestMockFileOutput + /// Whether to generate type validation for the generated code. + public let generateTypeValidation: Bool /// This var helps maintain backwards compatibility with legacy operation manifest generation /// with the new `OperationManifestConfiguration` and will be fully removed in v2.0 @@ -174,6 +176,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public struct Default { public static let operations: OperationsFileOutput = .inSchemaModule public static let testMocks: TestMockFileOutput = .none + public static let generateTypeValidation: Bool = false } /// Designated initializer. @@ -188,15 +191,18 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// with persisted queries or /// [Automatic Persisted Queries (APQs)](https://www.apollographql.com/docs/apollo-server/performance/apq). /// Defaults to `nil`. + /// - generateTypeValidation: Whether to generate type validation for the generated code. public init( schemaTypes: SchemaTypesFileOutput, operations: OperationsFileOutput = Default.operations, - testMocks: TestMockFileOutput = Default.testMocks + testMocks: TestMockFileOutput = Default.testMocks, + generateTypeValidation: Bool = Default.generateTypeValidation ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = nil + self.generateTypeValidation = generateTypeValidation } // MARK: Codable @@ -206,6 +212,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { case operations case testMocks case operationIdentifiersPath + case generateTypeValidation } /// `Decodable` implementation to allow for properties to be optional in the encoded JSON with @@ -230,6 +237,10 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { String.self, forKey: .operationIdentifiersPath ) + generateTypeValidation = try values.decodeIfPresent( + Bool.self, + forKey: .generateTypeValidation + ) ?? Default.generateTypeValidation } public func encode(to encoder: any Encoder) throws { @@ -238,6 +249,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { try container.encode(self.schemaTypes, forKey: .schemaTypes) try container.encode(self.operations, forKey: .operations) try container.encode(self.testMocks, forKey: .testMocks) + try container.encode(self.generateTypeValidation, forKey: .generateTypeValidation) } } @@ -1698,18 +1710,21 @@ extension ApolloCodegenConfiguration.FileOutput { /// If `.none`, test mocks will not be generated. Defaults to `.none`. /// - operationIdentifiersPath: An absolute location to an operation id JSON map file /// for use with APQ registration. Defaults to `nil`. + /// - generateTypeValidation: Whether to generate type validation for the generated code. @available(*, deprecated, renamed: "init(schemaTypes:operations:testMocks:)") @_disfavoredOverload public init( schemaTypes: ApolloCodegenConfiguration.SchemaTypesFileOutput, operations: ApolloCodegenConfiguration.OperationsFileOutput = Default.operations, testMocks: ApolloCodegenConfiguration.TestMockFileOutput = Default.testMocks, + generateTypeValidation: Bool = Default.generateTypeValidation, operationIdentifiersPath: String? ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = operationIdentifiersPath + self.generateTypeValidation = generateTypeValidation } /// An absolute location to an operation id JSON map file. diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift index 4bd4f5f6a..7aee98c4b 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift @@ -25,6 +25,7 @@ struct FragmentTemplate: TemplateRenderer { struct \(fragment.generatedDefinitionName.asFragmentName): \ \(fragment.renderedSelectionSetType(config)), Fragment\ \(if: fragment.isIdentifiable, ", Identifiable")\ + \(if: config.config.output.generateTypeValidation, ", Validatable, Codable")\ { \(if: includeDefinition, """ \(accessControlModifier(for: .member))\ @@ -36,6 +37,7 @@ struct FragmentTemplate: TemplateRenderer { \(SelectionSetTemplate( definition: fragment, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: fragment), + generateTypeValidation: config.config.output.generateTypeValidation, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift index 8f0483c15..96903be27 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift @@ -33,6 +33,7 @@ struct LocalCacheMutationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), + generateTypeValidation: config.config.output.generateTypeValidation, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index 7b25cc5b1..2f55eaa30 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -36,6 +36,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), + generateTypeValidation: config.config.output.generateTypeValidation, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index b5bda9593..c4fd44a0a 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -9,6 +9,7 @@ struct SelectionSetTemplate { let definition: any IR.Definition let generateInitializers: Bool + let generateTypeValidation: Bool let config: ApolloCodegen.ConfigurationContext let nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder let renderAccessControl: () -> String @@ -20,12 +21,14 @@ struct SelectionSetTemplate { init( definition: any IR.Definition, generateInitializers: Bool, + generateTypeValidation: Bool, config: ApolloCodegen.ConfigurationContext, nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder, renderAccessControl: @autoclosure @escaping () -> String ) { self.definition = definition self.generateInitializers = generateInitializers + self.generateTypeValidation = generateTypeValidation self.config = config self.nonFatalErrorRecorder = nonFatalErrorRecorder self.renderAccessControl = renderAccessControl @@ -104,6 +107,7 @@ struct SelectionSetTemplate { \(renderAccessControl())\ struct \(fieldSelectionSetName): \(SelectionSetType())\ \(if: selectionSet.isIdentifiable, ", Identifiable")\ + \(if: config.output.generateTypeValidation, ", Decodable")\ { \(BodyTemplate(context)) } @@ -121,6 +125,7 @@ struct SelectionSetTemplate { struct \(inlineFragment.renderedTypeName): \(SelectionSetType(asInlineFragment: true))\ \(if: inlineFragment.isCompositeInlineFragment, ", \(config.ApolloAPITargetName).CompositeInlineFragment")\ \(if: inlineFragment.isIdentifiable, ", Identifiable")\ + \(if: config.output.generateTypeValidation, ", Validatable, Codable")\ { \(BodyTemplate(context)) } @@ -171,6 +176,8 @@ struct SelectionSetTemplate { return """ \(DataPropertyTemplate()) \(DesignatedInitializerTemplate()) + \(section: "\(if: generateTypeValidation, ValidatorTemplate(selectionSet, inlineFragments: computedChildSelectionSets))") + \(section: "\(if: generateTypeValidation, CodableTemplate(selectionSet, inlineFragments: computedChildSelectionSets))") \(RootEntityTypealias(selectionSet)) \(ParentTypeTemplate(selectionSet.parentType)) @@ -190,7 +197,7 @@ struct SelectionSetTemplate { \(section: ChildTypeCaseSelectionSets(computedChildSelectionSets)) """ } - + private func DesignatedInitializerTemplate( _ propertiesTemplate: @autoclosure () -> TemplateString? = { nil }() ) -> String { @@ -212,6 +219,82 @@ struct SelectionSetTemplate { """ ).description } + + private func ValidatorTemplate(_ selectionSet: ComputedSelectionSet, inlineFragments: [SelectionSetContext]) -> TemplateString { + let scope = selectionSet.typeInfo.scope + return """ + \(renderAccessControl())static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + \(selectionSet.allFields.map { field in + """ + try value.validate(\(typeName(for: field, forceOptional: field.isConditionallyIncluded(in: scope))).self, for: "\(field.responseKey)") + """ + }, separator: "\n") + \(inlineFragments.map{ InlineFragmentValidatorTemplate($0.selectionSet) }, separator: "\n") + } + """ + } + + private func InlineFragmentValidatorTemplate( + _ inlineFragment: IR.ComputedSelectionSet + ) -> TemplateString { + guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } + + let typeName = inlineFragment.renderedTypeName + return """ + try value.\(typeName.firstLowercased)?.validate() + """ + } + + private func CodableTemplate(_ selectionSet: ComputedSelectionSet, inlineFragments: [SelectionSetContext]) -> TemplateString { + let scope = selectionSet.typeInfo.scope + return """ + \(renderAccessControl())init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + \(selectionSet.allFields.map { field in + """ + "\(field.responseKey)": try container.decode(\(typeName(for: field, forceOptional: field.isConditionallyIncluded(in: scope))).self, forKey: "\(field.responseKey)") + """ + }, separator: ",\n") + ], fulfilledFragments: [ + \(inlineFragments.map{ InlineFragmentDecodableTemplate($0.selectionSet) }, separator: ",\n") + ]) + } + + \(renderAccessControl())func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + \(selectionSet.allFields.map { field in + """ + try container.encode(\(field.responseKey.renderAsFieldPropertyName(config: config.config)), forKey: "\(field.responseKey)") + """ + }, separator: "\n") + \(inlineFragments.map{ InlineFragmentEncodableTemplate($0.selectionSet) }, separator: "\n") + } + """ + } + + private func InlineFragmentDecodableTemplate( + _ inlineFragment: IR.ComputedSelectionSet + ) -> TemplateString { + guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } + + let typeName = inlineFragment.renderedTypeName + return "(try? \(typeName)(from: decoder)) != nil ? ObjectIdentifier(\(typeName).self): nil" + } + + private func InlineFragmentEncodableTemplate( + _ inlineFragment: IR.ComputedSelectionSet + ) -> TemplateString { + guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } + + let typeName = inlineFragment.renderedTypeName + return """ + try self.\(typeName.firstLowercased)?.encode(to: encoder) + """ + } private func DataPropertyTemplate() -> TemplateString { "\(renderAccessControl())\(isMutable ? "var" : "let") __data: DataDict" @@ -1262,3 +1345,14 @@ extension OrderedDictionary { values.contains(where: { $0.typeInfo.deferCondition != nil }) } } + +extension ComputedSelectionSet { + var allFields: [IR.Field] { + var fields: [IR.Field] = [] + if let directFields = direct?.fields.values { + fields.append(contentsOf: directFields) + } + fields.append(contentsOf: merged.fields.values) + return fields + } +} diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index e6f53873a..39df12b92 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -68,7 +68,7 @@ public struct DataDict: Hashable { get { if DataDict._AnyHashableCanBeCoerced { return _data[key] as! T - } else { + } else { let value = _data[key] if value == DataDict._NullValue { return (Optional.none as Any) as! T @@ -183,6 +183,85 @@ extension DataDict { } +// MARK: - Encodable +struct StringKey: CodingKey { + let stringValue: String + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init(_ key: String) { + self.stringValue = key + } + + var intValue: Int? { nil } + + init?(intValue: Int) { + nil + } + +} + +extension DataDict: @retroactive Encodable { + public func encode(to encoder: any Encoder) throws { + let keys = _data.keys.sorted() + var container = encoder.container(keyedBy: StringKey.self) + for key in keys { + if let value = _data[key] { + try value.encode(in: &container, at: StringKey(key)) + } + } + } +} + +extension AnyHashable { + func encode(in container: inout KeyedEncodingContainer, at key: StringKey) throws { + if let encodableValue = self.base as? Encodable { + try encodableValue.encode(to: container.superEncoder(forKey: key)) + } else if let array = self.base as? [AnyHashable] { + var arrayContainer = container.nestedUnkeyedContainer(forKey: key) + for item in array { + try item.encode(in: &arrayContainer) + } + } else if let array = self.base as? [AnyHashable?] { + var arrayContainer = container.nestedUnkeyedContainer(forKey: key) + for item in array { + if let item = item { + try item.encode(in: &arrayContainer) + } else { + try arrayContainer.encodeNil() + } + } + } else { + throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) + } + } + + func encode(in container: inout UnkeyedEncodingContainer) throws { + if let encodableValue = self.base as? Encodable { + try encodableValue.encode(to: container.superEncoder()) + } else if let array = self.base as? [AnyHashable] { + var arrayContainer = container.nestedUnkeyedContainer() + for item in array { + try item.encode(in: &arrayContainer) + } + } else if let array = self.base as? [AnyHashable?] { + var arrayContainer = container.nestedUnkeyedContainer() + for item in array { + if let item = item { + try item.encode(in: &arrayContainer) + } else { + try arrayContainer.encodeNil() + } + } + } else { + throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) + } + } +} + + // MARK: - Value Conversion Helpers public protocol SelectionSetEntityValue { diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift index c64f2b6d5..c705f4805 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift @@ -121,6 +121,10 @@ extension SelectionSet { public var debugDescription: String { return "\(self.__data._data as AnyObject)" } + + public func encode(to encoder: Encoder) throws { + try self.__data.encode(to: encoder) + } } extension SelectionSet where Fragments: FragmentContainer { diff --git a/test-codegen/Sources/testComplexQuery.graphql b/test-codegen/Sources/testComplexQuery.graphql index 97f2bb491..ec79b6a27 100644 --- a/test-codegen/Sources/testComplexQuery.graphql +++ b/test-codegen/Sources/testComplexQuery.graphql @@ -11,11 +11,31 @@ fragment NodeFragment on Node { ...on Person { name goodOrBad + homeworld { + name + } } ...PlanetInfo + ...PersonOrPlanetInfo } fragment PlanetInfo on Planet { name orbitalPeriod +} + +fragment PersonOrPlanetInfo on PersonOrPlanet { + ...on Person { + nestedStringArray + nestedPlanetArray { + name + } + homeworld { + climates + } + } + ...on Planet { + climates + diameter + } } \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift index 262caca2f..41038a636 100644 --- a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift +++ b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift @@ -11,7 +11,7 @@ public enum ValidationError: Error { extension AnyScalarType { public static func validate(value: Self?) throws { - guard let value = value else { + guard value != nil else { throw ValidationError.dataIsNil } } @@ -63,3 +63,40 @@ extension Validatable { try Self.validate(value: self) } } + +// MARK - Codable + +//extension String: @retroactive CodingKey { +// public var stringValue: String { +// self +// } +// public var intValue: Int? { +// nil +// } +// public init?(intValue: Int) { +// nil +// } +// public init?(stringValue: String) { +// self = stringValue +// } +//} + +extension GraphQLEnum: Codable { + +} + +extension DataDict { + public init( + data: [String: AnyHashable], + fulfilledFragments: [ObjectIdentifier?], + deferredFragments: [ObjectIdentifier?] = [] + ) { + self.init(data: data, fulfilledFragments: Set(fulfilledFragments.compactMap { $0 }), deferredFragments: Set(deferredFragments.compactMap { $0 })) + } +} + +extension ApolloAPI.SelectionSet { + public func encode(to encoder: Encoder) throws { + try self.__data.encode(to: encoder) + } +} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift index 5cb4bffbd..04b459497 100644 --- a/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift @@ -3,7 +3,7 @@ @_exported import ApolloAPI -public struct FilmFragment: SwapiSchema.SelectionSet, Fragment { +public struct FilmFragment: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { public static var fragmentDefinition: StaticString { #"fragment FilmFragment on Film { __typename director episodeID }"# } @@ -15,6 +15,22 @@ public struct FilmFragment: SwapiSchema.SelectionSet, Fragment { try value.validate(String?.self, for: "director") try value.validate(Int?.self, for: "episodeID") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "director": try container.decode(String?.self, forKey: "director"), + "episodeID": try container.decode(Int?.self, forKey: "episodeID") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(director, forKey: "director") + try container.encode(episodeID, forKey: "episodeID") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } public static var __selections: [ApolloAPI.Selection] { [ diff --git a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift index 331950618..b4fb6a18f 100644 --- a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift @@ -3,9 +3,9 @@ @_exported import ApolloAPI -public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { +public struct NodeFragment: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { public static var fragmentDefinition: StaticString { - #"fragment NodeFragment on Node { __typename id ... on Person { name goodOrBad } ...PlanetInfo }"# + #"fragment NodeFragment on Node { __typename id ... on Person { name goodOrBad homeworld { __typename name } } ...PlanetInfo ...PersonOrPlanetInfo }"# } public let __data: DataDict @@ -15,6 +15,27 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { try value.validate(SwapiSchema.ID.self, for: "id") try value.asPerson?.validate() try value.asPlanet?.validate() + try value.asPersonOrPlanet?.validate() + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "id": try container.decode(SwapiSchema.ID.self, forKey: "id") + ], fulfilledFragments: [ + (try? AsPerson(from: decoder)) != nil ? ObjectIdentifier(AsPerson.self): nil, + (try? AsPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPlanet.self): nil, + (try? AsPersonOrPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPersonOrPlanet.self): nil + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(id, forKey: "id") + try self.asPerson?.encode(to: encoder) + try self.asPlanet?.encode(to: encoder) + try self.asPersonOrPlanet?.encode(to: encoder) } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Interfaces.Node } @@ -23,6 +44,7 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { .field("id", SwapiSchema.ID.self), .inlineFragment(AsPerson.self), .inlineFragment(AsPlanet.self), + .inlineFragment(AsPersonOrPlanet.self), ] } /// The id of the object. @@ -30,18 +52,46 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public var asPerson: AsPerson? { _asInlineFragment() } public var asPlanet: AsPlanet? { _asInlineFragment() } + public var asPersonOrPlanet: AsPersonOrPlanet? { _asInlineFragment() } /// AsPerson /// /// Parent Type: `Person` - public struct AsPerson: SwapiSchema.InlineFragment, Validatable { + public struct AsPerson: SwapiSchema.InlineFragment, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { guard let value else { throw ValidationError.dataIsNil } try value.validate(String?.self, for: "name") try value.validate(GraphQLEnum?.self, for: "goodOrBad") + try value.validate(Homeworld?.self, for: "homeworld") try value.validate(SwapiSchema.ID.self, for: "id") + try value.validate([[[String?]?]?].self, for: "nestedStringArray") + try value.validate([[[NestedPlanetArray?]?]?].self, for: "nestedPlanetArray") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "name": try container.decode(String?.self, forKey: "name"), + "goodOrBad": try container.decode(GraphQLEnum?.self, forKey: "goodOrBad"), + "homeworld": try container.decode(Homeworld?.self, forKey: "homeworld"), + "id": try container.decode(SwapiSchema.ID.self, forKey: "id"), + "nestedStringArray": try container.decode([[[String?]?]?].self, forKey: "nestedStringArray"), + "nestedPlanetArray": try container.decode([[[NestedPlanetArray?]?]?].self, forKey: "nestedPlanetArray") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(name, forKey: "name") + try container.encode(goodOrBad, forKey: "goodOrBad") + try container.encode(homeworld, forKey: "homeworld") + try container.encode(id, forKey: "id") + try container.encode(nestedStringArray, forKey: "nestedStringArray") + try container.encode(nestedPlanetArray, forKey: "nestedPlanetArray") } public typealias RootEntityType = NodeFragment @@ -49,20 +99,74 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { public static var __selections: [ApolloAPI.Selection] { [ .field("name", String?.self), .field("goodOrBad", GraphQLEnum?.self), + .field("homeworld", Homeworld?.self), ] } /// The name of this person. public var name: String? { __data["name"] } /// Whether this is a good person or a bad one public var goodOrBad: GraphQLEnum? { __data["goodOrBad"] } + /// A planet that this person was born on or inhabits. + public var homeworld: Homeworld? { __data["homeworld"] } /// The id of the object. public var id: SwapiSchema.ID { __data["id"] } + public var nestedStringArray: [[[String?]?]?] { __data["nestedStringArray"] } + public var nestedPlanetArray: [[[NestedPlanetArray?]?]?] { __data["nestedPlanetArray"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } + } + + /// AsPerson.Homeworld + /// + /// Parent Type: `Planet` + public struct Homeworld: SwapiSchema.SelectionSet, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "name") + try value.validate([String?]?.self, for: "climates") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "name": try container.decode(String?.self, forKey: "name"), + "climates": try container.decode([String?]?.self, forKey: "climates") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(name, forKey: "name") + try container.encode(climates, forKey: "climates") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("name", String?.self), + ] } + + /// The name of this planet. + public var name: String? { __data["name"] } + /// The climates of this planet. + public var climates: [String?]? { __data["climates"] } + } + + public typealias NestedPlanetArray = PersonOrPlanetInfo.AsPerson.NestedPlanetArray } /// AsPlanet /// /// Parent Type: `Planet` - public struct AsPlanet: SwapiSchema.InlineFragment, Validatable { + public struct AsPlanet: SwapiSchema.InlineFragment, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { @@ -70,6 +174,30 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { try value.validate(SwapiSchema.ID.self, for: "id") try value.validate(String?.self, for: "name") try value.validate(Int?.self, for: "orbitalPeriod") + try value.validate([String?]?.self, for: "climates") + try value.validate(Int?.self, for: "diameter") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "id": try container.decode(SwapiSchema.ID.self, forKey: "id"), + "name": try container.decode(String?.self, forKey: "name"), + "orbitalPeriod": try container.decode(Int?.self, forKey: "orbitalPeriod"), + "climates": try container.decode([String?]?.self, forKey: "climates"), + "diameter": try container.decode(Int?.self, forKey: "diameter") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(id, forKey: "id") + try container.encode(name, forKey: "name") + try container.encode(orbitalPeriod, forKey: "orbitalPeriod") + try container.encode(climates, forKey: "climates") + try container.encode(diameter, forKey: "diameter") } public typealias RootEntityType = NodeFragment @@ -85,12 +213,59 @@ public struct NodeFragment: SwapiSchema.SelectionSet, Fragment { /// The number of standard days it takes for this planet to complete a single orbit /// of its local star. public var orbitalPeriod: Int? { __data["orbitalPeriod"] } + /// The climates of this planet. + public var climates: [String?]? { __data["climates"] } + /// The diameter of this planet in kilometers. + public var diameter: Int? { __data["diameter"] } public struct Fragments: FragmentContainer { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public var planetInfo: PlanetInfo { _toFragment() } + public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } + } + } + + /// AsPersonOrPlanet + /// + /// Parent Type: `PersonOrPlanet` + public struct AsPersonOrPlanet: SwapiSchema.InlineFragment, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(SwapiSchema.ID.self, for: "id") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "id": try container.decode(SwapiSchema.ID.self, forKey: "id") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(id, forKey: "id") + } + + public typealias RootEntityType = NodeFragment + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Unions.PersonOrPlanet } + public static var __selections: [ApolloAPI.Selection] { [ + .fragment(PersonOrPlanetInfo.self), + ] } + + /// The id of the object. + public var id: SwapiSchema.ID { __data["id"] } + + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } } } } diff --git a/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift new file mode 100644 index 000000000..4cabc0d6f --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift @@ -0,0 +1,200 @@ +// @generated +// This file was automatically generated and should not be edited. + +@_exported import ApolloAPI + +public struct PersonOrPlanetInfo: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { + public static var fragmentDefinition: StaticString { + #"fragment PersonOrPlanetInfo on PersonOrPlanet { __typename ... on Person { nestedStringArray nestedPlanetArray { __typename name } homeworld { __typename climates } } ... on Planet { climates diameter } }"# + } + + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.asPerson?.validate() + try value.asPlanet?.validate() + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + ], fulfilledFragments: [ + (try? AsPerson(from: decoder)) != nil ? ObjectIdentifier(AsPerson.self): nil, + (try? AsPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPlanet.self): nil + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try self.asPerson?.encode(to: encoder) + try self.asPlanet?.encode(to: encoder) + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Unions.PersonOrPlanet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .inlineFragment(AsPerson.self), + .inlineFragment(AsPlanet.self), + ] } + + public var asPerson: AsPerson? { _asInlineFragment() } + public var asPlanet: AsPlanet? { _asInlineFragment() } + + /// AsPerson + /// + /// Parent Type: `Person` + public struct AsPerson: SwapiSchema.InlineFragment, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate([[[String?]?]?].self, for: "nestedStringArray") + try value.validate([[[NestedPlanetArray?]?]?].self, for: "nestedPlanetArray") + try value.validate(Homeworld?.self, for: "homeworld") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "nestedStringArray": try container.decode([[[String?]?]?].self, forKey: "nestedStringArray"), + "nestedPlanetArray": try container.decode([[[NestedPlanetArray?]?]?].self, forKey: "nestedPlanetArray"), + "homeworld": try container.decode(Homeworld?.self, forKey: "homeworld") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(nestedStringArray, forKey: "nestedStringArray") + try container.encode(nestedPlanetArray, forKey: "nestedPlanetArray") + try container.encode(homeworld, forKey: "homeworld") + } + + public typealias RootEntityType = PersonOrPlanetInfo + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Person } + public static var __selections: [ApolloAPI.Selection] { [ + .field("nestedStringArray", [[[String?]?]?].self), + .field("nestedPlanetArray", [[[NestedPlanetArray?]?]?].self), + .field("homeworld", Homeworld?.self), + ] } + + public var nestedStringArray: [[[String?]?]?] { __data["nestedStringArray"] } + public var nestedPlanetArray: [[[NestedPlanetArray?]?]?] { __data["nestedPlanetArray"] } + /// A planet that this person was born on or inhabits. + public var homeworld: Homeworld? { __data["homeworld"] } + + /// AsPerson.NestedPlanetArray + /// + /// Parent Type: `Planet` + public struct NestedPlanetArray: SwapiSchema.SelectionSet, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate(String?.self, for: "name") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "name": try container.decode(String?.self, forKey: "name") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(name, forKey: "name") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("name", String?.self), + ] } + + /// The name of this planet. + public var name: String? { __data["name"] } + } + + /// AsPerson.Homeworld + /// + /// Parent Type: `Planet` + public struct Homeworld: SwapiSchema.SelectionSet, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate([String?]?.self, for: "climates") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "climates": try container.decode([String?]?.self, forKey: "climates") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(climates, forKey: "climates") + } + + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("climates", [String?]?.self), + ] } + + /// The climates of this planet. + public var climates: [String?]? { __data["climates"] } + } + } + + /// AsPlanet + /// + /// Parent Type: `Planet` + public struct AsPlanet: SwapiSchema.InlineFragment, Validatable, Codable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + public static func validate(value: Self?) throws { + guard let value else { throw ValidationError.dataIsNil } + try value.validate([String?]?.self, for: "climates") + try value.validate(Int?.self, for: "diameter") + } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "climates": try container.decode([String?]?.self, forKey: "climates"), + "diameter": try container.decode(Int?.self, forKey: "diameter") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(climates, forKey: "climates") + try container.encode(diameter, forKey: "diameter") + } + + public typealias RootEntityType = PersonOrPlanetInfo + public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } + public static var __selections: [ApolloAPI.Selection] { [ + .field("climates", [String?]?.self), + .field("diameter", Int?.self), + ] } + + /// The climates of this planet. + public var climates: [String?]? { __data["climates"] } + /// The diameter of this planet in kilometers. + public var diameter: Int? { __data["diameter"] } + } +} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift index e20f37163..1d316b932 100644 --- a/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift @@ -3,7 +3,7 @@ @_exported import ApolloAPI -public struct PlanetInfo: SwapiSchema.SelectionSet, Fragment { +public struct PlanetInfo: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { public static var fragmentDefinition: StaticString { #"fragment PlanetInfo on Planet { __typename name orbitalPeriod }"# } @@ -15,6 +15,22 @@ public struct PlanetInfo: SwapiSchema.SelectionSet, Fragment { try value.validate(String?.self, for: "name") try value.validate(Int?.self, for: "orbitalPeriod") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "name": try container.decode(String?.self, forKey: "name"), + "orbitalPeriod": try container.decode(Int?.self, forKey: "orbitalPeriod") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(name, forKey: "name") + try container.encode(orbitalPeriod, forKey: "orbitalPeriod") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } public static var __selections: [ApolloAPI.Selection] { [ diff --git a/test-codegen/SwapiSchema/Sources/JSON.swift b/test-codegen/SwapiSchema/Sources/JSON.swift new file mode 100644 index 000000000..0446e69dc --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/JSON.swift @@ -0,0 +1,396 @@ +// +// JSON.swift +// SwapiSchema +// +// Created by Guigui on 8/12/25. + +import Foundation + +// MARK: - JSON + +public enum JSON: Codable, Equatable, Sendable { + case object(_ value: [String: JSON.Value]) + case array(_ value: [JSON.Value]) + + // MARK: - JSONValue + + public enum Value: Codable, Equatable, Sendable { + case string(_ value: String) + case object(_ value: [String: JSON.Value]) + case array(_ value: [JSON.Value]) + case bool(_ value: Bool) + case number(_ value: Double) + case null + } +} + +// MARK: ExpressibleByDictionaryLiteral + +extension JSON: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, JSON.Value)...) { + self.init(dictionaryLiteral: elements) + } + + public init(dictionaryLiteral elements: [(String, JSON.Value)]) { + var object = [String: JSON.Value]() + + for element in elements { + object[element.0] = element.1 + } + + self = .object(object) + } + + public init(_ dictionary: [String: some JSONValueConvertible]) { + self.init(dictionaryLiteral: dictionary.map { k, v in (k, v.asJSONValue) }) + } +} + +// MARK: ExpressibleByArrayLiteral + +extension JSON: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JSON.Value...) { + var array = [JSON.Value]() + + for element in elements { + array.append(element) + } + + self = .array(array) + } +} + +// MARK: - JSON.Value + ExpressibleByNilLiteral + +extension JSON.Value: ExpressibleByNilLiteral { + public init(nilLiteral _: ()) { + self = .null + } +} + +// MARK: - JSON.Value + ExpressibleByDictionaryLiteral + +extension JSON.Value: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, JSON.Value)...) { + var object = [String: JSON.Value]() + + for element in elements { + object[element.0] = element.1 + } + + self = .object(object) + } +} + +// MARK: - JSON.Value + ExpressibleByStringLiteral + +extension JSON.Value: ExpressibleByStringLiteral { + public init(stringLiteral: String) { + self = .string(stringLiteral) + } +} + +// MARK: - JSON.Value + ExpressibleByIntegerLiteral + +extension JSON.Value: ExpressibleByIntegerLiteral { + public init(integerLiteral value: IntegerLiteralType) { + self = .number(Double(value)) + } +} + +// MARK: - JSON.Value + ExpressibleByFloatLiteral + +extension JSON.Value: ExpressibleByFloatLiteral { + public init(floatLiteral value: FloatLiteralType) { + self = .number(value) + } +} + +// MARK: - JSON.Value + ExpressibleByArrayLiteral + +extension JSON.Value: ExpressibleByArrayLiteral { + public init(arrayLiteral elements: JSON.Value...) { + var array = [JSON.Value]() + + for element in elements { + array.append(element) + } + + self = .array(array) + } +} + +// MARK: - JSON.Value + ExpressibleByBooleanLiteral + +extension JSON.Value: ExpressibleByBooleanLiteral { + public init(booleanLiteral value: BooleanLiteralType) { + self = .bool(value) + } +} + +extension JSON { + + public init(from decoder: Decoder) throws { + do { + // Object + let container = try decoder.container(keyedBy: String.self) + let keys = container.allKeys + let object = keys.reduce(into: [String: JSON.Value]()) { result, key in + if let value = try? container.decode(JSON.Value.self, forKey: key) { + result[key] = value + } + } + self = .object(object) + } catch { + // Array + var container = try decoder.unkeyedContainer() + var array: [JSON.Value] = [] + while !container.isAtEnd { + if let value = try? container.decode(JSON.Value.self) { + array.append(value) + } + } + self = .array(array) + } + } + + public static func ==(lhs: JSON, rhs: JSON) -> Bool { + lhs.asValue == rhs.asValue + } + + public func encode(to encoder: any Encoder) throws { + try asValue.encode(to: encoder) + } + + var asValue: JSON.Value { + switch self { + case .object(let value): + .object(value) + case .array(let value): + .array(value) + } + } +} + +extension JSON.Value { + + public init(from decoder: Decoder) throws { + do { + // Single values + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let value = try? container.decode(String.self) { + self = .string(value) + } else if let value = try? container.decode(Bool.self) { + self = .bool(value) + } else if let value = try? container.decode(Double.self) { + self = .number(value) + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid JSON") + } + } catch { + // Object + do { + let container = try decoder.container(keyedBy: String.self) + let keys = container.allKeys + let object = keys.reduce(into: [String: JSON.Value]()) { result, key in + if let value = try? container.decode(JSON.Value.self, forKey: key) { + result[key] = value + } + } + self = .object(object) + } catch { + // Array + var container = try decoder.unkeyedContainer() + var array: [JSON.Value] = [] + while !container.isAtEnd { + if let value = try? container.decode(JSON.Value.self) { + array.append(value) + } + } + self = .array(array) + } + } + } + + public static func ==(lhs: JSON.Value, rhs: JSON.Value) -> Bool { + switch (lhs, rhs) { + case (.null, .null): + true + case (.string(let lhs), .string(let rhs)): + lhs == rhs + case (.bool(let lhs), .bool(let rhs)): + lhs == rhs + case (.number(let lhs), .number(let rhs)): + lhs == rhs + case (.object(let lhs), .object(let rhs)): + lhs == rhs + case (.array(let lhs), .array(let rhs)): + lhs == rhs + default: + false + } + } + + public func encode(to encoder: any Encoder) throws { + switch self { + case .null: + var container = encoder.singleValueContainer() + try container.encodeNil() + + case .string(let value): + var container = encoder.singleValueContainer() + try container.encode(value) + + case .bool(let value): + var container = encoder.singleValueContainer() + try container.encode(value) + + case .number(let value): + var container = encoder.singleValueContainer() + try container.encode(value) + + case .object(let value): + var container = encoder.container(keyedBy: String.self) + for (key, value) in value { + try container.encode(value, forKey: key) + } + + case .array(let value): + var container = encoder.unkeyedContainer() + for value in value { + try container.encode(value) + } + } + } +} + +extension JSON { + public var asAny: Any { + switch self { + case .object(let value): + value.keys.reduce(into: [String: Any]()) { result, key in + result[key] = value[key]?.asAny + } + + case .array(let value): + value.map(\.asAny) + } + } + + public var asObject: [String: JSON.Value]? { + guard case .object(let value) = self else { return nil } + return value + } + + public var asArray: [JSON.Value]? { + guard case .array(let value) = self else { return nil } + return value + } + + public func asJSONData(options: JSONSerialization.WritingOptions = []) throws -> Data { + try JSONSerialization.data(withJSONObject: asAny, options: options) + } + + public func asJSONString(options: JSONSerialization.WritingOptions = []) throws -> String { + guard let string = try String(data: asJSONData(options: options), encoding: .utf8) else { + throw NSError(domain: "JSON", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to convert JSON to string"]) + } + return string + } +} + +extension JSON.Value { + public var asAny: Any { + switch self { + case .string(let value): + value + + case .object(let value): + value.keys.reduce(into: [String: Any]()) { result, key in + result[key] = value[key]?.asAny + } + + case .array(let value): + value.map(\.asAny) + + case .bool(let value): + value + + case .number(let value): + value + + case .null: + NSNull() + } + } + + public var asString: String? { + guard case .string(let value) = self else { return nil } + return value + } + + public var asObject: [String: JSON.Value]? { + guard case .object(let value) = self else { return nil } + return value + } + + public var asArray: [JSON.Value]? { + guard case .array(let value) = self else { return nil } + return value + } + + public var asBool: Bool? { + guard case .bool(let value) = self else { return nil } + return value + } + + public var asNumber: Double? { + guard case .number(let value) = self else { return nil } + return value + } +} + +// MARK: - String + CodingKey + +extension String: @retroactive CodingKey { + + public init?(stringValue: String) { + self = stringValue + } + + public init?(intValue: Int) { + self = "\(intValue)" + } + + public var stringValue: String { self } + public var intValue: Int? { Int(self) } +} + +public protocol JSONValueConvertible { + var asJSONValue: JSON.Value { get } +} + +extension String: JSONValueConvertible { + public var asJSONValue: JSON.Value { .string(self) } +} + +extension Int: JSONValueConvertible { + public var asJSONValue: JSON.Value { .number(Double(self)) } +} + +extension Bool: JSONValueConvertible { + public var asJSONValue: JSON.Value { .bool(self) } +} + +extension Optional: JSONValueConvertible where Wrapped: JSONValueConvertible { + public var asJSONValue: JSON.Value { + switch self { + case .none: + .null + case .some(let value): + value.asJSONValue + } + } +} diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift index 51189a0ba..e8df0733a 100644 --- a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift @@ -42,6 +42,20 @@ public class TestComplexQuery: GraphQLQuery { guard let value else { throw ValidationError.dataIsNil } try value.validate(AllFilms?.self, for: "allFilms") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "allFilms": try container.decode(AllFilms?.self, forKey: "allFilms") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(allFilms, forKey: "allFilms") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } public static var __selections: [ApolloAPI.Selection] { [ @@ -58,13 +72,27 @@ public class TestComplexQuery: GraphQLQuery { /// AllFilms /// /// Parent Type: `FilmsConnection` - public struct AllFilms: SwapiSchema.SelectionSet, Validatable { + public struct AllFilms: SwapiSchema.SelectionSet, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { guard let value else { throw ValidationError.dataIsNil } try value.validate([Film?]?.self, for: "films") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "films": try container.decode([Film?]?.self, forKey: "films") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(films, forKey: "films") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } public static var __selections: [ApolloAPI.Selection] { [ @@ -83,7 +111,7 @@ public class TestComplexQuery: GraphQLQuery { /// AllFilms.Film /// /// Parent Type: `Film` - public struct Film: SwapiSchema.SelectionSet, Validatable { + public struct Film: SwapiSchema.SelectionSet, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { @@ -91,6 +119,22 @@ public class TestComplexQuery: GraphQLQuery { try value.validate(String?.self, for: "director") try value.validate(Int?.self, for: "episodeID") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "director": try container.decode(String?.self, forKey: "director"), + "episodeID": try container.decode(Int?.self, forKey: "episodeID") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(director, forKey: "director") + try container.encode(episodeID, forKey: "episodeID") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } public static var __selections: [ApolloAPI.Selection] { [ diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift index 879c8053c..d9a5c77c1 100644 --- a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift +++ b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift @@ -42,6 +42,20 @@ public class TestQuery: GraphQLQuery { guard let value else { throw ValidationError.dataIsNil } try value.validate(AllFilms?.self, for: "allFilms") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "allFilms": try container.decode(AllFilms?.self, forKey: "allFilms") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(allFilms, forKey: "allFilms") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } public static var __selections: [ApolloAPI.Selection] { [ @@ -58,13 +72,27 @@ public class TestQuery: GraphQLQuery { /// AllFilms /// /// Parent Type: `FilmsConnection` - public struct AllFilms: SwapiSchema.SelectionSet, Validatable { + public struct AllFilms: SwapiSchema.SelectionSet, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { guard let value else { throw ValidationError.dataIsNil } try value.validate([Film?]?.self, for: "films") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "films": try container.decode([Film?]?.self, forKey: "films") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(films, forKey: "films") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } public static var __selections: [ApolloAPI.Selection] { [ @@ -83,7 +111,7 @@ public class TestQuery: GraphQLQuery { /// AllFilms.Film /// /// Parent Type: `Film` - public struct Film: SwapiSchema.SelectionSet, Validatable { + public struct Film: SwapiSchema.SelectionSet, Validatable, Codable { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } public static func validate(value: Self?) throws { @@ -91,6 +119,22 @@ public class TestQuery: GraphQLQuery { try value.validate(String?.self, for: "director") try value.validate(Int?.self, for: "episodeID") } + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: String.self) + __data = DataDict(data: [ + "__typename": try container.decode(String.self, forKey: "__typename"), + "director": try container.decode(String?.self, forKey: "director"), + "episodeID": try container.decode(Int?.self, forKey: "episodeID") + ], fulfilledFragments: [ + ]) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: String.self) + try container.encode(__typename, forKey: "__typename") + try container.encode(director, forKey: "director") + try container.encode(episodeID, forKey: "episodeID") + } public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } public static var __selections: [ApolloAPI.Selection] { [ diff --git a/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift new file mode 100644 index 000000000..7e7d1e9f1 --- /dev/null +++ b/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift @@ -0,0 +1,14 @@ +// @generated +// This file was automatically generated and should not be edited. + +import ApolloAPI + +public extension Unions { + static let PersonOrPlanet = Union( + name: "PersonOrPlanet", + possibleTypes: [ + Objects.Person.self, + Objects.Planet.self + ] + ) +} \ No newline at end of file diff --git a/test-codegen/TestPackage/Package.resolved b/test-codegen/TestPackage/Package.resolved deleted file mode 100644 index 48e3df3ed..000000000 --- a/test-codegen/TestPackage/Package.resolved +++ /dev/null @@ -1,14 +0,0 @@ -{ - "pins" : [ - { - "identity" : "apollo-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apollographql/apollo-ios", - "state" : { - "revision" : "d591c1dd55824867877cff4f6551fe32983d7f51", - "version" : "1.23.0" - } - } - ], - "version" : 2 -} diff --git a/test-codegen/TestPackage/Package.swift b/test-codegen/TestPackage/Package.swift index 8c88fa88e..31160d594 100644 --- a/test-codegen/TestPackage/Package.swift +++ b/test-codegen/TestPackage/Package.swift @@ -14,7 +14,8 @@ let package = Package( .library(name: "TestPackage", targets: ["TestPackage"]), ], dependencies: [ - .package(path: "../SwapiSchema") + .package(path: "../SwapiSchema"), + .package(path: "../../apollo-ios"), ], targets: [ .target( @@ -24,5 +25,13 @@ let package = Package( ], path: "./Sources" ), + .testTarget(name: "TestPackageTests", + dependencies: [ + "TestPackage", + .product(name: "SwapiSchema", package: "SwapiSchema"), + .product(name: "ApolloAPI", package: "apollo-ios"), + .product(name: "Apollo", package: "apollo-ios"), + ], + path: "./Tests") ] ) diff --git a/test-codegen/TestPackage/Tests/CodableTests.swift b/test-codegen/TestPackage/Tests/CodableTests.swift new file mode 100644 index 000000000..98779dbfc --- /dev/null +++ b/test-codegen/TestPackage/Tests/CodableTests.swift @@ -0,0 +1,152 @@ + +import Testing +import Foundation +import SwapiSchema +@_spi(Execution) import Apollo + +struct CodableTests { + @Test + func testSimpleFragment() throws { + let json = """ + { + "__typename": "Film", + "director": "George Lucas", + "episodeID": 1 + } + """.utf8Data + + let value = try JSONDecoder().decode(FilmFragment.self, from: json) + let encoded = try JSONEncoder().encode(value) + + #expect(encoded.jsonString() == json.jsonString()) + } + + @Test + func testSimpleFragment_failsWithBadData() throws { + let json = """ + { + "__typename": "Film", + "director": "George Lucas" + } + """.utf8Data + + try expect({ + try JSONDecoder().decode(FilmFragment.self, from: json) + }, toThrow: { error in + guard let decodingError = error as? DecodingError else { + throw error + } + switch decodingError { + case .keyNotFound(let key, _): + #expect(key.stringValue == "episodeID") + default: + throw decodingError + } + }) + } + + @Test + func testComplexFragment_withSpecies() throws { + let json = """ + { + "__typename": "Species", + "id": "123", + } + """.utf8Data + + let value = try JSONDecoder().decode(NodeFragment.self, from: json) + let encoded = try JSONEncoder().encode(value) + + #expect(encoded.jsonString() == json.jsonString()) + #expect(value.id == "123") + #expect(value.__typename == "Species") + #expect(value.asPerson == nil) + #expect(value.asPlanet == nil) + } + + @Test + func testComplexFragment_withPerson() throws { +// let apolloClient = ApolloClient(url: URL(string: "http://localhost:4000/graphql")!) +// apolloClient.fetch(query: TestQuery(after: .none, before: .none, first: .none, last: .none)) { result in +// guard let data = try? result.get().data else { return } +// } + + + + + let json = """ + { + "__typename": "Person", + "id": "123", + "name": "Luke", + "goodOrBad": "GOOD", + "homeworld": { + "__typename": "Planet", + "name": "Tatooine", + "climates": [], + }, + "nestedStringArray": [[["one", "two"], ["three", "four"]]], + "nestedPlanetArray": [[[{ "__typename": "Planet", "name": "Tatouine" }], [{ "__typename": "Planet", "name": "Naboo" }]]], + } + """.utf8Data + + let dataEntry = try JSONSerialization.jsonObject(with: json, options: []) as! JSONObject + + let res = try GraphQLExecutor(executionSource: NetworkResponseExecutionSource()).execute( + selectionSet: NodeFragment.self, + on: dataEntry, // JSONObject + withRootCacheReference: nil, + variables: nil, + accumulator: GraphQLSelectionSetMapper() + ) + + let homeworld: NodeFragment.AsPerson.Homeworld? = res.__data["homeworld"] + let homeworld2: PersonOrPlanetInfo.AsPerson.Homeworld? = res.__data["homeworld"] + + let encodedData = try JSONEncoder().encode(res.__data) + let encodedStr = encodedData.jsonString() + + let value = try JSONDecoder().decode(NodeFragment.self, from: json) + let encoded = try JSONEncoder().encode(value) + + #expect(encoded.jsonString() == json.jsonString()) + #expect(value.id == "123") + #expect(value.__typename == "Person") + #expect(value.asPerson?.goodOrBad == .case(.good)) + + } + + @Test + func testComplexFragment_failsWithBadData() throws { + let json = """ + { + "__typename": "Film", + "director": "George Lucas" + } + """.utf8Data + + try expect({ + try JSONDecoder().decode(NodeFragment.self, from: json) + }, toThrow: { error in + guard let decodingError = error as? DecodingError else { + throw error + } + switch decodingError { + case .keyNotFound(let key, _): + #expect(key.stringValue == "episodeID") + default: + throw decodingError + } + }) + } + + private func expect(_ operation: () throws -> T, toThrow: (Error) throws -> Void) throws { + do { + _ = try operation() + Issue.record("Expected operation to throw an error, but it did not.") + } catch { + try toThrow(error) + } + } + +} diff --git a/test-codegen/TestPackage/Tests/JSON+helpers.swift b/test-codegen/TestPackage/Tests/JSON+helpers.swift new file mode 100644 index 000000000..0f554b5f0 --- /dev/null +++ b/test-codegen/TestPackage/Tests/JSON+helpers.swift @@ -0,0 +1,106 @@ +import Foundation +import Testing + +extension Data { + public func jsonString(ignoring ignoredKeys: [String] = []) -> String { + do { + var object = try JSONSerialization.jsonObject(with: self, options: []) + if !ignoredKeys.isEmpty { + object = (object as? [String: Any?])? + .filter { !ignoredKeys.contains($0.key) } as Any + } + let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) + guard let string = String(data: data, encoding: .utf8) else { + throw TestError("Invalid JSON data") + } + return string + } catch { + Issue.record(error) + return "" + } + } + + public func expectToMatch(_ expected: String, ignoring ignoredKeys: [String] = []) { + let received = jsonString(ignoring: ignoredKeys) + let expected = expected.utf8Data.jsonString(ignoring: ignoredKeys) + #expect(expected == received) + } + + public func expectToMatch(_ expected: String, ignoring ignoredKey: String) { + expectToMatch(expected, ignoring: [ignoredKey]) + } +} + +/// Test decoding the Json data to the given type, encoding it back to Json, and comparing the results. +public func testDecodingEncodingOf(_ json: String, with _: T.Type) throws { + let jsonData = json.utf8Data + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(T.self, from: jsonData) + + let encoder = JSONEncoder() + let encoded = try encoder.encode(decoded) + + let value = encoded.jsonString() + let expected = jsonData.jsonString() + + #expect(expected == value) +} + +/// Test that encoding the value gives the expected json, and that decoding the json and re-encoding it doesn't change the value. +public func testDecodingEncoding( + of value: T, + _ json: String, + decoder: JSONDecoder = JSONDecoder(), + encoder: JSONEncoder = JSONEncoder()) + throws +{ + // Validate that encoding the value gives the expected json + try testEncoding(value, json, encoder: encoder) + + // Validate that decoding the json and re-encoding it gives the same json + let decoded = try decoder.decode(T.self, from: json.utf8Data) + let encoded = try encoder.encode(decoded) + + let value = encoded.jsonString() + let expected = json.utf8Data.jsonString() + + #expect(expected == value) +} + +/// Test that encoding the value gives the expected json. +public func testEncoding(_ value: some Encodable, _ json: String, encoder: JSONEncoder = JSONEncoder()) throws { + let encoded = try encoder.encode(value) + let encodedString = encoded.jsonString() + + // Reformat the json expectation (pretty print, sort keys) + let jsonData = json.utf8Data + let expected = jsonData.jsonString() + + #expect(expected == encodedString) +} + +/// Test that decoding the json gives the expected value. +public func testDecoding(_ value: T, _ json: String) throws { + let decoded = try JSONDecoder().decode(T.self, from: json.utf8Data) + #expect(decoded == value) +} + +/// Test that encoding the value gives the expected json, and that decoding the json gives the expected value. +public func testEncodingDecoding(_ value: some Codable & Equatable, _ json: String) throws { + try testEncoding(value, json) + try testDecoding(value, json) +} + +public struct TestError: Error { + public init(_ message: String) { + self.message = message + } + + public let message: String +} + +extension String { + public var utf8Data: Data { + Data(utf8) + } +} diff --git a/test-codegen/apollo-codegen-config.json b/test-codegen/apollo-codegen-config.json index ac4a60018..93b648608 100644 --- a/test-codegen/apollo-codegen-config.json +++ b/test-codegen/apollo-codegen-config.json @@ -16,7 +16,7 @@ "schemaTypes" : { "path" : "./SwapiSchema", "moduleType" : { - "swiftPackageManager" : { + "swiftPackage" : { } } }, diff --git a/test-codegen/schema.graphqls b/test-codegen/schema.graphqls index 97c4cb547..856891962 100644 --- a/test-codegen/schema.graphqls +++ b/test-codegen/schema.graphqls @@ -369,6 +369,9 @@ type Person implements Node { vehicleConnection(after: String, before: String, first: Int, last: Int): PersonVehiclesConnection """Whether this is a good person or a bad one""" goodOrBad: GoodOrBad + + nestedStringArray: [[[String]]]! + nestedPlanetArray: [[[Planet]]]! } """A connection to a list of items.""" @@ -1171,4 +1174,6 @@ type VehiclesEdge { enum GoodOrBad { GOOD BAD -} \ No newline at end of file +} + +union PersonOrPlanet = Person | Planet From f9fbc47f9e99ce7d4f4bd4ca1728b8a7b1fa26f1 Mon Sep 17 00:00:00 2001 From: Guillaume Sabran Date: Tue, 12 Aug 2025 19:59:56 -0700 Subject: [PATCH 4/4] clean up --- .../SelectionSetTemplateTests.swift | 26 +- ...ctionSetTemplate_ErrorHandling_Tests.swift | 4 +- ...ectionSetTemplate_FieldMerging_Tests.swift | 2 +- ...ectionSetTemplate_Initializers_Tests.swift | 4 +- ...SetTemplate_LocalCacheMutation_Tests.swift | 2 +- ...plateString_DeprecationMessage_Tests.swift | 4 +- Tests/ApolloTests/DataDictTests.swift | 161 +++ .../ApolloCodegenConfiguration.swift | 26 +- .../Templates/FragmentTemplate.swift | 4 +- ...LocalCacheMutationDefinitionTemplate.swift | 2 +- .../OperationDefinitionTemplate.swift | 2 +- .../Templates/SelectionSetTemplate.swift | 101 +- apollo-ios/Sources/ApolloAPI/DataDict.swift | 20 +- test-codegen/Sources/testComplexQuery.graphql | 41 - test-codegen/Sources/testQuery.graphql | 12 - test-codegen/SwapiSchema/Package.swift | 28 - .../Sources/ApolloAPIExtensions.swift | 102 -- .../Fragments/FilmFragment.graphql.swift | 46 - .../Fragments/NodeFragment.graphql.swift | 271 ---- .../PersonOrPlanetInfo.graphql.swift | 200 --- .../Fragments/PlanetInfo.graphql.swift | 47 - test-codegen/SwapiSchema/Sources/JSON.swift | 396 ------ .../Queries/TestComplexQuery.graphql.swift | 159 --- .../Queries/TestQuery.graphql.swift | 159 --- .../Sources/Schema/CustomScalars/ID.swift | 11 - .../Schema/Enums/GoodOrBad.graphql.swift | 9 - .../Schema/Interfaces/Node.graphql.swift | 20 - .../Sources/Schema/Objects/Film.graphql.swift | 13 - .../Objects/FilmsConnection.graphql.swift | 13 - .../Schema/Objects/Person.graphql.swift | 13 - .../Schema/Objects/Planet.graphql.swift | 14 - .../Sources/Schema/Objects/Root.graphql.swift | 12 - .../Schema/Objects/Species.graphql.swift | 13 - .../Schema/Objects/Starship.graphql.swift | 13 - .../Schema/Objects/Vehicle.graphql.swift | 13 - .../Sources/Schema/SchemaConfiguration.swift | 15 - .../Schema/SchemaMetadata.graphql.swift | 38 - .../Unions/PersonOrPlanet.graphql.swift | 14 - test-codegen/TestPackage/Package.swift | 37 - .../TestPackage/Sources/TestPackage.swift | 6 - .../TestPackage/Tests/CodableTests.swift | 152 --- .../TestPackage/Tests/JSON+helpers.swift | 106 -- test-codegen/apollo-codegen-config.json | 29 - test-codegen/schema.graphqls | 1179 ----------------- 44 files changed, 221 insertions(+), 3318 deletions(-) create mode 100644 Tests/ApolloTests/DataDictTests.swift delete mode 100644 test-codegen/Sources/testComplexQuery.graphql delete mode 100644 test-codegen/Sources/testQuery.graphql delete mode 100644 test-codegen/SwapiSchema/Package.swift delete mode 100644 test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/JSON.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift delete mode 100644 test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift delete mode 100644 test-codegen/TestPackage/Package.swift delete mode 100644 test-codegen/TestPackage/Sources/TestPackage.swift delete mode 100644 test-codegen/TestPackage/Tests/CodableTests.swift delete mode 100644 test-codegen/TestPackage/Tests/JSON+helpers.swift delete mode 100644 test-codegen/apollo-codegen-config.json delete mode 100644 test-codegen/schema.graphqls diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 4cedfc3da..9faa88c9b 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -58,7 +58,7 @@ class SelectionSetTemplateTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -3334,7 +3334,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3413,7 +3413,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3502,7 +3502,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3591,7 +3591,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3680,7 +3680,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3774,7 +3774,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3874,7 +3874,7 @@ class SelectionSetTemplateTests: XCTestCase { let basicFragmentSubject = SelectionSetTemplate( definition: allAnimals_basicFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -3967,7 +3967,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -4060,7 +4060,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9694,7 +9694,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_asDog_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -9780,7 +9780,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentSubject = SelectionSetTemplate( definition: allAnimals_animalFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() @@ -11952,7 +11952,7 @@ class SelectionSetTemplateTests: XCTestCase { let fragmentTemplate = SelectionSetTemplate( definition: detailsFragment.fragment, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: self.subject.config, nonFatalErrorRecorder: .init(), renderAccessControl: self.subject.renderAccessControl() diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift index 7fc9b1986..7c88c5878 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_ErrorHandling_Tests.swift @@ -56,7 +56,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -80,7 +80,7 @@ class SelectionSetTemplate_ErrorHandling_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: errorRecorder, renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift index c242e2464..d6502c933 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_FieldMerging_Tests.swift @@ -67,7 +67,7 @@ class SelectionSetTemplate_FieldMerging_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: selectionSetInitializers, - generateTypeValidation: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift index d7edd442e..1ce9d35e7 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_Initializers_Tests.swift @@ -49,7 +49,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) @@ -78,7 +78,7 @@ class SelectionSetTemplate_Initializers_Tests: XCTestCase { subject = SelectionSetTemplate( definition: fragment.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: ApolloCodegen.ConfigurationContext(config: config), nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift index c3b6b4b86..bf64df38e 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplate_LocalCacheMutation_Tests.swift @@ -50,7 +50,7 @@ class SelectionSetTemplate_LocalCacheMutationTests: XCTestCase { subject = SelectionSetTemplate( definition: self.operation.irObject, generateInitializers: false, - generateTypeValidation: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: mockTemplateRenderer.accessControlModifier(for: .member) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift index d0efbdbea..80c4d27c0 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/TemplateString_DeprecationMessage_Tests.swift @@ -273,7 +273,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() @@ -404,7 +404,7 @@ final class TemplateString_DeprecationMessage_Tests: XCTestCase { let subject = SelectionSetTemplate( definition: operation.irObject, generateInitializers: true, - generateTypeValidation: false, + generateDecodableTypes: false, config: config, nonFatalErrorRecorder: .init(), renderAccessControl: { "does not matter" }() diff --git a/Tests/ApolloTests/DataDictTests.swift b/Tests/ApolloTests/DataDictTests.swift new file mode 100644 index 000000000..854c1b21a --- /dev/null +++ b/Tests/ApolloTests/DataDictTests.swift @@ -0,0 +1,161 @@ +import XCTest +@testable import Apollo +import ApolloAPI +import ApolloInternalTestHelpers +import Nimble + +class DataDictTests: XCTestCase { + + class Data: MockSelectionSet { + class Animal: MockSelectionSet { + class Predator: MockSelectionSet { } + } + } + + func test__encoding_simpleDataStructure_works() throws { + // given + let subject = DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [], + deferredFragments: [] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "__typename": "Animal", + "name": "Dog" + } + """)) + } + + func test__encoding_nestedDataStructure_works() throws { + // given + let subject = DataDict( + data: [ + "animals": [ + DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [ + ObjectIdentifier(Data.Animal.self), + ], + deferredFragments: [ + ObjectIdentifier(Data.Animal.Predator.self) + ] + ), + DataDict( + data: [ + "__typename": "Animal", + "name": "Cat" + ], + fulfilledFragments: [ + ObjectIdentifier(Data.Animal.self), + ], + deferredFragments: [ + ObjectIdentifier(Data.Animal.Predator.self) + ] + ) + ] + ], + fulfilledFragments: [ + ObjectIdentifier(Data.self), + ] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "animals": [ + { + "__typename": "Animal", + "name": "Dog" + }, { + "__typename": "Animal", + "name": "Cat" + } + ] + } + """)) + } + + func test__encoding_dataStructureWithArrayProperties_works() throws { + // givens + let subject = DataDict( + data: [ + "animals": [ + DataDict( + data: [ + "__typename": "Animal", + "name": "Dog" + ], + fulfilledFragments: [], + deferredFragments: [] + ), + DataDict( + data: [ + "__typename": "Animal", + "name": "Cat" + ], + fulfilledFragments: [], + deferredFragments: [] + ) + ], + "coordinates": [[1.0, 2.0], [3.0, 4.0], DataDict._NullValue], + ], + fulfilledFragments: [] + ) + + // then + expect( + try JSONEncoder().encode(subject).jsonString() + ).to(match(""" + { + "animals": [ + { + "__typename": "Animal", + "name": "Dog" + }, { + "__typename": "Animal", + "name": "Cat" + } + ], + "coordinates": [ + [1.0, 2.0], + [3.0, 4.0], + null + ] + } + """)) + } + + +} + +extension Data { + public func jsonString(ignoring ignoredKeys: [String] = []) throws -> String { + var object = try JSONSerialization.jsonObject(with: self, options: []) + if !ignoredKeys.isEmpty { + object = (object as? [String: Any?])? + .filter { !ignoredKeys.contains($0.key) } as Any + } + let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) + return String(data: data, encoding: .utf8)! + } +} + +func match(_ expectedValue: String) -> Matcher { + let expectedData = Data(expectedValue.utf8) + + let expectedString = try! expectedData.jsonString() + return equal(expectedString) +} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift index 0772c9444..984ac35c1 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift @@ -166,7 +166,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// The local path structure for the test mock operation object files. public let testMocks: TestMockFileOutput /// Whether to generate type validation for the generated code. - public let generateTypeValidation: Bool + public let generateDecodableTypes: Bool /// This var helps maintain backwards compatibility with legacy operation manifest generation /// with the new `OperationManifestConfiguration` and will be fully removed in v2.0 @@ -176,7 +176,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { public struct Default { public static let operations: OperationsFileOutput = .inSchemaModule public static let testMocks: TestMockFileOutput = .none - public static let generateTypeValidation: Bool = false + public static let generateDecodableTypes: Bool = false } /// Designated initializer. @@ -191,18 +191,18 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// with persisted queries or /// [Automatic Persisted Queries (APQs)](https://www.apollographql.com/docs/apollo-server/performance/apq). /// Defaults to `nil`. - /// - generateTypeValidation: Whether to generate type validation for the generated code. + /// - generateDecodableTypes: Whether to generate type validation for the generated code. public init( schemaTypes: SchemaTypesFileOutput, operations: OperationsFileOutput = Default.operations, testMocks: TestMockFileOutput = Default.testMocks, - generateTypeValidation: Bool = Default.generateTypeValidation + generateDecodableTypes: Bool = Default.generateDecodableTypes ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = nil - self.generateTypeValidation = generateTypeValidation + self.generateDecodableTypes = generateDecodableTypes } // MARK: Codable @@ -212,7 +212,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { case operations case testMocks case operationIdentifiersPath - case generateTypeValidation + case generateDecodableTypes } /// `Decodable` implementation to allow for properties to be optional in the encoded JSON with @@ -237,10 +237,10 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { String.self, forKey: .operationIdentifiersPath ) - generateTypeValidation = try values.decodeIfPresent( + generateDecodableTypes = try values.decodeIfPresent( Bool.self, - forKey: .generateTypeValidation - ) ?? Default.generateTypeValidation + forKey: .generateDecodableTypes + ) ?? Default.generateDecodableTypes } public func encode(to encoder: any Encoder) throws { @@ -249,7 +249,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { try container.encode(self.schemaTypes, forKey: .schemaTypes) try container.encode(self.operations, forKey: .operations) try container.encode(self.testMocks, forKey: .testMocks) - try container.encode(self.generateTypeValidation, forKey: .generateTypeValidation) + try container.encode(self.generateDecodableTypes, forKey: .generateDecodableTypes) } } @@ -1710,21 +1710,21 @@ extension ApolloCodegenConfiguration.FileOutput { /// If `.none`, test mocks will not be generated. Defaults to `.none`. /// - operationIdentifiersPath: An absolute location to an operation id JSON map file /// for use with APQ registration. Defaults to `nil`. - /// - generateTypeValidation: Whether to generate type validation for the generated code. + /// - generateDecodableTypes: Whether to generate type validation for the generated code. @available(*, deprecated, renamed: "init(schemaTypes:operations:testMocks:)") @_disfavoredOverload public init( schemaTypes: ApolloCodegenConfiguration.SchemaTypesFileOutput, operations: ApolloCodegenConfiguration.OperationsFileOutput = Default.operations, testMocks: ApolloCodegenConfiguration.TestMockFileOutput = Default.testMocks, - generateTypeValidation: Bool = Default.generateTypeValidation, + generateDecodableTypes: Bool = Default.generateDecodableTypes, operationIdentifiersPath: String? ) { self.schemaTypes = schemaTypes self.operations = operations self.testMocks = testMocks self.operationIDsPath = operationIdentifiersPath - self.generateTypeValidation = generateTypeValidation + self.generateDecodableTypes = generateDecodableTypes } /// An absolute location to an operation id JSON map file. diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift index 7aee98c4b..fd6bb421a 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/FragmentTemplate.swift @@ -25,7 +25,7 @@ struct FragmentTemplate: TemplateRenderer { struct \(fragment.generatedDefinitionName.asFragmentName): \ \(fragment.renderedSelectionSetType(config)), Fragment\ \(if: fragment.isIdentifiable, ", Identifiable")\ - \(if: config.config.output.generateTypeValidation, ", Validatable, Codable")\ + \(if: config.config.output.generateDecodableTypes, ", Encodable")\ { \(if: includeDefinition, """ \(accessControlModifier(for: .member))\ @@ -37,7 +37,7 @@ struct FragmentTemplate: TemplateRenderer { \(SelectionSetTemplate( definition: fragment, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: fragment), - generateTypeValidation: config.config.output.generateTypeValidation, + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift index 96903be27..0021140a9 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/LocalCacheMutationDefinitionTemplate.swift @@ -33,7 +33,7 @@ struct LocalCacheMutationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), - generateTypeValidation: config.config.output.generateTypeValidation, + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index 2f55eaa30..ee22dd942 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -36,7 +36,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { \(SelectionSetTemplate( definition: operation, generateInitializers: config.config.shouldGenerateSelectionSetInitializers(for: operation), - generateTypeValidation: config.config.output.generateTypeValidation, + generateDecodableTypes: config.config.output.generateDecodableTypes, config: config, nonFatalErrorRecorder: nonFatalErrorRecorder, renderAccessControl: { accessControlModifier(for: .member) }() diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift index c4fd44a0a..35fecff55 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SelectionSetTemplate.swift @@ -9,7 +9,7 @@ struct SelectionSetTemplate { let definition: any IR.Definition let generateInitializers: Bool - let generateTypeValidation: Bool + let generateDecodableTypes: Bool let config: ApolloCodegen.ConfigurationContext let nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder let renderAccessControl: () -> String @@ -21,14 +21,14 @@ struct SelectionSetTemplate { init( definition: any IR.Definition, generateInitializers: Bool, - generateTypeValidation: Bool, + generateDecodableTypes: Bool, config: ApolloCodegen.ConfigurationContext, nonFatalErrorRecorder: ApolloCodegen.NonFatalError.Recorder, renderAccessControl: @autoclosure @escaping () -> String ) { self.definition = definition self.generateInitializers = generateInitializers - self.generateTypeValidation = generateTypeValidation + self.generateDecodableTypes = generateDecodableTypes self.config = config self.nonFatalErrorRecorder = nonFatalErrorRecorder self.renderAccessControl = renderAccessControl @@ -107,7 +107,7 @@ struct SelectionSetTemplate { \(renderAccessControl())\ struct \(fieldSelectionSetName): \(SelectionSetType())\ \(if: selectionSet.isIdentifiable, ", Identifiable")\ - \(if: config.output.generateTypeValidation, ", Decodable")\ + \(if: config.output.generateDecodableTypes, ", Encodable")\ { \(BodyTemplate(context)) } @@ -125,7 +125,7 @@ struct SelectionSetTemplate { struct \(inlineFragment.renderedTypeName): \(SelectionSetType(asInlineFragment: true))\ \(if: inlineFragment.isCompositeInlineFragment, ", \(config.ApolloAPITargetName).CompositeInlineFragment")\ \(if: inlineFragment.isIdentifiable, ", Identifiable")\ - \(if: config.output.generateTypeValidation, ", Validatable, Codable")\ + \(if: config.output.generateDecodableTypes, ", Encodable")\ { \(BodyTemplate(context)) } @@ -176,8 +176,6 @@ struct SelectionSetTemplate { return """ \(DataPropertyTemplate()) \(DesignatedInitializerTemplate()) - \(section: "\(if: generateTypeValidation, ValidatorTemplate(selectionSet, inlineFragments: computedChildSelectionSets))") - \(section: "\(if: generateTypeValidation, CodableTemplate(selectionSet, inlineFragments: computedChildSelectionSets))") \(RootEntityTypealias(selectionSet)) \(ParentTypeTemplate(selectionSet.parentType)) @@ -197,7 +195,7 @@ struct SelectionSetTemplate { \(section: ChildTypeCaseSelectionSets(computedChildSelectionSets)) """ } - + private func DesignatedInitializerTemplate( _ propertiesTemplate: @autoclosure () -> TemplateString? = { nil }() ) -> String { @@ -219,82 +217,6 @@ struct SelectionSetTemplate { """ ).description } - - private func ValidatorTemplate(_ selectionSet: ComputedSelectionSet, inlineFragments: [SelectionSetContext]) -> TemplateString { - let scope = selectionSet.typeInfo.scope - return """ - \(renderAccessControl())static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - \(selectionSet.allFields.map { field in - """ - try value.validate(\(typeName(for: field, forceOptional: field.isConditionallyIncluded(in: scope))).self, for: "\(field.responseKey)") - """ - }, separator: "\n") - \(inlineFragments.map{ InlineFragmentValidatorTemplate($0.selectionSet) }, separator: "\n") - } - """ - } - - private func InlineFragmentValidatorTemplate( - _ inlineFragment: IR.ComputedSelectionSet - ) -> TemplateString { - guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } - - let typeName = inlineFragment.renderedTypeName - return """ - try value.\(typeName.firstLowercased)?.validate() - """ - } - - private func CodableTemplate(_ selectionSet: ComputedSelectionSet, inlineFragments: [SelectionSetContext]) -> TemplateString { - let scope = selectionSet.typeInfo.scope - return """ - \(renderAccessControl())init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - \(selectionSet.allFields.map { field in - """ - "\(field.responseKey)": try container.decode(\(typeName(for: field, forceOptional: field.isConditionallyIncluded(in: scope))).self, forKey: "\(field.responseKey)") - """ - }, separator: ",\n") - ], fulfilledFragments: [ - \(inlineFragments.map{ InlineFragmentDecodableTemplate($0.selectionSet) }, separator: ",\n") - ]) - } - - \(renderAccessControl())func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - \(selectionSet.allFields.map { field in - """ - try container.encode(\(field.responseKey.renderAsFieldPropertyName(config: config.config)), forKey: "\(field.responseKey)") - """ - }, separator: "\n") - \(inlineFragments.map{ InlineFragmentEncodableTemplate($0.selectionSet) }, separator: "\n") - } - """ - } - - private func InlineFragmentDecodableTemplate( - _ inlineFragment: IR.ComputedSelectionSet - ) -> TemplateString { - guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } - - let typeName = inlineFragment.renderedTypeName - return "(try? \(typeName)(from: decoder)) != nil ? ObjectIdentifier(\(typeName).self): nil" - } - - private func InlineFragmentEncodableTemplate( - _ inlineFragment: IR.ComputedSelectionSet - ) -> TemplateString { - guard !inlineFragment.typeInfo.scope.isDeferred else { return "" } - - let typeName = inlineFragment.renderedTypeName - return """ - try self.\(typeName.firstLowercased)?.encode(to: encoder) - """ - } private func DataPropertyTemplate() -> TemplateString { "\(renderAccessControl())\(isMutable ? "var" : "let") __data: DataDict" @@ -1345,14 +1267,3 @@ extension OrderedDictionary { values.contains(where: { $0.typeInfo.deferCondition != nil }) } } - -extension ComputedSelectionSet { - var allFields: [IR.Field] { - var fields: [IR.Field] = [] - if let directFields = direct?.fields.values { - fields.append(contentsOf: directFields) - } - fields.append(contentsOf: merged.fields.values) - return fields - } -} diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index 39df12b92..2c5aa9003 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -203,7 +203,7 @@ struct StringKey: CodingKey { } -extension DataDict: @retroactive Encodable { +extension DataDict: Encodable { public func encode(to encoder: any Encoder) throws { let keys = _data.keys.sorted() var container = encoder.container(keyedBy: StringKey.self) @@ -217,7 +217,7 @@ extension DataDict: @retroactive Encodable { extension AnyHashable { func encode(in container: inout KeyedEncodingContainer, at key: StringKey) throws { - if let encodableValue = self.base as? Encodable { + if let encodableValue = self.base as? (any Encodable) { try encodableValue.encode(to: container.superEncoder(forKey: key)) } else if let array = self.base as? [AnyHashable] { var arrayContainer = container.nestedUnkeyedContainer(forKey: key) @@ -233,13 +233,19 @@ extension AnyHashable { try arrayContainer.encodeNil() } } + } else if let optional = self.base as? Optional { + if let value = optional { + try value.encode(in: &container, at: key) + } else { + try container.encodeNil(forKey: key) + } } else { throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) } } - func encode(in container: inout UnkeyedEncodingContainer) throws { - if let encodableValue = self.base as? Encodable { + func encode(in container: inout any UnkeyedEncodingContainer) throws { + if let encodableValue = self.base as? (any Encodable) { try encodableValue.encode(to: container.superEncoder()) } else if let array = self.base as? [AnyHashable] { var arrayContainer = container.nestedUnkeyedContainer() @@ -255,6 +261,12 @@ extension AnyHashable { try arrayContainer.encodeNil() } } + } else if let optional = self.base as? Optional { + if let value = optional { + try value.encode(in: &container) + } else { + try container.encodeNil() + } } else { throw EncodingError.invalidValue(self.base, .init(codingPath: container.codingPath, debugDescription: "Unexpected type for encoding: \(type(of: self.base))")) } diff --git a/test-codegen/Sources/testComplexQuery.graphql b/test-codegen/Sources/testComplexQuery.graphql deleted file mode 100644 index ec79b6a27..000000000 --- a/test-codegen/Sources/testComplexQuery.graphql +++ /dev/null @@ -1,41 +0,0 @@ -query TestComplexQuery($after: String, $before: String, $first: Int, $last: Int) { - allFilms(after: $after, before: $before, first: $first, last: $last) { - films { - ...FilmFragment - } - } -} - -fragment NodeFragment on Node { - id - ...on Person { - name - goodOrBad - homeworld { - name - } - } - ...PlanetInfo - ...PersonOrPlanetInfo -} - -fragment PlanetInfo on Planet { - name - orbitalPeriod -} - -fragment PersonOrPlanetInfo on PersonOrPlanet { - ...on Person { - nestedStringArray - nestedPlanetArray { - name - } - homeworld { - climates - } - } - ...on Planet { - climates - diameter - } -} \ No newline at end of file diff --git a/test-codegen/Sources/testQuery.graphql b/test-codegen/Sources/testQuery.graphql deleted file mode 100644 index 35d568c2f..000000000 --- a/test-codegen/Sources/testQuery.graphql +++ /dev/null @@ -1,12 +0,0 @@ -query TestQuery($after: String, $before: String, $first: Int, $last: Int) { - allFilms(after: $after, before: $before, first: $first, last: $last) { - films { - ...FilmFragment - } - } -} - -fragment FilmFragment on Film { - director - episodeID -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Package.swift b/test-codegen/SwapiSchema/Package.swift deleted file mode 100644 index 2e820f0d5..000000000 --- a/test-codegen/SwapiSchema/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version:5.9 - -import PackageDescription - -let package = Package( - name: "SwapiSchema", - platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5), - ], - products: [ - .library(name: "SwapiSchema", targets: ["SwapiSchema"]), - ], - dependencies: [ - .package(url: "https://github.com/apollographql/apollo-ios", exact: "1.23.0"), - ], - targets: [ - .target( - name: "SwapiSchema", - dependencies: [ - .product(name: "ApolloAPI", package: "apollo-ios"), - ], - path: "./Sources" - ), - ] -) diff --git a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift b/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift deleted file mode 100644 index 41038a636..000000000 --- a/test-codegen/SwapiSchema/Sources/ApolloAPIExtensions.swift +++ /dev/null @@ -1,102 +0,0 @@ -import ApolloAPI - -public protocol Validatable { - static func validate(value: Self?) throws -} - -public enum ValidationError: Error { - case dataIsNil - case dataCorrupted -} - -extension AnyScalarType { - public static func validate(value: Self?) throws { - guard value != nil else { - throw ValidationError.dataIsNil - } - } -} - -extension String: Validatable {} -extension Int: Validatable {} -extension Bool: Validatable {} -extension Float: Validatable {} -extension Double: Validatable {} -extension GraphQLEnum: Validatable {} - -extension Optional: Validatable where Wrapped: Validatable { - public static func validate(value: Wrapped??) throws { - switch value { - case .some(let value): - try Wrapped.validate(value: value) - case .none: - break - } - } -} - -extension Array: Validatable where Element: Validatable { - public static func validate(value: [Element]?) throws { - guard let value = value else { - throw ValidationError.dataIsNil - } - - for element in value { - try Element.validate(value: element) - } - } -} - -extension ApolloAPI.SelectionSet { - public func validate(_: T.Type, for key: String) throws { - let value: T? = self.__data[key] - try T.validate(value: value) - } - public func validate(_: T.Type, for key: String) throws { - let value: T? = self.__data[key] - try T.validate(value: value) - } -} - -extension Validatable { - public func validate() throws { - try Self.validate(value: self) - } -} - -// MARK - Codable - -//extension String: @retroactive CodingKey { -// public var stringValue: String { -// self -// } -// public var intValue: Int? { -// nil -// } -// public init?(intValue: Int) { -// nil -// } -// public init?(stringValue: String) { -// self = stringValue -// } -//} - -extension GraphQLEnum: Codable { - -} - -extension DataDict { - public init( - data: [String: AnyHashable], - fulfilledFragments: [ObjectIdentifier?], - deferredFragments: [ObjectIdentifier?] = [] - ) { - self.init(data: data, fulfilledFragments: Set(fulfilledFragments.compactMap { $0 }), deferredFragments: Set(deferredFragments.compactMap { $0 })) - } -} - -extension ApolloAPI.SelectionSet { - public func encode(to encoder: Encoder) throws { - try self.__data.encode(to: encoder) - } -} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift deleted file mode 100644 index 04b459497..000000000 --- a/test-codegen/SwapiSchema/Sources/Fragments/FilmFragment.graphql.swift +++ /dev/null @@ -1,46 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public struct FilmFragment: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { - public static var fragmentDefinition: StaticString { - #"fragment FilmFragment on Film { __typename director episodeID }"# - } - - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "director") - try value.validate(Int?.self, for: "episodeID") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "director": try container.decode(String?.self, forKey: "director"), - "episodeID": try container.decode(Int?.self, forKey: "episodeID") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(director, forKey: "director") - try container.encode(episodeID, forKey: "episodeID") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("director", String?.self), - .field("episodeID", Int?.self), - ] } - - /// The name of the director of this film. - public var director: String? { __data["director"] } - /// The episode number of this film. - public var episodeID: Int? { __data["episodeID"] } -} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift deleted file mode 100644 index b4fb6a18f..000000000 --- a/test-codegen/SwapiSchema/Sources/Fragments/NodeFragment.graphql.swift +++ /dev/null @@ -1,271 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public struct NodeFragment: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { - public static var fragmentDefinition: StaticString { - #"fragment NodeFragment on Node { __typename id ... on Person { name goodOrBad homeworld { __typename name } } ...PlanetInfo ...PersonOrPlanetInfo }"# - } - - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(SwapiSchema.ID.self, for: "id") - try value.asPerson?.validate() - try value.asPlanet?.validate() - try value.asPersonOrPlanet?.validate() - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "id": try container.decode(SwapiSchema.ID.self, forKey: "id") - ], fulfilledFragments: [ - (try? AsPerson(from: decoder)) != nil ? ObjectIdentifier(AsPerson.self): nil, - (try? AsPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPlanet.self): nil, - (try? AsPersonOrPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPersonOrPlanet.self): nil - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(id, forKey: "id") - try self.asPerson?.encode(to: encoder) - try self.asPlanet?.encode(to: encoder) - try self.asPersonOrPlanet?.encode(to: encoder) - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Interfaces.Node } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("id", SwapiSchema.ID.self), - .inlineFragment(AsPerson.self), - .inlineFragment(AsPlanet.self), - .inlineFragment(AsPersonOrPlanet.self), - ] } - - /// The id of the object. - public var id: SwapiSchema.ID { __data["id"] } - - public var asPerson: AsPerson? { _asInlineFragment() } - public var asPlanet: AsPlanet? { _asInlineFragment() } - public var asPersonOrPlanet: AsPersonOrPlanet? { _asInlineFragment() } - - /// AsPerson - /// - /// Parent Type: `Person` - public struct AsPerson: SwapiSchema.InlineFragment, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "name") - try value.validate(GraphQLEnum?.self, for: "goodOrBad") - try value.validate(Homeworld?.self, for: "homeworld") - try value.validate(SwapiSchema.ID.self, for: "id") - try value.validate([[[String?]?]?].self, for: "nestedStringArray") - try value.validate([[[NestedPlanetArray?]?]?].self, for: "nestedPlanetArray") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "name": try container.decode(String?.self, forKey: "name"), - "goodOrBad": try container.decode(GraphQLEnum?.self, forKey: "goodOrBad"), - "homeworld": try container.decode(Homeworld?.self, forKey: "homeworld"), - "id": try container.decode(SwapiSchema.ID.self, forKey: "id"), - "nestedStringArray": try container.decode([[[String?]?]?].self, forKey: "nestedStringArray"), - "nestedPlanetArray": try container.decode([[[NestedPlanetArray?]?]?].self, forKey: "nestedPlanetArray") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(name, forKey: "name") - try container.encode(goodOrBad, forKey: "goodOrBad") - try container.encode(homeworld, forKey: "homeworld") - try container.encode(id, forKey: "id") - try container.encode(nestedStringArray, forKey: "nestedStringArray") - try container.encode(nestedPlanetArray, forKey: "nestedPlanetArray") - } - - public typealias RootEntityType = NodeFragment - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Person } - public static var __selections: [ApolloAPI.Selection] { [ - .field("name", String?.self), - .field("goodOrBad", GraphQLEnum?.self), - .field("homeworld", Homeworld?.self), - ] } - - /// The name of this person. - public var name: String? { __data["name"] } - /// Whether this is a good person or a bad one - public var goodOrBad: GraphQLEnum? { __data["goodOrBad"] } - /// A planet that this person was born on or inhabits. - public var homeworld: Homeworld? { __data["homeworld"] } - /// The id of the object. - public var id: SwapiSchema.ID { __data["id"] } - public var nestedStringArray: [[[String?]?]?] { __data["nestedStringArray"] } - public var nestedPlanetArray: [[[NestedPlanetArray?]?]?] { __data["nestedPlanetArray"] } - - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } - } - - /// AsPerson.Homeworld - /// - /// Parent Type: `Planet` - public struct Homeworld: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "name") - try value.validate([String?]?.self, for: "climates") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "name": try container.decode(String?.self, forKey: "name"), - "climates": try container.decode([String?]?.self, forKey: "climates") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(name, forKey: "name") - try container.encode(climates, forKey: "climates") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("name", String?.self), - ] } - - /// The name of this planet. - public var name: String? { __data["name"] } - /// The climates of this planet. - public var climates: [String?]? { __data["climates"] } - } - - public typealias NestedPlanetArray = PersonOrPlanetInfo.AsPerson.NestedPlanetArray - } - - /// AsPlanet - /// - /// Parent Type: `Planet` - public struct AsPlanet: SwapiSchema.InlineFragment, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(SwapiSchema.ID.self, for: "id") - try value.validate(String?.self, for: "name") - try value.validate(Int?.self, for: "orbitalPeriod") - try value.validate([String?]?.self, for: "climates") - try value.validate(Int?.self, for: "diameter") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "id": try container.decode(SwapiSchema.ID.self, forKey: "id"), - "name": try container.decode(String?.self, forKey: "name"), - "orbitalPeriod": try container.decode(Int?.self, forKey: "orbitalPeriod"), - "climates": try container.decode([String?]?.self, forKey: "climates"), - "diameter": try container.decode(Int?.self, forKey: "diameter") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(id, forKey: "id") - try container.encode(name, forKey: "name") - try container.encode(orbitalPeriod, forKey: "orbitalPeriod") - try container.encode(climates, forKey: "climates") - try container.encode(diameter, forKey: "diameter") - } - - public typealias RootEntityType = NodeFragment - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .fragment(PlanetInfo.self), - ] } - - /// The id of the object. - public var id: SwapiSchema.ID { __data["id"] } - /// The name of this planet. - public var name: String? { __data["name"] } - /// The number of standard days it takes for this planet to complete a single orbit - /// of its local star. - public var orbitalPeriod: Int? { __data["orbitalPeriod"] } - /// The climates of this planet. - public var climates: [String?]? { __data["climates"] } - /// The diameter of this planet in kilometers. - public var diameter: Int? { __data["diameter"] } - - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var planetInfo: PlanetInfo { _toFragment() } - public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } - } - } - - /// AsPersonOrPlanet - /// - /// Parent Type: `PersonOrPlanet` - public struct AsPersonOrPlanet: SwapiSchema.InlineFragment, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(SwapiSchema.ID.self, for: "id") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "id": try container.decode(SwapiSchema.ID.self, forKey: "id") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(id, forKey: "id") - } - - public typealias RootEntityType = NodeFragment - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Unions.PersonOrPlanet } - public static var __selections: [ApolloAPI.Selection] { [ - .fragment(PersonOrPlanetInfo.self), - ] } - - /// The id of the object. - public var id: SwapiSchema.ID { __data["id"] } - - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var personOrPlanetInfo: PersonOrPlanetInfo { _toFragment() } - } - } -} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift deleted file mode 100644 index 4cabc0d6f..000000000 --- a/test-codegen/SwapiSchema/Sources/Fragments/PersonOrPlanetInfo.graphql.swift +++ /dev/null @@ -1,200 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public struct PersonOrPlanetInfo: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { - public static var fragmentDefinition: StaticString { - #"fragment PersonOrPlanetInfo on PersonOrPlanet { __typename ... on Person { nestedStringArray nestedPlanetArray { __typename name } homeworld { __typename climates } } ... on Planet { climates diameter } }"# - } - - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.asPerson?.validate() - try value.asPlanet?.validate() - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - ], fulfilledFragments: [ - (try? AsPerson(from: decoder)) != nil ? ObjectIdentifier(AsPerson.self): nil, - (try? AsPlanet(from: decoder)) != nil ? ObjectIdentifier(AsPlanet.self): nil - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try self.asPerson?.encode(to: encoder) - try self.asPlanet?.encode(to: encoder) - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Unions.PersonOrPlanet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .inlineFragment(AsPerson.self), - .inlineFragment(AsPlanet.self), - ] } - - public var asPerson: AsPerson? { _asInlineFragment() } - public var asPlanet: AsPlanet? { _asInlineFragment() } - - /// AsPerson - /// - /// Parent Type: `Person` - public struct AsPerson: SwapiSchema.InlineFragment, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate([[[String?]?]?].self, for: "nestedStringArray") - try value.validate([[[NestedPlanetArray?]?]?].self, for: "nestedPlanetArray") - try value.validate(Homeworld?.self, for: "homeworld") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "nestedStringArray": try container.decode([[[String?]?]?].self, forKey: "nestedStringArray"), - "nestedPlanetArray": try container.decode([[[NestedPlanetArray?]?]?].self, forKey: "nestedPlanetArray"), - "homeworld": try container.decode(Homeworld?.self, forKey: "homeworld") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(nestedStringArray, forKey: "nestedStringArray") - try container.encode(nestedPlanetArray, forKey: "nestedPlanetArray") - try container.encode(homeworld, forKey: "homeworld") - } - - public typealias RootEntityType = PersonOrPlanetInfo - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Person } - public static var __selections: [ApolloAPI.Selection] { [ - .field("nestedStringArray", [[[String?]?]?].self), - .field("nestedPlanetArray", [[[NestedPlanetArray?]?]?].self), - .field("homeworld", Homeworld?.self), - ] } - - public var nestedStringArray: [[[String?]?]?] { __data["nestedStringArray"] } - public var nestedPlanetArray: [[[NestedPlanetArray?]?]?] { __data["nestedPlanetArray"] } - /// A planet that this person was born on or inhabits. - public var homeworld: Homeworld? { __data["homeworld"] } - - /// AsPerson.NestedPlanetArray - /// - /// Parent Type: `Planet` - public struct NestedPlanetArray: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "name") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "name": try container.decode(String?.self, forKey: "name") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(name, forKey: "name") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("name", String?.self), - ] } - - /// The name of this planet. - public var name: String? { __data["name"] } - } - - /// AsPerson.Homeworld - /// - /// Parent Type: `Planet` - public struct Homeworld: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate([String?]?.self, for: "climates") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "climates": try container.decode([String?]?.self, forKey: "climates") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(climates, forKey: "climates") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("climates", [String?]?.self), - ] } - - /// The climates of this planet. - public var climates: [String?]? { __data["climates"] } - } - } - - /// AsPlanet - /// - /// Parent Type: `Planet` - public struct AsPlanet: SwapiSchema.InlineFragment, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate([String?]?.self, for: "climates") - try value.validate(Int?.self, for: "diameter") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "climates": try container.decode([String?]?.self, forKey: "climates"), - "diameter": try container.decode(Int?.self, forKey: "diameter") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(climates, forKey: "climates") - try container.encode(diameter, forKey: "diameter") - } - - public typealias RootEntityType = PersonOrPlanetInfo - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("climates", [String?]?.self), - .field("diameter", Int?.self), - ] } - - /// The climates of this planet. - public var climates: [String?]? { __data["climates"] } - /// The diameter of this planet in kilometers. - public var diameter: Int? { __data["diameter"] } - } -} diff --git a/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift b/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift deleted file mode 100644 index 1d316b932..000000000 --- a/test-codegen/SwapiSchema/Sources/Fragments/PlanetInfo.graphql.swift +++ /dev/null @@ -1,47 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public struct PlanetInfo: SwapiSchema.SelectionSet, Fragment, Validatable, Codable { - public static var fragmentDefinition: StaticString { - #"fragment PlanetInfo on Planet { __typename name orbitalPeriod }"# - } - - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "name") - try value.validate(Int?.self, for: "orbitalPeriod") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "name": try container.decode(String?.self, forKey: "name"), - "orbitalPeriod": try container.decode(Int?.self, forKey: "orbitalPeriod") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(name, forKey: "name") - try container.encode(orbitalPeriod, forKey: "orbitalPeriod") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Planet } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("name", String?.self), - .field("orbitalPeriod", Int?.self), - ] } - - /// The name of this planet. - public var name: String? { __data["name"] } - /// The number of standard days it takes for this planet to complete a single orbit - /// of its local star. - public var orbitalPeriod: Int? { __data["orbitalPeriod"] } -} diff --git a/test-codegen/SwapiSchema/Sources/JSON.swift b/test-codegen/SwapiSchema/Sources/JSON.swift deleted file mode 100644 index 0446e69dc..000000000 --- a/test-codegen/SwapiSchema/Sources/JSON.swift +++ /dev/null @@ -1,396 +0,0 @@ -// -// JSON.swift -// SwapiSchema -// -// Created by Guigui on 8/12/25. - -import Foundation - -// MARK: - JSON - -public enum JSON: Codable, Equatable, Sendable { - case object(_ value: [String: JSON.Value]) - case array(_ value: [JSON.Value]) - - // MARK: - JSONValue - - public enum Value: Codable, Equatable, Sendable { - case string(_ value: String) - case object(_ value: [String: JSON.Value]) - case array(_ value: [JSON.Value]) - case bool(_ value: Bool) - case number(_ value: Double) - case null - } -} - -// MARK: ExpressibleByDictionaryLiteral - -extension JSON: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, JSON.Value)...) { - self.init(dictionaryLiteral: elements) - } - - public init(dictionaryLiteral elements: [(String, JSON.Value)]) { - var object = [String: JSON.Value]() - - for element in elements { - object[element.0] = element.1 - } - - self = .object(object) - } - - public init(_ dictionary: [String: some JSONValueConvertible]) { - self.init(dictionaryLiteral: dictionary.map { k, v in (k, v.asJSONValue) }) - } -} - -// MARK: ExpressibleByArrayLiteral - -extension JSON: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: JSON.Value...) { - var array = [JSON.Value]() - - for element in elements { - array.append(element) - } - - self = .array(array) - } -} - -// MARK: - JSON.Value + ExpressibleByNilLiteral - -extension JSON.Value: ExpressibleByNilLiteral { - public init(nilLiteral _: ()) { - self = .null - } -} - -// MARK: - JSON.Value + ExpressibleByDictionaryLiteral - -extension JSON.Value: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, JSON.Value)...) { - var object = [String: JSON.Value]() - - for element in elements { - object[element.0] = element.1 - } - - self = .object(object) - } -} - -// MARK: - JSON.Value + ExpressibleByStringLiteral - -extension JSON.Value: ExpressibleByStringLiteral { - public init(stringLiteral: String) { - self = .string(stringLiteral) - } -} - -// MARK: - JSON.Value + ExpressibleByIntegerLiteral - -extension JSON.Value: ExpressibleByIntegerLiteral { - public init(integerLiteral value: IntegerLiteralType) { - self = .number(Double(value)) - } -} - -// MARK: - JSON.Value + ExpressibleByFloatLiteral - -extension JSON.Value: ExpressibleByFloatLiteral { - public init(floatLiteral value: FloatLiteralType) { - self = .number(value) - } -} - -// MARK: - JSON.Value + ExpressibleByArrayLiteral - -extension JSON.Value: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: JSON.Value...) { - var array = [JSON.Value]() - - for element in elements { - array.append(element) - } - - self = .array(array) - } -} - -// MARK: - JSON.Value + ExpressibleByBooleanLiteral - -extension JSON.Value: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: BooleanLiteralType) { - self = .bool(value) - } -} - -extension JSON { - - public init(from decoder: Decoder) throws { - do { - // Object - let container = try decoder.container(keyedBy: String.self) - let keys = container.allKeys - let object = keys.reduce(into: [String: JSON.Value]()) { result, key in - if let value = try? container.decode(JSON.Value.self, forKey: key) { - result[key] = value - } - } - self = .object(object) - } catch { - // Array - var container = try decoder.unkeyedContainer() - var array: [JSON.Value] = [] - while !container.isAtEnd { - if let value = try? container.decode(JSON.Value.self) { - array.append(value) - } - } - self = .array(array) - } - } - - public static func ==(lhs: JSON, rhs: JSON) -> Bool { - lhs.asValue == rhs.asValue - } - - public func encode(to encoder: any Encoder) throws { - try asValue.encode(to: encoder) - } - - var asValue: JSON.Value { - switch self { - case .object(let value): - .object(value) - case .array(let value): - .array(value) - } - } -} - -extension JSON.Value { - - public init(from decoder: Decoder) throws { - do { - // Single values - let container = try decoder.singleValueContainer() - if container.decodeNil() { - self = .null - } else if let value = try? container.decode(String.self) { - self = .string(value) - } else if let value = try? container.decode(Bool.self) { - self = .bool(value) - } else if let value = try? container.decode(Double.self) { - self = .number(value) - } else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid JSON") - } - } catch { - // Object - do { - let container = try decoder.container(keyedBy: String.self) - let keys = container.allKeys - let object = keys.reduce(into: [String: JSON.Value]()) { result, key in - if let value = try? container.decode(JSON.Value.self, forKey: key) { - result[key] = value - } - } - self = .object(object) - } catch { - // Array - var container = try decoder.unkeyedContainer() - var array: [JSON.Value] = [] - while !container.isAtEnd { - if let value = try? container.decode(JSON.Value.self) { - array.append(value) - } - } - self = .array(array) - } - } - } - - public static func ==(lhs: JSON.Value, rhs: JSON.Value) -> Bool { - switch (lhs, rhs) { - case (.null, .null): - true - case (.string(let lhs), .string(let rhs)): - lhs == rhs - case (.bool(let lhs), .bool(let rhs)): - lhs == rhs - case (.number(let lhs), .number(let rhs)): - lhs == rhs - case (.object(let lhs), .object(let rhs)): - lhs == rhs - case (.array(let lhs), .array(let rhs)): - lhs == rhs - default: - false - } - } - - public func encode(to encoder: any Encoder) throws { - switch self { - case .null: - var container = encoder.singleValueContainer() - try container.encodeNil() - - case .string(let value): - var container = encoder.singleValueContainer() - try container.encode(value) - - case .bool(let value): - var container = encoder.singleValueContainer() - try container.encode(value) - - case .number(let value): - var container = encoder.singleValueContainer() - try container.encode(value) - - case .object(let value): - var container = encoder.container(keyedBy: String.self) - for (key, value) in value { - try container.encode(value, forKey: key) - } - - case .array(let value): - var container = encoder.unkeyedContainer() - for value in value { - try container.encode(value) - } - } - } -} - -extension JSON { - public var asAny: Any { - switch self { - case .object(let value): - value.keys.reduce(into: [String: Any]()) { result, key in - result[key] = value[key]?.asAny - } - - case .array(let value): - value.map(\.asAny) - } - } - - public var asObject: [String: JSON.Value]? { - guard case .object(let value) = self else { return nil } - return value - } - - public var asArray: [JSON.Value]? { - guard case .array(let value) = self else { return nil } - return value - } - - public func asJSONData(options: JSONSerialization.WritingOptions = []) throws -> Data { - try JSONSerialization.data(withJSONObject: asAny, options: options) - } - - public func asJSONString(options: JSONSerialization.WritingOptions = []) throws -> String { - guard let string = try String(data: asJSONData(options: options), encoding: .utf8) else { - throw NSError(domain: "JSON", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to convert JSON to string"]) - } - return string - } -} - -extension JSON.Value { - public var asAny: Any { - switch self { - case .string(let value): - value - - case .object(let value): - value.keys.reduce(into: [String: Any]()) { result, key in - result[key] = value[key]?.asAny - } - - case .array(let value): - value.map(\.asAny) - - case .bool(let value): - value - - case .number(let value): - value - - case .null: - NSNull() - } - } - - public var asString: String? { - guard case .string(let value) = self else { return nil } - return value - } - - public var asObject: [String: JSON.Value]? { - guard case .object(let value) = self else { return nil } - return value - } - - public var asArray: [JSON.Value]? { - guard case .array(let value) = self else { return nil } - return value - } - - public var asBool: Bool? { - guard case .bool(let value) = self else { return nil } - return value - } - - public var asNumber: Double? { - guard case .number(let value) = self else { return nil } - return value - } -} - -// MARK: - String + CodingKey - -extension String: @retroactive CodingKey { - - public init?(stringValue: String) { - self = stringValue - } - - public init?(intValue: Int) { - self = "\(intValue)" - } - - public var stringValue: String { self } - public var intValue: Int? { Int(self) } -} - -public protocol JSONValueConvertible { - var asJSONValue: JSON.Value { get } -} - -extension String: JSONValueConvertible { - public var asJSONValue: JSON.Value { .string(self) } -} - -extension Int: JSONValueConvertible { - public var asJSONValue: JSON.Value { .number(Double(self)) } -} - -extension Bool: JSONValueConvertible { - public var asJSONValue: JSON.Value { .bool(self) } -} - -extension Optional: JSONValueConvertible where Wrapped: JSONValueConvertible { - public var asJSONValue: JSON.Value { - switch self { - case .none: - .null - case .some(let value): - value.asJSONValue - } - } -} diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift deleted file mode 100644 index e8df0733a..000000000 --- a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestComplexQuery.graphql.swift +++ /dev/null @@ -1,159 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public class TestComplexQuery: GraphQLQuery { - public static let operationName: String = "TestComplexQuery" - public static let operationDocument: ApolloAPI.OperationDocument = .init( - definition: .init( - #"query TestComplexQuery($after: String, $before: String, $first: Int, $last: Int) { allFilms(after: $after, before: $before, first: $first, last: $last) { __typename films { __typename ...FilmFragment } } }"#, - fragments: [FilmFragment.self] - )) - - public var after: GraphQLNullable - public var before: GraphQLNullable - public var first: GraphQLNullable - public var last: GraphQLNullable - - public init( - after: GraphQLNullable, - before: GraphQLNullable, - first: GraphQLNullable, - last: GraphQLNullable - ) { - self.after = after - self.before = before - self.first = first - self.last = last - } - - public var __variables: Variables? { [ - "after": after, - "before": before, - "first": first, - "last": last - ] } - - public struct Data: SwapiSchema.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(AllFilms?.self, for: "allFilms") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "allFilms": try container.decode(AllFilms?.self, forKey: "allFilms") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(allFilms, forKey: "allFilms") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } - public static var __selections: [ApolloAPI.Selection] { [ - .field("allFilms", AllFilms?.self, arguments: [ - "after": .variable("after"), - "before": .variable("before"), - "first": .variable("first"), - "last": .variable("last") - ]), - ] } - - public var allFilms: AllFilms? { __data["allFilms"] } - - /// AllFilms - /// - /// Parent Type: `FilmsConnection` - public struct AllFilms: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate([Film?]?.self, for: "films") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "films": try container.decode([Film?]?.self, forKey: "films") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(films, forKey: "films") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("films", [Film?]?.self), - ] } - - /// A list of all of the objects returned in the connection. This is a convenience - /// field provided for quickly exploring the API; rather than querying for - /// "{ edges { node } }" when no edge data is needed, this field can be be used - /// instead. Note that when clients like Relay need to fetch the "cursor" field on - /// the edge to enable efficient pagination, this shortcut cannot be used, and the - /// full "{ edges { node } }" version should be used instead. - public var films: [Film?]? { __data["films"] } - - /// AllFilms.Film - /// - /// Parent Type: `Film` - public struct Film: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "director") - try value.validate(Int?.self, for: "episodeID") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "director": try container.decode(String?.self, forKey: "director"), - "episodeID": try container.decode(Int?.self, forKey: "episodeID") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(director, forKey: "director") - try container.encode(episodeID, forKey: "episodeID") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .fragment(FilmFragment.self), - ] } - - /// The name of the director of this film. - public var director: String? { __data["director"] } - /// The episode number of this film. - public var episodeID: Int? { __data["episodeID"] } - - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var filmFragment: FilmFragment { _toFragment() } - } - } - } - } -} diff --git a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift b/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift deleted file mode 100644 index d9a5c77c1..000000000 --- a/test-codegen/SwapiSchema/Sources/Operations/Queries/TestQuery.graphql.swift +++ /dev/null @@ -1,159 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -@_exported import ApolloAPI - -public class TestQuery: GraphQLQuery { - public static let operationName: String = "TestQuery" - public static let operationDocument: ApolloAPI.OperationDocument = .init( - definition: .init( - #"query TestQuery($after: String, $before: String, $first: Int, $last: Int) { allFilms(after: $after, before: $before, first: $first, last: $last) { __typename films { __typename ...FilmFragment } } }"#, - fragments: [FilmFragment.self] - )) - - public var after: GraphQLNullable - public var before: GraphQLNullable - public var first: GraphQLNullable - public var last: GraphQLNullable - - public init( - after: GraphQLNullable, - before: GraphQLNullable, - first: GraphQLNullable, - last: GraphQLNullable - ) { - self.after = after - self.before = before - self.first = first - self.last = last - } - - public var __variables: Variables? { [ - "after": after, - "before": before, - "first": first, - "last": last - ] } - - public struct Data: SwapiSchema.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(AllFilms?.self, for: "allFilms") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "allFilms": try container.decode(AllFilms?.self, forKey: "allFilms") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(allFilms, forKey: "allFilms") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Root } - public static var __selections: [ApolloAPI.Selection] { [ - .field("allFilms", AllFilms?.self, arguments: [ - "after": .variable("after"), - "before": .variable("before"), - "first": .variable("first"), - "last": .variable("last") - ]), - ] } - - public var allFilms: AllFilms? { __data["allFilms"] } - - /// AllFilms - /// - /// Parent Type: `FilmsConnection` - public struct AllFilms: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate([Film?]?.self, for: "films") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "films": try container.decode([Film?]?.self, forKey: "films") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(films, forKey: "films") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.FilmsConnection } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("films", [Film?]?.self), - ] } - - /// A list of all of the objects returned in the connection. This is a convenience - /// field provided for quickly exploring the API; rather than querying for - /// "{ edges { node } }" when no edge data is needed, this field can be be used - /// instead. Note that when clients like Relay need to fetch the "cursor" field on - /// the edge to enable efficient pagination, this shortcut cannot be used, and the - /// full "{ edges { node } }" version should be used instead. - public var films: [Film?]? { __data["films"] } - - /// AllFilms.Film - /// - /// Parent Type: `Film` - public struct Film: SwapiSchema.SelectionSet, Validatable, Codable { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - public static func validate(value: Self?) throws { - guard let value else { throw ValidationError.dataIsNil } - try value.validate(String?.self, for: "director") - try value.validate(Int?.self, for: "episodeID") - } - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: String.self) - __data = DataDict(data: [ - "__typename": try container.decode(String.self, forKey: "__typename"), - "director": try container.decode(String?.self, forKey: "director"), - "episodeID": try container.decode(Int?.self, forKey: "episodeID") - ], fulfilledFragments: [ - ]) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: String.self) - try container.encode(__typename, forKey: "__typename") - try container.encode(director, forKey: "director") - try container.encode(episodeID, forKey: "episodeID") - } - - public static var __parentType: any ApolloAPI.ParentType { SwapiSchema.Objects.Film } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .fragment(FilmFragment.self), - ] } - - /// The name of the director of this film. - public var director: String? { __data["director"] } - /// The episode number of this film. - public var episodeID: Int? { __data["episodeID"] } - - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var filmFragment: FilmFragment { _toFragment() } - } - } - } - } -} diff --git a/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift b/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift deleted file mode 100644 index 18e23c4cc..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/CustomScalars/ID.swift +++ /dev/null @@ -1,11 +0,0 @@ -// @generated -// This file was automatically generated and can be edited to -// implement advanced custom scalar functionality. -// -// Any changes to this file will not be overwritten by future -// code generation execution. - -import ApolloAPI - -/// The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID. -public typealias ID = String diff --git a/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift deleted file mode 100644 index 6c83ad658..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Enums/GoodOrBad.graphql.swift +++ /dev/null @@ -1,9 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public enum GoodOrBad: String, EnumType { - case good = "GOOD" - case bad = "BAD" -} diff --git a/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift deleted file mode 100644 index 229d73dc0..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Interfaces/Node.graphql.swift +++ /dev/null @@ -1,20 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Interfaces { - /// An object with an ID - static let Node = ApolloAPI.Interface( - name: "Node", - keyFields: nil, - implementingObjects: [ - "Film", - "Person", - "Planet", - "Species", - "Starship", - "Vehicle" - ] - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift deleted file mode 100644 index 75a0c1c11..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Film.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A single film. - static let Film = ApolloAPI.Object( - typename: "Film", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift deleted file mode 100644 index 3ec6933c6..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/FilmsConnection.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A connection to a list of items. - static let FilmsConnection = ApolloAPI.Object( - typename: "FilmsConnection", - implementedInterfaces: [], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift deleted file mode 100644 index 8916b98da..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Person.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// An individual person or character within the Star Wars universe. - static let Person = ApolloAPI.Object( - typename: "Person", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift deleted file mode 100644 index 0a9b9dd64..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Planet.graphql.swift +++ /dev/null @@ -1,14 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A large mass, planet or planetoid in the Star Wars Universe, at the time of - /// 0 ABY. - static let Planet = ApolloAPI.Object( - typename: "Planet", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift deleted file mode 100644 index 23657f671..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Root.graphql.swift +++ /dev/null @@ -1,12 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - static let Root = ApolloAPI.Object( - typename: "Root", - implementedInterfaces: [], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift deleted file mode 100644 index a6e8d6dee..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Species.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A type of person or character within the Star Wars Universe. - static let Species = ApolloAPI.Object( - typename: "Species", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift deleted file mode 100644 index 9d3cc70ab..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Starship.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A single transport craft that has hyperdrive capability. - static let Starship = ApolloAPI.Object( - typename: "Starship", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift deleted file mode 100644 index d8b92ebfa..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Objects/Vehicle.graphql.swift +++ /dev/null @@ -1,13 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Objects { - /// A single transport craft that does not have hyperdrive capability - static let Vehicle = ApolloAPI.Object( - typename: "Vehicle", - implementedInterfaces: [Interfaces.Node.self], - keyFields: nil - ) -} \ No newline at end of file diff --git a/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift b/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift deleted file mode 100644 index 87235012c..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/SchemaConfiguration.swift +++ /dev/null @@ -1,15 +0,0 @@ -// @generated -// This file was automatically generated and can be edited to -// provide custom configuration for a generated GraphQL schema. -// -// Any changes to this file will not be overwritten by future -// code generation execution. - -import ApolloAPI - -public enum SchemaConfiguration: ApolloAPI.SchemaConfiguration { - public static func cacheKeyInfo(for type: ApolloAPI.Object, object: ApolloAPI.ObjectData) -> CacheKeyInfo? { - // Implement this function to configure cache key resolution for your schema types. - return nil - } -} diff --git a/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift deleted file mode 100644 index 3ee5ac312..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/SchemaMetadata.graphql.swift +++ /dev/null @@ -1,38 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public protocol SelectionSet: ApolloAPI.SelectionSet & ApolloAPI.RootSelectionSet -where Schema == SwapiSchema.SchemaMetadata {} - -public protocol InlineFragment: ApolloAPI.SelectionSet & ApolloAPI.InlineFragment -where Schema == SwapiSchema.SchemaMetadata {} - -public protocol MutableSelectionSet: ApolloAPI.MutableRootSelectionSet -where Schema == SwapiSchema.SchemaMetadata {} - -public protocol MutableInlineFragment: ApolloAPI.MutableSelectionSet & ApolloAPI.InlineFragment -where Schema == SwapiSchema.SchemaMetadata {} - -public enum SchemaMetadata: ApolloAPI.SchemaMetadata { - public static let configuration: any ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self - - public static func objectType(forTypename typename: String) -> ApolloAPI.Object? { - switch typename { - case "Film": return SwapiSchema.Objects.Film - case "FilmsConnection": return SwapiSchema.Objects.FilmsConnection - case "Person": return SwapiSchema.Objects.Person - case "Planet": return SwapiSchema.Objects.Planet - case "Root": return SwapiSchema.Objects.Root - case "Species": return SwapiSchema.Objects.Species - case "Starship": return SwapiSchema.Objects.Starship - case "Vehicle": return SwapiSchema.Objects.Vehicle - default: return nil - } - } -} - -public enum Objects {} -public enum Interfaces {} -public enum Unions {} diff --git a/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift b/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift deleted file mode 100644 index 7e7d1e9f1..000000000 --- a/test-codegen/SwapiSchema/Sources/Schema/Unions/PersonOrPlanet.graphql.swift +++ /dev/null @@ -1,14 +0,0 @@ -// @generated -// This file was automatically generated and should not be edited. - -import ApolloAPI - -public extension Unions { - static let PersonOrPlanet = Union( - name: "PersonOrPlanet", - possibleTypes: [ - Objects.Person.self, - Objects.Planet.self - ] - ) -} \ No newline at end of file diff --git a/test-codegen/TestPackage/Package.swift b/test-codegen/TestPackage/Package.swift deleted file mode 100644 index 31160d594..000000000 --- a/test-codegen/TestPackage/Package.swift +++ /dev/null @@ -1,37 +0,0 @@ -// swift-tools-version:5.9 - -import PackageDescription - -let package = Package( - name: "TestPackage", - platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5), - ], - products: [ - .library(name: "TestPackage", targets: ["TestPackage"]), - ], - dependencies: [ - .package(path: "../SwapiSchema"), - .package(path: "../../apollo-ios"), - ], - targets: [ - .target( - name: "TestPackage", - dependencies: [ - .product(name: "SwapiSchema", package: "SwapiSchema"), - ], - path: "./Sources" - ), - .testTarget(name: "TestPackageTests", - dependencies: [ - "TestPackage", - .product(name: "SwapiSchema", package: "SwapiSchema"), - .product(name: "ApolloAPI", package: "apollo-ios"), - .product(name: "Apollo", package: "apollo-ios"), - ], - path: "./Tests") - ] -) diff --git a/test-codegen/TestPackage/Sources/TestPackage.swift b/test-codegen/TestPackage/Sources/TestPackage.swift deleted file mode 100644 index d24165e11..000000000 --- a/test-codegen/TestPackage/Sources/TestPackage.swift +++ /dev/null @@ -1,6 +0,0 @@ - -import SwapiSchema - -func test(data: TestQuery.Data) { - data.allFilms?.films?.count -} diff --git a/test-codegen/TestPackage/Tests/CodableTests.swift b/test-codegen/TestPackage/Tests/CodableTests.swift deleted file mode 100644 index 98779dbfc..000000000 --- a/test-codegen/TestPackage/Tests/CodableTests.swift +++ /dev/null @@ -1,152 +0,0 @@ - -import Testing -import Foundation -import SwapiSchema -@_spi(Execution) import Apollo - -struct CodableTests { - @Test - func testSimpleFragment() throws { - let json = """ - { - "__typename": "Film", - "director": "George Lucas", - "episodeID": 1 - } - """.utf8Data - - let value = try JSONDecoder().decode(FilmFragment.self, from: json) - let encoded = try JSONEncoder().encode(value) - - #expect(encoded.jsonString() == json.jsonString()) - } - - @Test - func testSimpleFragment_failsWithBadData() throws { - let json = """ - { - "__typename": "Film", - "director": "George Lucas" - } - """.utf8Data - - try expect({ - try JSONDecoder().decode(FilmFragment.self, from: json) - }, toThrow: { error in - guard let decodingError = error as? DecodingError else { - throw error - } - switch decodingError { - case .keyNotFound(let key, _): - #expect(key.stringValue == "episodeID") - default: - throw decodingError - } - }) - } - - @Test - func testComplexFragment_withSpecies() throws { - let json = """ - { - "__typename": "Species", - "id": "123", - } - """.utf8Data - - let value = try JSONDecoder().decode(NodeFragment.self, from: json) - let encoded = try JSONEncoder().encode(value) - - #expect(encoded.jsonString() == json.jsonString()) - #expect(value.id == "123") - #expect(value.__typename == "Species") - #expect(value.asPerson == nil) - #expect(value.asPlanet == nil) - } - - @Test - func testComplexFragment_withPerson() throws { -// let apolloClient = ApolloClient(url: URL(string: "http://localhost:4000/graphql")!) -// apolloClient.fetch(query: TestQuery(after: .none, before: .none, first: .none, last: .none)) { result in -// guard let data = try? result.get().data else { return } -// } - - - - - let json = """ - { - "__typename": "Person", - "id": "123", - "name": "Luke", - "goodOrBad": "GOOD", - "homeworld": { - "__typename": "Planet", - "name": "Tatooine", - "climates": [], - }, - "nestedStringArray": [[["one", "two"], ["three", "four"]]], - "nestedPlanetArray": [[[{ "__typename": "Planet", "name": "Tatouine" }], [{ "__typename": "Planet", "name": "Naboo" }]]], - } - """.utf8Data - - let dataEntry = try JSONSerialization.jsonObject(with: json, options: []) as! JSONObject - - let res = try GraphQLExecutor(executionSource: NetworkResponseExecutionSource()).execute( - selectionSet: NodeFragment.self, - on: dataEntry, // JSONObject - withRootCacheReference: nil, - variables: nil, - accumulator: GraphQLSelectionSetMapper() - ) - - let homeworld: NodeFragment.AsPerson.Homeworld? = res.__data["homeworld"] - let homeworld2: PersonOrPlanetInfo.AsPerson.Homeworld? = res.__data["homeworld"] - - let encodedData = try JSONEncoder().encode(res.__data) - let encodedStr = encodedData.jsonString() - - let value = try JSONDecoder().decode(NodeFragment.self, from: json) - let encoded = try JSONEncoder().encode(value) - - #expect(encoded.jsonString() == json.jsonString()) - #expect(value.id == "123") - #expect(value.__typename == "Person") - #expect(value.asPerson?.goodOrBad == .case(.good)) - - } - - @Test - func testComplexFragment_failsWithBadData() throws { - let json = """ - { - "__typename": "Film", - "director": "George Lucas" - } - """.utf8Data - - try expect({ - try JSONDecoder().decode(NodeFragment.self, from: json) - }, toThrow: { error in - guard let decodingError = error as? DecodingError else { - throw error - } - switch decodingError { - case .keyNotFound(let key, _): - #expect(key.stringValue == "episodeID") - default: - throw decodingError - } - }) - } - - private func expect(_ operation: () throws -> T, toThrow: (Error) throws -> Void) throws { - do { - _ = try operation() - Issue.record("Expected operation to throw an error, but it did not.") - } catch { - try toThrow(error) - } - } - -} diff --git a/test-codegen/TestPackage/Tests/JSON+helpers.swift b/test-codegen/TestPackage/Tests/JSON+helpers.swift deleted file mode 100644 index 0f554b5f0..000000000 --- a/test-codegen/TestPackage/Tests/JSON+helpers.swift +++ /dev/null @@ -1,106 +0,0 @@ -import Foundation -import Testing - -extension Data { - public func jsonString(ignoring ignoredKeys: [String] = []) -> String { - do { - var object = try JSONSerialization.jsonObject(with: self, options: []) - if !ignoredKeys.isEmpty { - object = (object as? [String: Any?])? - .filter { !ignoredKeys.contains($0.key) } as Any - } - let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys]) - guard let string = String(data: data, encoding: .utf8) else { - throw TestError("Invalid JSON data") - } - return string - } catch { - Issue.record(error) - return "" - } - } - - public func expectToMatch(_ expected: String, ignoring ignoredKeys: [String] = []) { - let received = jsonString(ignoring: ignoredKeys) - let expected = expected.utf8Data.jsonString(ignoring: ignoredKeys) - #expect(expected == received) - } - - public func expectToMatch(_ expected: String, ignoring ignoredKey: String) { - expectToMatch(expected, ignoring: [ignoredKey]) - } -} - -/// Test decoding the Json data to the given type, encoding it back to Json, and comparing the results. -public func testDecodingEncodingOf(_ json: String, with _: T.Type) throws { - let jsonData = json.utf8Data - let jsonDecoder = JSONDecoder() - let decoded = try jsonDecoder.decode(T.self, from: jsonData) - - let encoder = JSONEncoder() - let encoded = try encoder.encode(decoded) - - let value = encoded.jsonString() - let expected = jsonData.jsonString() - - #expect(expected == value) -} - -/// Test that encoding the value gives the expected json, and that decoding the json and re-encoding it doesn't change the value. -public func testDecodingEncoding( - of value: T, - _ json: String, - decoder: JSONDecoder = JSONDecoder(), - encoder: JSONEncoder = JSONEncoder()) - throws -{ - // Validate that encoding the value gives the expected json - try testEncoding(value, json, encoder: encoder) - - // Validate that decoding the json and re-encoding it gives the same json - let decoded = try decoder.decode(T.self, from: json.utf8Data) - let encoded = try encoder.encode(decoded) - - let value = encoded.jsonString() - let expected = json.utf8Data.jsonString() - - #expect(expected == value) -} - -/// Test that encoding the value gives the expected json. -public func testEncoding(_ value: some Encodable, _ json: String, encoder: JSONEncoder = JSONEncoder()) throws { - let encoded = try encoder.encode(value) - let encodedString = encoded.jsonString() - - // Reformat the json expectation (pretty print, sort keys) - let jsonData = json.utf8Data - let expected = jsonData.jsonString() - - #expect(expected == encodedString) -} - -/// Test that decoding the json gives the expected value. -public func testDecoding(_ value: T, _ json: String) throws { - let decoded = try JSONDecoder().decode(T.self, from: json.utf8Data) - #expect(decoded == value) -} - -/// Test that encoding the value gives the expected json, and that decoding the json gives the expected value. -public func testEncodingDecoding(_ value: some Codable & Equatable, _ json: String) throws { - try testEncoding(value, json) - try testDecoding(value, json) -} - -public struct TestError: Error { - public init(_ message: String) { - self.message = message - } - - public let message: String -} - -extension String { - public var utf8Data: Data { - Data(utf8) - } -} diff --git a/test-codegen/apollo-codegen-config.json b/test-codegen/apollo-codegen-config.json deleted file mode 100644 index 93b648608..000000000 --- a/test-codegen/apollo-codegen-config.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "schemaNamespace" : "SwapiSchema", - "input" : { - "operationSearchPaths" : [ - "**/*.graphql" - ], - "schemaSearchPaths" : [ - "**/*.graphqls" - ] - }, - "output" : { - "testMocks" : { - "none" : { - } - }, - "schemaTypes" : { - "path" : "./SwapiSchema", - "moduleType" : { - "swiftPackage" : { - } - } - }, - "operations" : { - "inSchemaModule" : { - } - }, - "generateTypeValidation": true - } -} \ No newline at end of file diff --git a/test-codegen/schema.graphqls b/test-codegen/schema.graphqls deleted file mode 100644 index 856891962..000000000 --- a/test-codegen/schema.graphqls +++ /dev/null @@ -1,1179 +0,0 @@ -schema { - query: Root -} - -"""A single film.""" -type Film implements Node { - characterConnection(after: String, before: String, first: Int, last: Int): FilmCharactersConnection - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The name of the director of this film.""" - director: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """The episode number of this film.""" - episodeID: Int - - """The ID of an object""" - id: ID! - - """The opening paragraphs at the beginning of this film.""" - openingCrawl: String - planetConnection(after: String, before: String, first: Int, last: Int): FilmPlanetsConnection - - """The name(s) of the producer(s) of this film.""" - producers: [String] - - """The ISO 8601 date format of film release at original creator country.""" - releaseDate: String - speciesConnection(after: String, before: String, first: Int, last: Int): FilmSpeciesConnection - starshipConnection(after: String, before: String, first: Int, last: Int): FilmStarshipsConnection - - """The title of this film.""" - title: String - vehicleConnection(after: String, before: String, first: Int, last: Int): FilmVehiclesConnection -} - -"""A connection to a list of items.""" -type FilmCharactersConnection { - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - characters: [Person] - - """A list of edges.""" - edges: [FilmCharactersEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type FilmCharactersEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""A connection to a list of items.""" -type FilmPlanetsConnection { - """A list of edges.""" - edges: [FilmPlanetsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - planets: [Planet] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type FilmPlanetsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Planet -} - -"""A connection to a list of items.""" -type FilmSpeciesConnection { - """A list of edges.""" - edges: [FilmSpeciesEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - species: [Species] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type FilmSpeciesEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Species -} - -"""A connection to a list of items.""" -type FilmStarshipsConnection { - """A list of edges.""" - edges: [FilmStarshipsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type FilmStarshipsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Starship -} - -"""A connection to a list of items.""" -type FilmVehiclesConnection { - """A list of edges.""" - edges: [FilmVehiclesEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] -} - -"""An edge in a connection.""" -type FilmVehiclesEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Vehicle -} - -"""A connection to a list of items.""" -type FilmsConnection { - """A list of edges.""" - edges: [FilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type FilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""An object with an ID""" -interface Node { - """The id of the object.""" - id: ID! -} - -"""Information about pagination in a connection.""" -type PageInfo { - """When paginating forwards, the cursor to continue.""" - endCursor: String - - """When paginating forwards, are there more items?""" - hasNextPage: Boolean! - - """When paginating backwards, are there more items?""" - hasPreviousPage: Boolean! - - """When paginating backwards, the cursor to continue.""" - startCursor: String -} - -"""A connection to a list of items.""" -type PeopleConnection { - """A list of edges.""" - edges: [PeopleEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - people: [Person] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PeopleEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""An individual person or character within the Star Wars universe.""" -type Person implements Node { - """ - The birth year of the person, using the in-universe standard of BBY or ABY - - Before the Battle of Yavin or After the Battle of Yavin. The Battle of Yavin is - a battle that occurs at the end of Star Wars episode IV: A New Hope. - """ - birthYear: String - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """ - The eye color of this person. Will be "unknown" if not known or "n/a" if the - person does not have an eye. - """ - eyeColor: String - filmConnection(after: String, before: String, first: Int, last: Int): PersonFilmsConnection - - """ - The gender of this person. Either "Male", "Female" or "unknown", - "n/a" if the person does not have a gender. - """ - gender: String - - """ - The hair color of this person. Will be "unknown" if not known or "n/a" if the - person does not have hair. - """ - hairColor: String - - """The height of the person in centimeters.""" - height: Int - - """A planet that this person was born on or inhabits.""" - homeworld: Planet - - """The ID of an object""" - id: ID! - - """The mass of the person in kilograms.""" - mass: Float - - """The name of this person.""" - name: String - - """The skin color of this person.""" - skinColor: String - - """The species that this person belongs to, or null if unknown.""" - species: Species - starshipConnection(after: String, before: String, first: Int, last: Int): PersonStarshipsConnection - vehicleConnection(after: String, before: String, first: Int, last: Int): PersonVehiclesConnection - """Whether this is a good person or a bad one""" - goodOrBad: GoodOrBad - - nestedStringArray: [[[String]]]! - nestedPlanetArray: [[[Planet]]]! -} - -"""A connection to a list of items.""" -type PersonFilmsConnection { - """A list of edges.""" - edges: [PersonFilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PersonFilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""A connection to a list of items.""" -type PersonStarshipsConnection { - """A list of edges.""" - edges: [PersonStarshipsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PersonStarshipsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Starship -} - -"""A connection to a list of items.""" -type PersonVehiclesConnection { - """A list of edges.""" - edges: [PersonVehiclesEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] -} - -"""An edge in a connection.""" -type PersonVehiclesEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Vehicle -} - -""" -A large mass, planet or planetoid in the Star Wars Universe, at the time of -0 ABY. -""" -type Planet implements Node { - """The climates of this planet.""" - climates: [String] - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The diameter of this planet in kilometers.""" - diameter: Int - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - filmConnection(after: String, before: String, first: Int, last: Int): PlanetFilmsConnection - - """ - A number denoting the gravity of this planet, where "1" is normal or 1 standard - G. "2" is twice or 2 standard Gs. "0.5" is half or 0.5 standard Gs. - """ - gravity: String - - """The ID of an object""" - id: ID! - - """The name of this planet.""" - name: String - - """ - The number of standard days it takes for this planet to complete a single orbit - of its local star. - """ - orbitalPeriod: Int - - """The average population of sentient beings inhabiting this planet.""" - population: Float - residentConnection(after: String, before: String, first: Int, last: Int): PlanetResidentsConnection - - """ - The number of standard hours it takes for this planet to complete a single - rotation on its axis. - """ - rotationPeriod: Int - - """ - The percentage of the planet surface that is naturally occuring water or bodies - of water. - """ - surfaceWater: Float - - """The terrains of this planet.""" - terrains: [String] -} - -"""A connection to a list of items.""" -type PlanetFilmsConnection { - """A list of edges.""" - edges: [PlanetFilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PlanetFilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""A connection to a list of items.""" -type PlanetResidentsConnection { - """A list of edges.""" - edges: [PlanetResidentsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - residents: [Person] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PlanetResidentsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""A connection to a list of items.""" -type PlanetsConnection { - """A list of edges.""" - edges: [PlanetsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - planets: [Planet] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type PlanetsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Planet -} - -type Root { - allFilms(after: String, before: String, first: Int, last: Int): FilmsConnection - allPeople(after: String, before: String, first: Int, last: Int): PeopleConnection - allPlanets(after: String, before: String, first: Int, last: Int): PlanetsConnection - allSpecies(after: String, before: String, first: Int, last: Int): SpeciesConnection - allStarships(after: String, before: String, first: Int, last: Int): StarshipsConnection - allVehicles(after: String, before: String, first: Int, last: Int): VehiclesConnection - film(filmID: ID, id: ID): Film - - """Fetches an object given its ID""" - node( - """The ID of an object""" - id: ID! - ): Node - person(id: ID, personID: ID): Person - planet(id: ID, planetID: ID): Planet - species(id: ID, speciesID: ID): Species - starship(id: ID, starshipID: ID): Starship - vehicle(id: ID, vehicleID: ID): Vehicle -} - -"""A type of person or character within the Star Wars Universe.""" -type Species implements Node { - """The average height of this species in centimeters.""" - averageHeight: Float - - """The average lifespan of this species in years, null if unknown.""" - averageLifespan: Int - - """The classification of this species, such as "mammal" or "reptile".""" - classification: String - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The designation of this species, such as "sentient".""" - designation: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - - """ - Common eye colors for this species, null if this species does not typically - have eyes. - """ - eyeColors: [String] - filmConnection(after: String, before: String, first: Int, last: Int): SpeciesFilmsConnection - - """ - Common hair colors for this species, null if this species does not typically - have hair. - """ - hairColors: [String] - - """A planet that this species originates from.""" - homeworld: Planet - - """The ID of an object""" - id: ID! - - """The language commonly spoken by this species.""" - language: String - - """The name of this species.""" - name: String - personConnection(after: String, before: String, first: Int, last: Int): SpeciesPeopleConnection - - """ - Common skin colors for this species, null if this species does not typically - have skin. - """ - skinColors: [String] -} - -"""A connection to a list of items.""" -type SpeciesConnection { - """A list of edges.""" - edges: [SpeciesEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - species: [Species] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type SpeciesEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Species -} - -"""A connection to a list of items.""" -type SpeciesFilmsConnection { - """A list of edges.""" - edges: [SpeciesFilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type SpeciesFilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""A connection to a list of items.""" -type SpeciesPeopleConnection { - """A list of edges.""" - edges: [SpeciesPeopleEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - people: [Person] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type SpeciesPeopleEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""A single transport craft that has hyperdrive capability.""" -type Starship implements Node { - """ - The Maximum number of Megalights this starship can travel in a standard hour. - A "Megalight" is a standard unit of distance and has never been defined before - within the Star Wars universe. This figure is only really useful for measuring - the difference in speed of starships. We can assume it is similar to AU, the - distance between our Sun (Sol) and Earth. - """ - MGLT: Int - - """The maximum number of kilograms that this starship can transport.""" - cargoCapacity: Float - - """ - The maximum length of time that this starship can provide consumables for its - entire crew without having to resupply. - """ - consumables: String - - """The cost of this starship new, in galactic credits.""" - costInCredits: Float - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The number of personnel needed to run or pilot this starship.""" - crew: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - filmConnection(after: String, before: String, first: Int, last: Int): StarshipFilmsConnection - - """The class of this starships hyperdrive.""" - hyperdriveRating: Float - - """The ID of an object""" - id: ID! - - """The length of this starship in meters.""" - length: Float - - """The manufacturers of this starship.""" - manufacturers: [String] - - """ - The maximum speed of this starship in atmosphere. null if this starship is - incapable of atmosphering flight. - """ - maxAtmospheringSpeed: Int - - """ - The model or official name of this starship. Such as "T-65 X-wing" or "DS-1 - Orbital Battle Station". - """ - model: String - - """The name of this starship. The common name, such as "Death Star".""" - name: String - - """The number of non-essential people this starship can transport.""" - passengers: String - pilotConnection(after: String, before: String, first: Int, last: Int): StarshipPilotsConnection - - """ - The class of this starship, such as "Starfighter" or "Deep Space Mobile - Battlestation" - """ - starshipClass: String -} - -"""A connection to a list of items.""" -type StarshipFilmsConnection { - """A list of edges.""" - edges: [StarshipFilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type StarshipFilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""A connection to a list of items.""" -type StarshipPilotsConnection { - """A list of edges.""" - edges: [StarshipPilotsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - pilots: [Person] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type StarshipPilotsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""A connection to a list of items.""" -type StarshipsConnection { - """A list of edges.""" - edges: [StarshipsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - starships: [Starship] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type StarshipsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Starship -} - -"""A single transport craft that does not have hyperdrive capability""" -type Vehicle implements Node { - """The maximum number of kilograms that this vehicle can transport.""" - cargoCapacity: Float - - """ - The maximum length of time that this vehicle can provide consumables for its - entire crew without having to resupply. - """ - consumables: String - - """The cost of this vehicle new, in Galactic Credits.""" - costInCredits: Float - - """The ISO 8601 date format of the time that this resource was created.""" - created: String - - """The number of personnel needed to run or pilot this vehicle.""" - crew: String - - """The ISO 8601 date format of the time that this resource was edited.""" - edited: String - filmConnection(after: String, before: String, first: Int, last: Int): VehicleFilmsConnection - - """The ID of an object""" - id: ID! - - """The length of this vehicle in meters.""" - length: Float - - """The manufacturers of this vehicle.""" - manufacturers: [String] - - """The maximum speed of this vehicle in atmosphere.""" - maxAtmospheringSpeed: Int - - """ - The model or official name of this vehicle. Such as "All-Terrain Attack - Transport". - """ - model: String - - """ - The name of this vehicle. The common name, such as "Sand Crawler" or "Speeder - bike". - """ - name: String - - """The number of non-essential people this vehicle can transport.""" - passengers: String - pilotConnection(after: String, before: String, first: Int, last: Int): VehiclePilotsConnection - - """The class of this vehicle, such as "Wheeled" or "Repulsorcraft".""" - vehicleClass: String -} - -"""A connection to a list of items.""" -type VehicleFilmsConnection { - """A list of edges.""" - edges: [VehicleFilmsEdge] - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - films: [Film] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type VehicleFilmsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Film -} - -"""A connection to a list of items.""" -type VehiclePilotsConnection { - """A list of edges.""" - edges: [VehiclePilotsEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - pilots: [Person] - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int -} - -"""An edge in a connection.""" -type VehiclePilotsEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Person -} - -"""A connection to a list of items.""" -type VehiclesConnection { - """A list of edges.""" - edges: [VehiclesEdge] - - """Information to aid in pagination.""" - pageInfo: PageInfo! - - """ - A count of the total number of objects in this connection, ignoring pagination. - This allows a client to fetch the first five objects by passing "5" as the - argument to "first", then fetch the total count so it could display "5 of 83", - for example. - """ - totalCount: Int - - """ - A list of all of the objects returned in the connection. This is a convenience - field provided for quickly exploring the API; rather than querying for - "{ edges { node } }" when no edge data is needed, this field can be be used - instead. Note that when clients like Relay need to fetch the "cursor" field on - the edge to enable efficient pagination, this shortcut cannot be used, and the - full "{ edges { node } }" version should be used instead. - """ - vehicles: [Vehicle] -} - -"""An edge in a connection.""" -type VehiclesEdge { - """A cursor for use in pagination""" - cursor: String! - - """The item at the end of the edge""" - node: Vehicle -} - -enum GoodOrBad { - GOOD - BAD -} - -union PersonOrPlanet = Person | Planet