Skip to content

Draft: feat!: Uses swift concurrency under the hood #152

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 5 additions & 32 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import PackageDescription

let package = Package(
name: "Graphiti",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)],
products: [
.library(name: "Graphiti", targets: ["Graphiti"]),
],
dependencies: [
.package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "3.0.0"),
// TODO: Mainline after merge: https://github.com/GraphQLSwift/GraphQL/pull/166
.package(
url: "https://github.com/NeedleInAJayStack/GraphQL.git",
branch: "feat/swift-concurrency"
),
],
targets: [
.target(name: "Graphiti", dependencies: ["GraphQL"]),
Expand Down
42 changes: 9 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Graphiti
# Graphiti

Graphiti is a Swift library for building GraphQL schemas fast, safely and easily.

Expand Down Expand Up @@ -86,7 +86,7 @@ struct MessageAPI : API {
let resolver: Resolver
let schema: Schema<Resolver, Context>
}

let api = MessageAPI(
resolver: Resolver()
schema: try! Schema<Resolver, Context> {
Expand Down Expand Up @@ -124,7 +124,7 @@ let api = MessageAPI(
resolver: Resolver()
schema: schema
)
```
```

</details>
<details>
Expand All @@ -136,7 +136,7 @@ final class ChatSchema: PartialSchema<Resolver, Context> {
public override var types: Types {
Type(Message.self) {
Field("content", at: \.content)
}
}
}

@FieldDefinitions
Expand All @@ -152,7 +152,7 @@ let api = MessageAPI(
resolver: Resolver()
schema: schema
)
```
```

</details>

Expand All @@ -164,7 +164,7 @@ let chatSchema = PartialSchema<Resolver, Context>(
types: {
Type(Message.self) {
Field("content", at: \.content)
}
}
},
query: {
Field("message", at: Resolver.message)
Expand All @@ -178,7 +178,7 @@ let api = MessageAPI(
resolver: Resolver()
schema: schema
)
```
```

</details>

Expand All @@ -190,20 +190,10 @@ let api = MessageAPI(

#### Querying

To query the schema we need to pass in a NIO EventLoopGroup to feed the execute function alongside the query itself.

```swift
import NIO

let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
defer {
try? group.syncShutdownGracefully()
}

let result = try await api.execute(
request: "{ message { content } }",
context: Context(),
on: group
context: Context()
)
print(result)
```
Expand All @@ -228,27 +218,13 @@ struct Resolver {
}
```

#### NIO resolvers

The resolver functions also support `NIO`-style concurrency. To do so, just add one more parameter with type `EventLoopGroup` to the resolver function and change the return type to `EventLoopFuture<YouReturnType>`. Don't forget to import NIO.

```swift
import NIO

struct Resolver {
func message(context: Context, arguments: NoArguments, group: EventLoopGroup) -> EventLoopFuture<Message> {
group.next().makeSucceededFuture(context.message())
}
}
```

#### Subscription

This library supports GraphQL subscriptions, and supports them through the Swift Concurrency `AsyncThrowingStream` type. See the [Usage Guide](UsageGuide.md#subscriptions) for details.

If you are unable to use Swift Concurrency, you must create a concrete subclass of the `EventStream` class that implements event streaming
functionality. If you don't feel like creating a subclass yourself, you can use the [GraphQLRxSwift](https://github.com/GraphQLSwift/GraphQLRxSwift) repository
to integrate [RxSwift](https://github.com/ReactiveX/RxSwift) observables out-of-the-box. Or you can use that repository as a reference to connect a different
to integrate [RxSwift](https://github.com/ReactiveX/RxSwift) observables out-of-the-box. Or you can use that repository as a reference to connect a different
stream library like [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift), [OpenCombine](https://github.com/OpenCombine/OpenCombine), or
one that you've created yourself.

Expand Down
89 changes: 2 additions & 87 deletions Sources/Graphiti/API/API.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import GraphQL
import NIO

public protocol API {
associatedtype Resolver
Expand All @@ -12,80 +11,6 @@ public extension API {
func execute(
request: String,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
variables: [String: Map] = [:],
operationName: String? = nil,
validationRules: [(ValidationContext) -> Visitor] = []
) -> EventLoopFuture<GraphQLResult> {
return schema.execute(
request: request,
resolver: resolver,
context: context,
eventLoopGroup: eventLoopGroup,
variables: variables,
operationName: operationName,
validationRules: validationRules
)
}

func execute(
request: GraphQLRequest,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
validationRules: [(ValidationContext) -> Visitor] = []
) -> EventLoopFuture<GraphQLResult> {
return execute(
request: request.query,
context: context,
on: eventLoopGroup,
variables: request.variables,
operationName: request.operationName,
validationRules: validationRules
)
}

func subscribe(
request: String,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
variables: [String: Map] = [:],
operationName: String? = nil,
validationRules: [(ValidationContext) -> Visitor] = []
) -> EventLoopFuture<SubscriptionResult> {
return schema.subscribe(
request: request,
resolver: resolver,
context: context,
eventLoopGroup: eventLoopGroup,
variables: variables,
operationName: operationName,
validationRules: validationRules
)
}

func subscribe(
request: GraphQLRequest,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
validationRules: [(ValidationContext) -> Visitor] = []
) -> EventLoopFuture<SubscriptionResult> {
return subscribe(
request: request.query,
context: context,
on: eventLoopGroup,
variables: request.variables,
operationName: request.operationName,
validationRules: validationRules
)
}
}

public extension API {
@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
func execute(
request: String,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
variables: [String: Map] = [:],
operationName: String? = nil,
validationRules: [(ValidationContext) -> Visitor] = []
Expand All @@ -94,35 +19,29 @@ public extension API {
request: request,
resolver: resolver,
context: context,
eventLoopGroup: eventLoopGroup,
variables: variables,
operationName: operationName,
validationRules: validationRules
).get()
)
}

@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
func execute(
request: GraphQLRequest,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
validationRules: [(ValidationContext) -> Visitor] = []
) async throws -> GraphQLResult {
return try await execute(
request: request.query,
context: context,
on: eventLoopGroup,
variables: request.variables,
operationName: request.operationName,
validationRules: validationRules
)
}

@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
func subscribe(
request: String,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
variables: [String: Map] = [:],
operationName: String? = nil,
validationRules: [(ValidationContext) -> Visitor] = []
Expand All @@ -131,24 +50,20 @@ public extension API {
request: request,
resolver: resolver,
context: context,
eventLoopGroup: eventLoopGroup,
variables: variables,
operationName: operationName,
validationRules: validationRules
).get()
)
}

@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *)
func subscribe(
request: GraphQLRequest,
context: ContextType,
on eventLoopGroup: EventLoopGroup,
validationRules: [(ValidationContext) -> Visitor] = []
) async throws -> SubscriptionResult {
return try await subscribe(
request: request.query,
context: context,
on: eventLoopGroup,
variables: request.variables,
operationName: request.operationName,
validationRules: validationRules
Expand Down
50 changes: 0 additions & 50 deletions Sources/Graphiti/Connection/Connection.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import Foundation
import GraphQL
import NIO

public struct Connection<Node> {
public let edges: [Edge<Node>]
public let pageInfo: PageInfo
}

@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable
public extension Connection where Node: Identifiable, Node.ID: LosslessStringConvertible {
static func id(_ cursor: String) -> Node.ID? {
cursor.base64Decoded().flatMap { Node.ID($0) }
Expand All @@ -18,54 +16,6 @@ public extension Connection where Node: Identifiable, Node.ID: LosslessStringCon
}
}

@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable
public extension EventLoopFuture where Value: Sequence, Value.Element: Identifiable,
Value.Element.ID: LosslessStringConvertible {
func connection(from arguments: Paginatable) -> EventLoopFuture<Connection<Value.Element>> {
connection(from: arguments, makeCursor: Connection<Value.Element>.cursor)
}

func connection(from arguments: ForwardPaginatable)
-> EventLoopFuture<Connection<Value.Element>> {
connection(from: arguments, makeCursor: Connection<Value.Element>.cursor)
}

func connection(from arguments: BackwardPaginatable)
-> EventLoopFuture<Connection<Value.Element>> {
connection(from: arguments, makeCursor: Connection<Value.Element>.cursor)
}
}

public extension EventLoopFuture where Value: Sequence {
func connection(
from arguments: Paginatable,
makeCursor: @escaping (Value.Element) throws -> String
) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments, makeCursor: makeCursor)
}
}

func connection(
from arguments: ForwardPaginatable,
makeCursor: @escaping (Value.Element) throws -> String
) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments, makeCursor: makeCursor)
}
}

func connection(
from arguments: BackwardPaginatable,
makeCursor: @escaping (Value.Element) throws -> String
) -> EventLoopFuture<Connection<Value.Element>> {
flatMapThrowing { value in
try value.connection(from: arguments, makeCursor: makeCursor)
}
}
}

@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable
public extension Sequence where Element: Identifiable,
Element.ID: LosslessStringConvertible {
func connection(from arguments: Paginatable) throws -> Connection<Element> {
Expand Down
Loading
Loading