diff --git a/Examples/GraphQLDotOrg/Package.resolved b/Examples/GraphQLDotOrg/Package.resolved deleted file mode 100644 index 95d3dd0..0000000 --- a/Examples/GraphQLDotOrg/Package.resolved +++ /dev/null @@ -1,33 +0,0 @@ -{ - "originHash" : "8492586f4b54f299fe741712fead9e927f4203d065c62d5462888cd97675f563", - "pins" : [ - { - "identity" : "graphql", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GraphQLSwift/GraphQL.git", - "state" : { - "revision" : "397c0f43a1eb6a401858f896263288375efcf0bd", - "version" : "4.1.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", - "version" : "1.7.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", - "version" : "1.3.0" - } - } - ], - "version" : 3 -} diff --git a/Examples/GraphQLDotOrg/Package.swift b/Examples/GraphQLDotOrg/Package.swift deleted file mode 100644 index 7257a86..0000000 --- a/Examples/GraphQLDotOrg/Package.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swift-tools-version: 6.0 - -import PackageDescription - -let package = Package( - name: "GraphQLDotOrg", - platforms: [ - .macOS(.v13), - ], - products: [ - .library( - name: "GraphQLDotOrg", - targets: ["GraphQLDotOrg"] - ), - ], - dependencies: [ - .package(name: "graphql-generator", path: "../.."), - .package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.1.0"), - ], - targets: [ - .target( - name: "GraphQLDotOrg", - dependencies: [ - .product(name: "GraphQL", package: "GraphQL"), - .product(name: "GraphQLGeneratorRuntime", package: "graphql-generator"), - ], - plugins: [ - .plugin(name: "GraphQLGeneratorPlugin", package: "graphql-generator"), - ] - ), - ] -) diff --git a/Examples/GraphQLDotOrg/README.md b/Examples/GraphQLDotOrg/README.md deleted file mode 100644 index c2e8d0d..0000000 --- a/Examples/GraphQLDotOrg/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# GraphQL.org - -This is an example using the example schema at https://graphql.org/graphql/ diff --git a/Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/GraphQLDotOrg.swift b/Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/GraphQLDotOrg.swift deleted file mode 100644 index 8aff80d..0000000 --- a/Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/GraphQLDotOrg.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Pacakge must define `GraphQLContext` type -actor GraphQLContext {} - -// The rest of the types should be implemented: - -// struct Resolvers { -// typealias Query = Root -// } -// struct Root { -// ... -// } diff --git a/Examples/GraphQLDotOrg/.gitignore b/Examples/StarWars/.gitignore similarity index 100% rename from Examples/GraphQLDotOrg/.gitignore rename to Examples/StarWars/.gitignore diff --git a/Examples/StarWars/Package.resolved b/Examples/StarWars/Package.resolved new file mode 100644 index 0000000..4bb0770 --- /dev/null +++ b/Examples/StarWars/Package.resolved @@ -0,0 +1,231 @@ +{ + "originHash" : "5baf1697a71440af6f325d83e68fc9fefa98355a82ec82eda703780f1c6337f7", + "pins" : [ + { + "identity" : "async-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/adam-fowler/async-collections", + "state" : { + "revision" : "726af96095a19df6b8053ddbaed0a727aa70ccb2", + "version" : "0.1.0" + } + }, + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client", + "state" : { + "revision" : "5dd84c7bb48b348751d7bbe7ba94a17bafdcef37", + "version" : "1.30.2" + } + }, + { + "identity" : "dataloader", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GraphQLSwift/DataLoader.git", + "state" : { + "revision" : "15ff3272acac747a127e578c8f23b727e1f4fb89", + "version" : "2.3.2" + } + }, + { + "identity" : "graphql", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GraphQLSwift/GraphQL.git", + "state" : { + "revision" : "397c0f43a1eb6a401858f896263288375efcf0bd", + "version" : "4.1.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", + "version" : "1.7.0" + } + }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "810496cf121e525d660cd0ea89a758740476b85f", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "133a347911b6ad0fc8fe3bf46ca90c66cff97130", + "version" : "1.17.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "6f70fa9eab24c1fd982af18c281c4525d05e3095", + "version" : "4.2.0" + } + }, + { + "identity" : "swift-distributed-tracing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-distributed-tracing.git", + "state" : { + "revision" : "baa932c1336f7894145cbaafcd34ce2dd0b77c97", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-http-structured-headers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-structured-headers.git", + "state" : { + "revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b", + "version" : "1.6.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types.git", + "state" : { + "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca", + "version" : "1.5.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "bc386b95f2a16ccd0150a8235e7c69eab2b866ca", + "version" : "1.8.0" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "a1605a3303a28e14d822dec8aaa53da8a9490461", + "version" : "2.92.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "1c90641b02b6ab47c6d0db2063a12198b04e83e2", + "version" : "1.31.2" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "c2ba4cfbb83f307c66f5a6df6bb43e3c88dfbf80", + "version" : "1.39.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "173cc69a058623525a58ae6710e2f5727c663793", + "version" : "2.36.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "60c3e187154421171721c1a38e800b390680fb5d", + "version" : "1.26.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-service-context", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-service-context.git", + "state" : { + "revision" : "1983448fefc717a2bc2ebde5490fe99873c5b8a6", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "1de37290c0ab3c5a96028e0f02911b672fd42348", + "version" : "2.9.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "395a77f0aa927f0ff73941d7ac35f2b46d47c9db", + "version" : "1.6.3" + } + } + ], + "version" : 3 +} diff --git a/Examples/StarWars/Package.swift b/Examples/StarWars/Package.swift new file mode 100644 index 0000000..ca87832 --- /dev/null +++ b/Examples/StarWars/Package.swift @@ -0,0 +1,42 @@ +// swift-tools-version: 6.0 + +import PackageDescription + +let package = Package( + name: "StarWars", + platforms: [ + .macOS(.v13), + ], + products: [ + .library( + name: "StarWars", + targets: ["StarWars"] + ), + ], + dependencies: [ + .package(name: "graphql-generator", path: "../.."), + .package(url: "https://github.com/GraphQLSwift/DataLoader", from: "2.0.0"), + .package(url: "https://github.com/GraphQLSwift/GraphQL", from: "4.1.0"), + .package(url: "https://github.com/swift-server/async-http-client", from: "1.0.0"), + ], + targets: [ + .target( + name: "StarWars", + dependencies: [ + .product(name: "AsyncDataLoader", package: "DataLoader"), + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "GraphQL", package: "GraphQL"), + .product(name: "GraphQLGeneratorRuntime", package: "graphql-generator"), + ], + plugins: [ + .plugin(name: "GraphQLGeneratorPlugin", package: "graphql-generator"), + ] + ), + .testTarget( + name: "StarWarsTests", + dependencies: [ + "StarWars", + ] + ), + ] +) diff --git a/Examples/StarWars/README.md b/Examples/StarWars/README.md new file mode 100644 index 0000000..7c970be --- /dev/null +++ b/Examples/StarWars/README.md @@ -0,0 +1,15 @@ +# Star Wars + +This is a `graphql-generator` example using the schema at https://graphql.org/graphql/, which wraps the [Star Wars API](https://www.swapi.tech) (SWAPI). For a Javascript implementation, see https://github.com/graphql/swapi-graphql + +It uses [async-http-client](https://github.com/swift-server/async-http-client) to make the web requests and a [DataLoader](https://github.com/GraphQLSwift/DataLoader) to cache SWAPI responses. + +## Getting Started + +To get started, simply open this directory and build the project: + +```swift +swift build +``` + +Running the tests will print some example query responses to the console. diff --git a/Examples/StarWars/Sources/StarWars/GraphQL/Relay.swift b/Examples/StarWars/Sources/StarWars/GraphQL/Relay.swift new file mode 100644 index 0000000..c3b8b15 --- /dev/null +++ b/Examples/StarWars/Sources/StarWars/GraphQL/Relay.swift @@ -0,0 +1,422 @@ +import Foundation +import GraphQL + +/// Encodes a SWAPI type and ID into a Relay Node ID. This is the base64-encoded string `:` +/// - Parameters: +/// - type: The SWAPI type of the resource +/// - id: The SWAPI id of the resource +/// - Returns: The Relay ID of the resouce +func encodeID(type: SwapiResource.Type, id: any StringProtocol) -> String { + let idData = "\(type.type.rawValue):\(id)".data(using: .utf8)! + return idData.base64EncodedString() +} + +/// Decodes a Relay ID into the SWAPI type and ID +/// - Parameter string: The Relay ID of the resouce +/// - Returns: A tuple containing the SWAPI type and ID of the resource +func decodeID(_ string: String) -> (type: SwapiResourceType, id: String) { + let typeAndId = String(data: Data(base64Encoded: string)!, encoding: .utf8)! + let split = typeAndId.split(separator: ":") + return (SwapiResourceType(rawValue: String(split.first!))!, String(split.last!)) +} + +/// Given a SWAPI resource url, extract the resource ID +func urlToID(_ url: String) -> String { + return String(url.split(separator: "/").last!) +} + +struct PageInfo: GraphQLGenerated.PageInfo { + let hasNextPage: Bool + let hasPreviousPage: Bool + let startCursor: String? + let endCursor: String? + + func hasNextPage(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Bool { + return hasNextPage + } + + func hasPreviousPage(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Bool { + return hasPreviousPage + } + + func startCursor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return startCursor + } + + func endCursor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return endCursor + } +} + +/// A generalized Relay connection. +struct Connection: Sendable { + let pageInfo: PageInfo + let edges: [Edge] + let totalCount: Int + + /// Create a connection by passing a total list of the available IDs in order. + init(ids: [String], after: String?, first: Int?, before: String?, last: Int?) { + guard !ids.isEmpty else { + pageInfo = PageInfo( + hasNextPage: false, + hasPreviousPage: false, + startCursor: nil, + endCursor: nil + ) + edges = [] + totalCount = 0 + return + } + + var startIndex = 0 + var endIndex = ids.count - 1 + if let after { + ids.firstIndex { after < $0 }.map { startIndex = $0 } + } + if let before { + ids.lastIndex { $0 < before }.map { endIndex = $0 } + } + if let first { + endIndex = min(startIndex + first, endIndex) + } + if let last { + startIndex = max(endIndex - last, startIndex) + } + let pageIds = ids[startIndex ... endIndex] + + pageInfo = PageInfo( + hasNextPage: endIndex < ids.count - 1, + hasPreviousPage: startIndex > 0, + startCursor: ids[startIndex], + endCursor: ids[endIndex] + ) + edges = pageIds.map { Edge(cursor: $0) } + totalCount = ids.count + } + + func pageInfo(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> any GraphQLGenerated.PageInfo { + return pageInfo + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [Edge]? { + return edges + } + + func totalCount(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return totalCount + } +} + +extension Connection: + GraphQLGenerated.FilmsConnection, + GraphQLGenerated.PersonFilmsConnection, + GraphQLGenerated.PlanetFilmsConnection, + GraphQLGenerated.SpeciesFilmsConnection, + GraphQLGenerated.StarshipFilmsConnection, + GraphQLGenerated.VehicleFilmsConnection + where T: GraphQLGenerated.Film +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PersonFilmsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PlanetFilmsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.SpeciesFilmsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.StarshipFilmsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.VehicleFilmsEdge]? { + return edges + } + + func films(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Film]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Film]? { + var nodes = [GraphQLGenerated.Film]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +extension Connection: + GraphQLGenerated.PeopleConnection, + GraphQLGenerated.FilmCharactersConnection, + GraphQLGenerated.PlanetResidentsConnection, + GraphQLGenerated.SpeciesPeopleConnection, + GraphQLGenerated.StarshipPilotsConnection, + GraphQLGenerated.VehiclePilotsConnection + where T: GraphQLGenerated.Person +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PeopleEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmCharactersEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PlanetResidentsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.SpeciesPeopleEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.StarshipPilotsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.VehiclePilotsEdge]? { + return edges + } + + func people(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Person]? { + return try await nodes(context: context, info: info) + } + + func characters(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Person]? { + return try await nodes(context: context, info: info) + } + + func residents(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Person]? { + return try await nodes(context: context, info: info) + } + + func pilots(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Person]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Person]? { + var nodes = [GraphQLGenerated.Person]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +extension Connection: + GraphQLGenerated.PlanetsConnection, + GraphQLGenerated.FilmPlanetsConnection + where T: GraphQLGenerated.Planet +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PlanetsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmPlanetsEdge]? { + return edges + } + + func planets(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Planet]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Planet]? { + var nodes = [GraphQLGenerated.Planet]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +extension Connection: + GraphQLGenerated.SpeciesConnection, + GraphQLGenerated.FilmSpeciesConnection + where T: GraphQLGenerated.Species +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.SpeciesEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmSpeciesEdge]? { + return edges + } + + func species(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Species]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Species]? { + var nodes = [GraphQLGenerated.Species]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +extension Connection: + GraphQLGenerated.StarshipsConnection, + GraphQLGenerated.FilmStarshipsConnection, + GraphQLGenerated.PersonStarshipsConnection + where T: GraphQLGenerated.Starship +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.StarshipsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmStarshipsEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PersonStarshipsEdge]? { + return edges + } + + func starships(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Starship]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Starship]? { + var nodes = [GraphQLGenerated.Starship]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +extension Connection: + GraphQLGenerated.VehiclesConnection, + GraphQLGenerated.FilmVehiclesConnection, + GraphQLGenerated.PersonVehiclesConnection + where T: GraphQLGenerated.Vehicle +{ + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.VehiclesEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.FilmVehiclesEdge]? { + return edges + } + + func edges(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.PersonVehiclesEdge]? { + return edges + } + + func vehicles(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Vehicle]? { + return try await nodes(context: context, info: info) + } + + private func nodes(context: GraphQLContext, info: GraphQL.GraphQLResolveInfo) async throws -> [any GraphQLGenerated.Vehicle]? { + var nodes = [GraphQLGenerated.Vehicle]() + for edge in edges { + if let node = try await edge.node(context: context, info: info) { + nodes.append(node) + } + } + return nodes + } +} + +struct Edge: Sendable { + let cursor: String + + func cursor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return cursor + } +} + +extension Edge: + GraphQLGenerated.FilmsEdge, + GraphQLGenerated.PersonFilmsEdge, + GraphQLGenerated.PlanetFilmsEdge, + GraphQLGenerated.SpeciesFilmsEdge, + GraphQLGenerated.StarshipFilmsEdge, + GraphQLGenerated.VehicleFilmsEdge + where T: GraphQLGenerated.Film +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Film)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Film.self, id: id) + } +} + +extension Edge: + GraphQLGenerated.PeopleEdge, + GraphQLGenerated.FilmCharactersEdge, + GraphQLGenerated.PlanetResidentsEdge, + GraphQLGenerated.SpeciesPeopleEdge, + GraphQLGenerated.StarshipPilotsEdge, + GraphQLGenerated.VehiclePilotsEdge + where T: GraphQLGenerated.Person +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Person)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Person.self, id: id) + } +} + +extension Edge: + GraphQLGenerated.PlanetsEdge, + GraphQLGenerated.FilmPlanetsEdge + where T: GraphQLGenerated.Planet +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Planet)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Planet.self, id: id) + } +} + +extension Edge: + GraphQLGenerated.SpeciesEdge, + GraphQLGenerated.FilmSpeciesEdge + where T: GraphQLGenerated.Species +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Species)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Species.self, id: id) + } +} + +extension Edge: + GraphQLGenerated.StarshipsEdge, + GraphQLGenerated.FilmStarshipsEdge, + GraphQLGenerated.PersonStarshipsEdge + where T: GraphQLGenerated.Starship +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Starship)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Starship.self, id: id) + } +} + +extension Edge: + GraphQLGenerated.VehiclesEdge, + GraphQLGenerated.FilmVehiclesEdge, + GraphQLGenerated.PersonVehiclesEdge + where T: GraphQLGenerated.Vehicle +{ + func node(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Vehicle)? { + let (_, id) = decodeID(cursor) + return try await context.client.get(type: Vehicle.self, id: id) + } +} diff --git a/Examples/StarWars/Sources/StarWars/GraphQL/Resolvers.swift b/Examples/StarWars/Sources/StarWars/GraphQL/Resolvers.swift new file mode 100644 index 0000000..a714bc5 --- /dev/null +++ b/Examples/StarWars/Sources/StarWars/GraphQL/Resolvers.swift @@ -0,0 +1,557 @@ +import AsyncHTTPClient +import Foundation +import GraphQL +import GraphQLGeneratorRuntime + +struct GraphQLContext { + /// A client for fetching SWAPI data. Since the data is not expected to change, this client may be shared + /// across context instances, which are typically created fresh for every GraphQL query. + let client: SwapiClient +} + +struct Resolvers: GraphQLGenerated.Resolvers { + typealias Query = Root +} + +struct Root: GraphQLGenerated.Root { + static func allFilms(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmsConnection)? { + let allIDs = try await context.client.getAllIDs(type: Film.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func film(id: String?, filmID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Film)? { + if let filmID { + return try await context.client.get(type: Film.self, id: filmID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Film.self, id: itemID) + } else { + return nil + } + } + + static func allPeople(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PeopleConnection)? { + let allIDs = try await context.client.getAllIDs(type: Person.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func person(id: String?, personID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Person)? { + if let personID { + return try await context.client.get(type: Person.self, id: personID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Person.self, id: itemID) + } else { + return nil + } + } + + static func allPlanets(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PlanetsConnection)? { + let allIDs = try await context.client.getAllIDs(type: Planet.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func planet(id: String?, planetID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Planet)? { + if let planetID { + return try await context.client.get(type: Planet.self, id: planetID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Planet.self, id: itemID) + } else { + return nil + } + } + + static func allSpecies(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.SpeciesConnection)? { + let allIDs = try await context.client.getAllIDs(type: Species.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func species(id: String?, speciesID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Species)? { + if let speciesID { + return try await context.client.get(type: Species.self, id: speciesID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Species.self, id: itemID) + } else { + return nil + } + } + + static func allStarships(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.StarshipsConnection)? { + let allIDs = try await context.client.getAllIDs(type: Starship.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func starship(id: String?, starshipID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Starship)? { + if let starshipID { + return try await context.client.get(type: Starship.self, id: starshipID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Starship.self, id: itemID) + } else { + return nil + } + } + + static func allVehicles(after: String?, first: Int?, before: String?, last: Int?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.VehiclesConnection)? { + let allIDs = try await context.client.getAllIDs(type: Vehicle.self) + return Connection(ids: allIDs, after: after, first: first, before: before, last: last) + } + + static func vehicle(id: String?, vehicleID: String?, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Vehicle)? { + if let vehicleID { + return try await context.client.get(type: Vehicle.self, id: vehicleID) + } else if let id { + let (_, itemID) = decodeID(id) + return try await context.client.get(type: Vehicle.self, id: itemID) + } else { + return nil + } + } + + static func node(id: String, context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Node)? { + let (type, itemID) = decodeID(id) + switch type { + case .films: + return try await context.client.get(type: Film.self, id: itemID) + case .people: + return try await context.client.get(type: Person.self, id: itemID) + case .planets: + return try await context.client.get(type: Planet.self, id: itemID) + case .species: + return try await context.client.get(type: Species.self, id: itemID) + case .starships: + return try await context.client.get(type: Starship.self, id: itemID) + case .vehicles: + return try await context.client.get(type: Vehicle.self, id: itemID) + } + } +} + +extension Film: GraphQLGenerated.Film { + var id: String { + return encodeID(type: Film.self, id: urlToID(url)) + } + + func title(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return title + } + + func episodeID(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return episode_id + } + + func openingCrawl(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return opening_crawl + } + + func director(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return director + } + + func producers(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return producer.split(separator: ",").map { String($0) } + } + + func releaseDate(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return release_date + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } + + func speciesConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmSpeciesConnection)? { + let filteredIDs = species.map { encodeID(type: Film.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func starshipConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmStarshipsConnection)? { + let filteredIDs = starships.map { encodeID(type: Starship.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func vehicleConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmVehiclesConnection)? { + let filteredIDs = vehicles.map { encodeID(type: Vehicle.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func characterConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmCharactersConnection)? { + let filteredIDs = characters.map { encodeID(type: Person.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func planetConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.FilmPlanetsConnection)? { + let filteredIDs = planets.map { encodeID(type: Planet.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } +} + +extension Person: GraphQLGenerated.Person { + var id: String { + return encodeID(type: Person.self, id: urlToID(url)) + } + + func name(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return name + } + + func birthYear(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return birth_year + } + + func eyeColor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return eye_color + } + + func gender(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return gender + } + + func hairColor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return hair_color + } + + func height(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(height) + } + + func mass(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(mass) + } + + func skinColor(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return skin_color + } + + func homeworld(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Planet)? { + try await context.client.get(type: Planet.self, id: urlToID(homeworld)) + } + + func filmConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PersonFilmsConnection)? { + let filteredIDs = films.map { encodeID(type: Film.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func species(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Species)? { + guard let firstSpecies = species?.first else { + return nil + } + return try await context.client.get(type: Species.self, id: urlToID(firstSpecies)) + } + + func starshipConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PersonStarshipsConnection)? { + let filteredIDs = starships.map { encodeID(type: Starship.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func vehicleConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PersonVehiclesConnection)? { + let filteredIDs = vehicles.map { encodeID(type: Vehicle.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } +} + +extension Planet: GraphQLGenerated.Planet { + var id: String { + return encodeID(type: Planet.self, id: urlToID(url)) + } + + func name(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return name + } + + func diameter(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(diameter) + } + + func rotationPeriod(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(rotation_period) + } + + func orbitalPeriod(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(orbital_period) + } + + func gravity(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return gravity + } + + func population(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(population) + } + + func climates(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return climate.split(separator: ",").map { String($0) } + } + + func terrains(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return terrain.split(separator: ",").map { String($0) } + } + + func surfaceWater(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(surface_water) + } + + func residentConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PlanetResidentsConnection)? { + let filteredIDs = residents?.map { encodeID(type: Person.self, id: urlToID($0)) } ?? [] + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func filmConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.PlanetFilmsConnection)? { + let filteredIDs = films?.map { encodeID(type: Film.self, id: urlToID($0)) } ?? [] + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } +} + +extension Species: GraphQLGenerated.Species { + var id: String { + return encodeID(type: Species.self, id: urlToID(url)) + } + + func name(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return name + } + + func classification(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return classification + } + + func designation(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return designation + } + + func averageHeight(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(average_height) + } + + func averageLifespan(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(average_lifespan) + } + + func eyeColors(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return eye_colors.split(separator: ",").map { String($0) } + } + + func hairColors(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return hair_colors.split(separator: ",").map { String($0) } + } + + func skinColors(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return skin_colors.split(separator: ",").map { String($0) } + } + + func language(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return language + } + + func homeworld(context: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.Planet)? { + return try await context.client.get(type: Planet.self, id: urlToID(homeworld)) + } + + func personConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.SpeciesPeopleConnection)? { + let filteredIDs = people.map { encodeID(type: Person.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func filmConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.SpeciesFilmsConnection)? { + let filteredIDs = films?.map { encodeID(type: Film.self, id: urlToID($0)) } ?? [] + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } +} + +extension Starship: GraphQLGenerated.Starship { + var id: String { + return encodeID(type: Starship.self, id: urlToID(url)) + } + + func name(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return name + } + + func model(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return model + } + + func starshipClass(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return starship_class + } + + func manufacturers(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return manufacturer.split(separator: ",").map { String($0) } + } + + func costInCredits(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(cost_in_credits) + } + + func length(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(length) + } + + func crew(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return crew + } + + func passengers(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return passengers + } + + func maxAtmospheringSpeed(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(max_atmosphering_speed) + } + + func hyperdriveRating(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(hyperdrive_rating) + } + + func mglt(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(MGLT) + } + + func cargoCapacity(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(cargo_capacity) + } + + func consumables(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return consumables + } + + func pilotConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.StarshipPilotsConnection)? { + let filteredIDs = pilots.map { encodeID(type: Person.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func filmConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.StarshipFilmsConnection)? { + let filteredIDs = films.map { encodeID(type: Film.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } +} + +extension Vehicle: GraphQLGenerated.Vehicle { + var id: String { + return encodeID(type: Vehicle.self, id: urlToID(url)) + } + + func name(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return name + } + + func model(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return model + } + + func vehicleClass(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return vehicle_class + } + + func manufacturers(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> [String]? { + return manufacturer.split(separator: ",").map { String($0) } + } + + func costInCredits(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(cost_in_credits) + } + + func length(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(length) + } + + func crew(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return crew + } + + func passengers(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return passengers + } + + func maxAtmospheringSpeed(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Int? { + return Int(max_atmosphering_speed) + } + + func cargoCapacity(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> Double? { + return Double(cargo_capacity) + } + + func consumables(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return consumables + } + + func pilotConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.VehiclePilotsConnection)? { + let filteredIDs = pilots.map { encodeID(type: Person.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func filmConnection(after: String?, first: Int?, before: String?, last: Int?, context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> (any GraphQLGenerated.VehicleFilmsConnection)? { + let filteredIDs = films.map { encodeID(type: Film.self, id: urlToID($0)) } + return Connection(ids: filteredIDs, after: after, first: first, before: before, last: last) + } + + func created(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return created + } + + func edited(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String? { + return edited + } + + func id(context _: GraphQLContext, info _: GraphQL.GraphQLResolveInfo) async throws -> String { + return id + } +} diff --git a/Examples/StarWars/Sources/StarWars/SWAPI/SwapiClient.swift b/Examples/StarWars/Sources/StarWars/SWAPI/SwapiClient.swift new file mode 100644 index 0000000..f201163 --- /dev/null +++ b/Examples/StarWars/Sources/StarWars/SWAPI/SwapiClient.swift @@ -0,0 +1,88 @@ +import AsyncDataLoader +import AsyncHTTPClient +import Foundation +import GraphQL + +/// A caching client for accessing the public SWAPI. +/// +/// See https://www.swapi.tech/documentation +struct SwapiClient { + let client: HTTPClient + let decoder = JSONDecoder() + private let rootUrl = "https://swapi.tech/api/" + + /// A dataloader that caches each response from the API. We can use this cross-client + /// cross-request because we don't expect the data to change. + private let dataLoader: DataLoader + + init(client: HTTPClient) { + self.client = client + dataLoader = DataLoader { urls in + // Retrieve the URLs in parallel + await withTaskGroup { group in + var results: [DataLoaderValue] = urls.map { _ in + .failure(GraphQLError(message: "Index must be populated")) + } + for (index, url) in urls.enumerated() { + group.addTask { + let result: Result + do { + let response = try await client.get(url: url).get() + result = .success(response) + } catch { + result = .failure(error) + } + return (index, result) + } + } + for await result in group { + results[result.0] = switch result.1 { + case let .success(response): + .success(response) + case let .failure(error): + .failure(error) + } + } + return results + } + } + } + + func get(type: T.Type, id: String) async throws -> T? { + let instance = try await get(url: "\(rootUrl)/\(type.type.rawValue)/\(id)", as: InstanceResponse.self) + return instance?.result.properties + } + + func getAllIDs(type: T.Type) async throws -> [String] { + var nextUrl: String? = "\(rootUrl)/\(type.type.rawValue)" + var ids = [String]() + while let url = nextUrl { + guard let page = try await get(url: url, as: PageResponse.self) else { + break + } + for result in page.results { + ids.append(result.uid) + } + nextUrl = page.next + } + return ids.map { + encodeID(type: T.self, id: $0) + }.sorted() + } + + private func get(url: String, as _: T.Type) async throws -> T? { + let response = try await dataLoader.load(key: url) + switch response.status { + case .ok: + break + case .notFound: + return nil + default: + throw GraphQLError(message: "Failed with HTTP status \(response.status) at \(url)") + } + guard let body = response.body else { + throw GraphQLError(message: "No body found from response at \(url)") + } + return try JSONDecoder().decode(T.self, from: body) + } +} diff --git a/Examples/StarWars/Sources/StarWars/SWAPI/SwapiTypes.swift b/Examples/StarWars/Sources/StarWars/SWAPI/SwapiTypes.swift new file mode 100644 index 0000000..931f368 --- /dev/null +++ b/Examples/StarWars/Sources/StarWars/SWAPI/SwapiTypes.swift @@ -0,0 +1,168 @@ +/// See https://www.swapi.tech/documentation +enum SwapiResourceType: String, RawRepresentable { + case films + case people + case planets + case species + case starships + case vehicles +} + +/// See https://www.swapi.tech/documentation +protocol SwapiResource: Codable, Sendable { + /// Represents the sub-path used to access the resource + static var type: SwapiResourceType { get } + + /// The URL that references this object + var url: String { get } +} + +/// See https://www.swapi.tech/documentation#films +struct Film: SwapiResource { + static let type = SwapiResourceType.films + + let characters: [String] + let created: String + let director: String + let edited: String + let episode_id: Int + let opening_crawl: String + let planets: [String] + let producer: String + let release_date: String + let species: [String] + let starships: [String] + let title: String + let url: String + let vehicles: [String] +} + +/// See https://www.swapi.tech/documentation#people +struct Person: SwapiResource { + static let type = SwapiResourceType.people + + let birth_year: String + let eye_color: String + let films: [String] + let gender: String + let hair_color: String + let height: String + let homeworld: String + let mass: String + let name: String + let skin_color: String + let created: String + let edited: String + let species: [String]? + let starships: [String] + let url: String + let vehicles: [String] +} + +/// See https://www.swapi.tech/documentation#planets +struct Planet: SwapiResource { + static let type = SwapiResourceType.planets + + let climate: String + let created: String + let diameter: String + let edited: String + let films: [String]? + let gravity: String + let name: String + let orbital_period: String + let population: String + let residents: [String]? + let rotation_period: String + let surface_water: String + let terrain: String + let url: String +} + +/// See https://www.swapi.tech/documentation#species +struct Species: SwapiResource { + static let type = SwapiResourceType.species + + let average_height: String + let average_lifespan: String + let classification: String + let created: String + let designation: String + let edited: String + let eye_colors: String + let hair_colors: String + let homeworld: String + let language: String + let name: String + let people: [String] + let films: [String]? + let skin_colors: String + let url: String +} + +/// See https://www.swapi.tech/documentation#starships +struct Starship: SwapiResource { + static let type = SwapiResourceType.starships + + let MGLT: String + let cargo_capacity: String + let consumables: String + let cost_in_credits: String + let created: String + let crew: String + let edited: String + let hyperdrive_rating: String + let length: String + let manufacturer: String + let max_atmosphering_speed: String + let model: String + let name: String + let passengers: String + let films: [String] + let pilots: [String] + let starship_class: String + let url: String +} + +/// See https://www.swapi.tech/documentation#vehicles +struct Vehicle: SwapiResource { + static let type = SwapiResourceType.vehicles + + let cargo_capacity: String + let consumables: String + let cost_in_credits: String + let created: String + let crew: String + let edited: String + let length: String + let manufacturer: String + let max_atmosphering_speed: String + let model: String + let name: String + let passengers: String + let pilots: [String] + let films: [String] + let url: String + let vehicle_class: String +} + +/// A response returned from 'get all the X resources' queries +struct PageResponse: Codable { + /// The URL of the next page + let next: String? + let results: [PageResult] + + struct PageResult: Codable { + /// The ID of the resource + let uid: String + } +} + +/// A response returned from 'get a specific X resource' queries +struct InstanceResponse: Codable { + let result: InstanceResult + + struct InstanceResult: Codable { + let properties: U + } +} diff --git a/Examples/StarWars/Sources/StarWars/Schema.swift b/Examples/StarWars/Sources/StarWars/Schema.swift new file mode 100644 index 0000000..cd35fa9 --- /dev/null +++ b/Examples/StarWars/Sources/StarWars/Schema.swift @@ -0,0 +1,5 @@ +import GraphQL + +public func swapiSchema() throws -> GraphQLSchema { + return try buildGraphQLSchema(resolvers: Resolvers.self) +} diff --git a/Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/graphql.graphql b/Examples/StarWars/Sources/StarWars/graphql.graphql similarity index 99% rename from Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/graphql.graphql rename to Examples/StarWars/Sources/StarWars/graphql.graphql index 119a1b9..75db70a 100644 --- a/Examples/GraphQLDotOrg/Sources/GraphQLDotOrg/graphql.graphql +++ b/Examples/StarWars/Sources/StarWars/graphql.graphql @@ -1,3 +1,5 @@ +# Source: https://graphql.org/graphql + schema { query: Root } @@ -1164,4 +1166,3 @@ type VehiclesEdge { """A cursor for use in pagination""" cursor: String! } - diff --git a/Examples/StarWars/Tests/StarWarsTests/StarWarsTests.swift b/Examples/StarWars/Tests/StarWarsTests/StarWarsTests.swift new file mode 100644 index 0000000..1f8b49c --- /dev/null +++ b/Examples/StarWars/Tests/StarWarsTests/StarWarsTests.swift @@ -0,0 +1,81 @@ +import GraphQL +@testable import StarWars +import Testing + +@Suite("display name") +struct StarWarsTests { + @Test func film() async throws { + let client = SwapiClient(client: .shared) + + let schema = try buildGraphQLSchema(resolvers: Resolvers.self) + let context = GraphQLContext(client: client) + try await print( + graphql( + schema: schema, + request: """ + { + film(filmID: 1) { + title + planetConnection(first: 2) { + totalCount + edges { + node { + id + name + diameter + } + } + } + vehicleConnection(first: 2) { + totalCount + edges { + node { + id + name + costInCredits + } + } + } + } + } + """, + context: context + ) + ) + } + + @Test func allPeople() async throws { + let client = SwapiClient(client: .shared) + + let schema = try buildGraphQLSchema(resolvers: Resolvers.self) + let context = GraphQLContext(client: client) + try await print( + graphql( + schema: schema, + request: """ + { + allPeople(first: 3) { + totalCount + edges { + node { + id + name + starshipConnection { + totalCount + edges { + node { + id + name + } + } + } + } + } + } + } + """, + context: context + ) + ) + } +} diff --git a/README.md b/README.md index 1deaee4..a14daca 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ # GraphQL Generator for Swift -A Swift package plugin that generates server-side GraphQL API code from GraphQL schema files, inspired by [GraphQL Tools' makeExecutableSchema](https://the-guild.dev/graphql/tools/docs/generate-schema) and [Swift's OpenAPI Generator](https://github.com/apple/swift-openapi-generator). +This is a Swift package plugin that generates server-side GraphQL API code from GraphQL schema files, inspired by [GraphQL Tools' makeExecutableSchema](https://the-guild.dev/graphql/tools/docs/generate-schema) and [Swift's OpenAPI Generator](https://github.com/apple/swift-openapi-generator). ## Features -- **Data-driven**: Guarantee conformance with declared GraphQL spec +- **Data-driven**: Guarantee conformance with the declared GraphQL spec - **Build-time code generation**: Code is generated at build time and doesn't need to be committed - **Type-safe**: Leverages Swift's type system for compile-time safety - **Minimal boilerplate**: Generates all GraphQL definition code - you write the business logic @@ -37,6 +37,8 @@ targets: [ ## Quick Start +*Protip*: Take a look at the projects in the `Examples` directory to see real, fully featured examples. + ### 1. Create a GraphQL Schema Create a `.graphql` file in your target's `Sources` directory: @@ -55,7 +57,7 @@ type Query { ### 2. Build Your Project -When you build, the plugin will automatically generate Swift code that you can view in the `.build/plugins/outputs` directory: +When you build, the plugin will automatically generate Swift code. If you want, you can view it in the `.build/plugins/outputs` directory: - `BuildGraphQLSchema.swift` - Defines `buildGraphQLSchema` function that builds an executable schema. - `GraphQLRawSDL.swift` - The `graphQLRawSDL` global property, which is a Swift string literal of the input schema. This is internally used at runtime to parse the schema. - `GraphQLTypes.swift` - Swift protocols and types for your GraphQL types. These are all namespaced within `GraphQLGenerated`. @@ -72,7 +74,7 @@ actor GraphQLContext { If your schema has any custom scalar types, you must create them manually in the `GraphQLScalars` namespace. See the `Scalars` usage section below for details. -Create a resolvers struct with the required typealiases: +Create a struct that conforms to `GraphQLGenerated.Resolvers` by defining the required typealiases: ```swift struct Resolvers: GraphQLGenerated.Resolvers { typealias Query = ExamplePackage.Query @@ -108,8 +110,12 @@ struct User: GraphQLGenerated.User { } ``` +Let the protocol conformance guide you on what resolver methods your types must define, and keep going until everything compiles. + ### 4. Execute GraphQL Queries +You're done! You can now instantiate your GraphQL schema by calling `buildGraphQLSchema`, and run queries against it: + ```swift import GraphQL @@ -117,7 +123,7 @@ import GraphQL let schema = try buildGraphQLSchema(resolvers: Resolvers.self) // Execute a query against it -let result = try await graphql(schema: schema, request: "{ users { name email } }") +let result = try await graphql(schema: schema, request: "{ users { name email } }", context: GraphQLContext()) print(result) ``` @@ -138,7 +144,7 @@ type A { } ``` -This would result in the following protocol: +This would result in the following generated protocol: ```swift protocol A: Sendable { func foo(context: GraphQLContext, info: GraphQLResolveInfo) async throws -> String diff --git a/Sources/GraphQLGenerator/main.swift b/Sources/GraphQLGenerator/GraphQLGeneratorCommand.swift similarity index 100% rename from Sources/GraphQLGenerator/main.swift rename to Sources/GraphQLGenerator/GraphQLGeneratorCommand.swift diff --git a/Sources/GraphQLGeneratorCore/Generator/BuildGraphQLSchemaGenerator.swift b/Sources/GraphQLGeneratorCore/Generator/BuildGraphQLSchemaGenerator.swift index 833076a..9a7d60c 100644 --- a/Sources/GraphQLGeneratorCore/Generator/BuildGraphQLSchemaGenerator.swift +++ b/Sources/GraphQLGeneratorCore/Generator/BuildGraphQLSchemaGenerator.swift @@ -184,16 +184,17 @@ package struct BuildGraphQLSchemaGenerator { ) throws -> String { var output = "" + // TODO: Swift 6.0 requires `@Sendable` explicitly here. We can remove it when we drop 6.0 support. if target == .subscription { output += """ - \(variableName)["\(fieldName)"]?.resolve = { source, _, _, _ in + \(variableName)["\(fieldName)"]?.resolve = { @Sendable source, _, _, _ in return source } - \(variableName)["\(fieldName)"]?.subscribe = { source, args, context, info in + \(variableName)["\(fieldName)"]?.subscribe = { @Sendable source, args, context, info in """ } else { output += """ - \(variableName)["\(fieldName)"]?.resolve = { source, args, context, info in + \(variableName)["\(fieldName)"]?.resolve = { @Sendable source, args, context, info in """ } diff --git a/Tests/GraphQLGeneratorCoreTests/SchemaGeneratorTests.swift b/Tests/GraphQLGeneratorCoreTests/SchemaGeneratorTests.swift index cae01f5..0316c31 100644 --- a/Tests/GraphQLGeneratorCoreTests/SchemaGeneratorTests.swift +++ b/Tests/GraphQLGeneratorCoreTests/SchemaGeneratorTests.swift @@ -54,7 +54,7 @@ struct SchemaGeneratorTests { let bar = schema.typeMap["Bar"] as? GraphQLObjectType let barFields = try bar?.fields() ?? [:] - barFields["foo"]?.resolve = { source, args, context, info in + barFields["foo"]?.resolve = { @Sendable source, args, context, info in let parent = try cast(source, to: (any GraphQLGenerated.Bar).self) let context = try cast(context, to: GraphQLContext.self) return try await parent.foo(context: context, info: info) @@ -65,11 +65,11 @@ struct SchemaGeneratorTests { let query = schema.typeMap["Query"] as? GraphQLObjectType let queryFields = try query?.fields() ?? [:] - queryFields["foo"]?.resolve = { source, args, context, info in + queryFields["foo"]?.resolve = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Query.foo(context: context, info: info) } - queryFields["bar"]?.resolve = { source, args, context, info in + queryFields["bar"]?.resolve = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Query.bar(context: context, info: info) } @@ -103,7 +103,7 @@ struct SchemaGeneratorTests { let node = schema.typeMap["Node"] as? GraphQLObjectType let nodeFields = try node?.fields() ?? [:] - nodeFields["id"]?.resolve = { source, args, context, info in + nodeFields["id"]?.resolve = { @Sendable source, args, context, info in let parent = try cast(source, to: (any GraphQLGenerated.Node).self) let context = try cast(context, to: GraphQLContext.self) return try await parent.id(context: context, info: info) @@ -136,12 +136,12 @@ struct SchemaGeneratorTests { let book = schema.typeMap["Book"] as? GraphQLObjectType let bookFields = try book?.fields() ?? [:] - bookFields["title"]?.resolve = { source, args, context, info in + bookFields["title"]?.resolve = { @Sendable source, args, context, info in let parent = try cast(source, to: (any GraphQLGenerated.Book).self) let context = try cast(context, to: GraphQLContext.self) return try await parent.title(context: context, info: info) } - bookFields["author"]?.resolve = { source, args, context, info in + bookFields["author"]?.resolve = { @Sendable source, args, context, info in let parent = try cast(source, to: (any GraphQLGenerated.Book).self) let context = try cast(context, to: GraphQLContext.self) return try await parent.author(context: context, info: info) @@ -172,7 +172,7 @@ struct SchemaGeneratorTests { let query = schema.typeMap["Query"] as? GraphQLObjectType let queryFields = try query?.fields() ?? [:] - queryFields["user"]?.resolve = { source, args, context, info in + queryFields["user"]?.resolve = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Query.user(context: context, info: info) } @@ -201,7 +201,7 @@ struct SchemaGeneratorTests { let mutation = schema.typeMap["Mutation"] as? GraphQLObjectType let mutationFields = try mutation?.fields() ?? [:] - mutationFields["createUser"]?.resolve = { source, args, context, info in + mutationFields["createUser"]?.resolve = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Mutation.createUser(context: context, info: info) } @@ -230,10 +230,10 @@ struct SchemaGeneratorTests { let subscription = schema.typeMap["Subscription"] as? GraphQLObjectType let subscriptionFields = try subscription?.fields() ?? [:] - subscriptionFields["userUpdated"]?.resolve = { source, _, _, _ in + subscriptionFields["userUpdated"]?.resolve = { @Sendable source, _, _, _ in return source } - subscriptionFields["userUpdated"]?.subscribe = { source, args, context, info in + subscriptionFields["userUpdated"]?.subscribe = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Subscription.userUpdated(context: context, info: info) } @@ -268,7 +268,7 @@ struct SchemaGeneratorTests { ) let expected = """ - queryFields["posts"]?.resolve = { source, args, context, info in + queryFields["posts"]?.resolve = { @Sendable source, args, context, info in let parent = try cast(source, to: (any GraphQLGenerated.User).self) let filter = args["filter"] != .undefined ? try decoder.decode((String?).self, from: args["filter"]) : nil let scalar = try decoder.decode((GraphQLScalars.Scalar).self, from: args["scalar"]) @@ -299,7 +299,7 @@ struct SchemaGeneratorTests { ) let expected = """ - queryFields["user"]?.resolve = { source, args, context, info in + queryFields["user"]?.resolve = { @Sendable source, args, context, info in let id = try decoder.decode((String).self, from: args["id"]) let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Query.user(id: id, context: context, info: info) @@ -325,10 +325,10 @@ struct SchemaGeneratorTests { ) let expected = """ - subscriptionFields["messageAdded"]?.resolve = { source, _, _, _ in + subscriptionFields["messageAdded"]?.resolve = { @Sendable source, _, _, _ in return source } - subscriptionFields["messageAdded"]?.subscribe = { source, args, context, info in + subscriptionFields["messageAdded"]?.subscribe = { @Sendable source, args, context, info in let context = try cast(context, to: GraphQLContext.self) return try await Resolvers.Subscription.messageAdded(context: context, info: info) }