Skip to content

Commit c3f398b

Browse files
committed
Merge branch 'feature/OpenSourcePrep' into 'master'
Open Source Prep See merge request PassiveLogic/platform/graphqltransportws!5
2 parents 16eaa0b + 3639dec commit c3f398b

File tree

9 files changed

+168
-44
lines changed

9 files changed

+168
-44
lines changed

README.md

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,136 @@
11
# GraphQLTransportWS
22

33
This implements the [graphql-transport-ws WebSocket subprotocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md).
4-
It is mainly intended for Server support, but there is a basic client implementation included.
4+
It is mainly intended for server support, but there is a basic client implementation included.
55

66
Features:
77
- Server implementation that implements defined protocol conversations
88
- Client and Server types that wrap messengers
99
- Codable Server and Client message structures
10+
- Custom authentication support
11+
12+
## Usage
13+
14+
To use this package, include it in your `Package.swift` dependencies:
15+
16+
```swift
17+
.package(url: "[email protected]:PassiveLogic/platform/GraphQLTransportWS.git", from: "<version>")
18+
```
19+
20+
Then create a class to implement the `Messenger` protocol. Here's an example using
21+
[`WebSocketKit`](https://github.com/vapor/websocket-kit):
22+
23+
```swift
24+
import WebSocketKit
25+
import GraphQLTransportWS
26+
27+
/// Messenger wrapper for WebSockets
28+
class WebSocketMessenger: Messenger {
29+
private weak var websocket: WebSocket?
30+
private var onRecieve: (String) -> Void = { _ in }
31+
32+
init(websocket: WebSocket) {
33+
self.websocket = websocket
34+
websocket.onText { _, message in
35+
self.onRecieve(message)
36+
}
37+
}
38+
39+
func send<S>(_ message: S) where S: Collection, S.Element == Character {
40+
guard let websocket = websocket else { return }
41+
websocket.send(message)
42+
}
43+
44+
func onRecieve(callback: @escaping (String) -> Void) {
45+
self.onRecieve = callback
46+
}
47+
48+
func error(_ message: String, code: Int) {
49+
guard let websocket = websocket else { return }
50+
websocket.send("\(code): \(message)")
51+
}
52+
53+
func close() {
54+
guard let websocket = websocket else { return }
55+
_ = websocket.close()
56+
}
57+
}
58+
```
59+
60+
Next create a `Server`, provide the messenger you just defined, and wrap the API `execute` and `subscribe` commands:
61+
62+
```swift
63+
routes.webSocket(
64+
"graphqlSubscribe",
65+
onUpgrade: { request, websocket in
66+
let messenger = WebSocketMessenger(websocket: websocket)
67+
let server = GraphQLTransportWS.Server<EmptyInitPayload?>(
68+
messenger: messenger,
69+
onExecute: { graphQLRequest in
70+
api.execute(
71+
request: graphQLRequest.query,
72+
context: context,
73+
on: self.eventLoop,
74+
variables: graphQLRequest.variables,
75+
operationName: graphQLRequest.operationName
76+
)
77+
},
78+
onSubscribe: { graphQLRequest in
79+
api.subscribe(
80+
request: graphQLRequest.query,
81+
context: context,
82+
on: self.eventLoop,
83+
variables: graphQLRequest.variables,
84+
operationName: graphQLRequest.operationName
85+
)
86+
}
87+
)
88+
}
89+
)
90+
```
91+
92+
### Authentication
93+
94+
This package exposes authentication hooks on the `connection_init` message. To perform custom authentication,
95+
provide a codable type to the Server init and define an `auth` callback on the server. For example:
96+
97+
```swift
98+
struct UsernameAndPasswordInitPayload: Equatable & Codable {
99+
let username: String
100+
let password: String
101+
}
102+
103+
let server = GraphQLTransportWS.Server<UsernameAndPasswordInitPayload>(
104+
messenger: messenger,
105+
onExecute: { ... },
106+
onSubscribe: { ... }
107+
)
108+
server.auth { payload in
109+
guard payload.username == "admin" else {
110+
throw Abort(.unauthorized)
111+
}
112+
}
113+
```
114+
115+
This example would require `connection_init` message from the client to look like this:
116+
117+
```json
118+
{
119+
"type": "connection_init",
120+
"payload": {
121+
"username": "admin",
122+
"password": "supersafe"
123+
}
124+
}
125+
```
126+
127+
If the `payload` field is not required on your server, you may make Server's generic declaration optional like `Server<Payload?>`
10128

11129
## Memory Management
12130

13-
Memory ownership among the Server, Client, and Messager may seem a little backwards. This is because the Swift/Vapor WebSocket
131+
Memory ownership among the Server, Client, and Messenger may seem a little backwards. This is because the Swift/Vapor WebSocket
14132
implementation persists WebSocket objects long after their callback and they are expected to retain strong memory references to the
15-
objects required for responses. In order to align cleanly and avoid memory cycles, Server and Client are injected strongly into Messager
16-
callbacks, and only hold weak references to their Messager. This means that Messager objects (or their enclosing WebSocket) must
17-
be persisted to have the connected Server or Client objects function. That is, if a Server's Messager falls out of scope and deinitializes,
133+
objects required for responses. In order to align cleanly and avoid memory cycles, Server and Client are injected strongly into Messenger
134+
callbacks, and only hold weak references to their Messenger. This means that Messenger objects (or their enclosing WebSocket) must
135+
be persisted to have the connected Server or Client objects function. That is, if a Server's Messenger falls out of scope and deinitializes,
18136
the Server will no longer respond to messages.

Sources/GraphQLTransportWS/Client.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Foundation
44
import GraphQL
55

66
/// Client is an open-ended implementation of the client side of the protocol. It parses and adds callbacks for each type of server respose.
7-
public class Client {
7+
public class Client<InitPayload: Equatable & Codable> {
88
// We keep this weak because we strongly inject this object into the messenger callback
99
weak var messenger: Messenger?
1010

@@ -110,7 +110,7 @@ public class Client {
110110
}
111111

112112
/// Send a `connection_init` request through the messenger
113-
public func sendConnectionInit(payload: ConnectionInitAuth?) {
113+
public func sendConnectionInit(payload: InitPayload) {
114114
guard let messenger = messenger else { return }
115115
messenger.send(
116116
ConnectionInitRequest(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Contains convenient `connection_init` payloads for users of this package
2+
3+
/// `connection_init` `payload` that is empty
4+
public struct EmptyInitPayload: Equatable & Codable { }
5+
6+
/// `connection_init` `payload` that includes an `authToken` field
7+
public struct TokenInitPayload: Equatable & Codable {
8+
let authToken: String
9+
}

Sources/GraphQLTransportWS/Messenger.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@
33
import Foundation
44
import NIO
55

6-
/// Protocol for an object that can send and recieve messages
6+
/// Protocol for an object that can send and recieve messages. This allows mocking in tests.
77
public protocol Messenger: AnyObject {
88
// AnyObject compliance requires that the implementing object is a class and we can reference it weakly
9+
10+
/// Send a message through this messenger
11+
/// - Parameter message: The message to send
912
func send<S>(_ message: S) -> Void where S: Collection, S.Element == Character
13+
14+
/// Set the callback that should be run when a message is recieved
1015
func onRecieve(callback: @escaping (String) -> Void) -> Void
11-
func onClose(callback: @escaping () -> Void) -> Void
16+
17+
/// Close the messenger
1218
func close() -> Void
19+
20+
/// Indicate that the messenger experienced an error.
21+
/// - Parameters:
22+
/// - message: The message describing the error
23+
/// - code: An error code
1324
func error(_ message: String, code: Int) -> Void
1425
}

Sources/GraphQLTransportWS/Requests.swift

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,14 @@ import GraphQL
1515
/// ```
1616

1717
/// A general request. This object's type is used to triage to other, more specific request objects.
18-
public struct Request: Equatable, JsonEncodable {
18+
struct Request: Equatable, JsonEncodable {
1919
let type: RequestMessageType
2020
}
2121

2222
/// A websocket `connection_init` request from the client to the server
23-
public struct ConnectionInitRequest: Equatable, JsonEncodable {
23+
struct ConnectionInitRequest<InitPayload: Codable & Equatable>: Equatable, JsonEncodable {
2424
var type = RequestMessageType.connectionInit
25-
public let payload: ConnectionInitAuth?
26-
}
27-
28-
// TODO: Make this structure user-defined
29-
/// Authorization format for a websocket `connection_init` request from the client to the server
30-
public struct ConnectionInitAuth: Equatable, JsonEncodable {
31-
public let authToken: String
32-
33-
public init(authToken: String) {
34-
self.authToken = authToken
35-
}
25+
let payload: InitPayload
3626
}
3727

3828
/// A websocket `subscribe` request from the client to the server

Sources/GraphQLTransportWS/Server.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ import NIO
77
import RxSwift
88

99
/// Server implements the server-side portion of the protocol, allowing a few callbacks for customization.
10-
public class Server {
10+
///
11+
/// By default, there are no authorization checks
12+
public class Server<InitPayload: Equatable & Codable> {
1113
// We keep this weak because we strongly inject this object into the messenger callback
1214
weak var messenger: Messenger?
1315

1416
let onExecute: (GraphQLRequest) -> EventLoopFuture<GraphQLResult>
1517
let onSubscribe: (GraphQLRequest) -> EventLoopFuture<SubscriptionResult>
1618

17-
var auth: (ConnectionInitRequest) throws -> Void = { _ in }
19+
var auth: (InitPayload) throws -> Void = { _ in }
1820
var onExit: () -> Void = { }
1921
var onMessage: (String) -> Void = { _ in }
2022

@@ -64,7 +66,7 @@ public class Server {
6466

6567
switch request.type {
6668
case .connectionInit:
67-
guard let connectionInitRequest = try? self.decoder.decode(ConnectionInitRequest.self, from: data) else {
69+
guard let connectionInitRequest = try? self.decoder.decode(ConnectionInitRequest<InitPayload>.self, from: data) else {
6870
self.error(.invalidRequestFormat(messageType: .connectionInit))
6971
return
7072
}
@@ -89,7 +91,7 @@ public class Server {
8991

9092
/// Define the callback run during `connection_init` resolution that allows authorization using the `payload`.
9193
/// Throw to indicate that authorization has failed. /// - Parameter callback: The callback to assign
92-
public func auth(_ callback: @escaping (ConnectionInitRequest) throws -> Void) {
94+
public func auth(_ callback: @escaping (InitPayload) throws -> Void) {
9395
self.auth = callback
9496
}
9597

@@ -105,14 +107,14 @@ public class Server {
105107
self.onMessage = callback
106108
}
107109

108-
private func onConnectionInit(_ connectionInitRequest: ConnectionInitRequest) {
110+
private func onConnectionInit(_ connectionInitRequest: ConnectionInitRequest<InitPayload>) {
109111
guard !initialized else {
110112
self.error(.tooManyInitializations())
111113
return
112114
}
113115

114116
do {
115-
try self.auth(connectionInitRequest)
117+
try self.auth(connectionInitRequest.payload)
116118
}
117119
catch {
118120
self.error(.unauthorized())

Tests/GraphQLTransportWSTests/GraphQLTransportWSTests.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,20 @@ import XCTest
1111
class GraphqlTransportWSTests: XCTestCase {
1212
var clientMessenger: TestMessenger!
1313
var serverMessenger: TestMessenger!
14-
var server: Server!
14+
var server: Server<TokenInitPayload>!
1515

1616
override func setUp() {
17+
// Point the client and server at each other
1718
clientMessenger = TestMessenger()
1819
serverMessenger = TestMessenger()
19-
2020
clientMessenger.other = serverMessenger
2121
serverMessenger.other = clientMessenger
2222

2323
let eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: 1).next()
2424
let api = TestAPI()
2525
let context = TestContext()
2626

27-
server = Server(
27+
server = Server<TokenInitPayload>(
2828
messenger: serverMessenger,
2929
onExecute: { graphQLRequest in
3030
api.execute(
@@ -48,7 +48,7 @@ class GraphqlTransportWSTests: XCTestCase {
4848
var messages = [String]()
4949
let completeExpectation = XCTestExpectation()
5050

51-
let client = Client(messenger: clientMessenger)
51+
let client = Client<TokenInitPayload>(messenger: clientMessenger)
5252
client.onMessage { message, _ in
5353
messages.append(message)
5454
completeExpectation.fulfill()
@@ -81,14 +81,14 @@ class GraphqlTransportWSTests: XCTestCase {
8181
var messages = [String]()
8282
let completeExpectation = XCTestExpectation()
8383

84-
let client = Client(messenger: clientMessenger)
84+
let client = Client<TokenInitPayload>(messenger: clientMessenger)
8585
client.onMessage { message, _ in
8686
messages.append(message)
8787
completeExpectation.fulfill()
8888
}
8989

9090
client.sendConnectionInit(
91-
payload: ConnectionInitAuth(
91+
payload: TokenInitPayload(
9292
authToken: ""
9393
)
9494
)
@@ -107,7 +107,7 @@ class GraphqlTransportWSTests: XCTestCase {
107107
var messages = [String]()
108108
let completeExpectation = XCTestExpectation()
109109

110-
let client = Client(messenger: clientMessenger)
110+
let client = Client<TokenInitPayload>(messenger: clientMessenger)
111111
client.onConnectionAck { _, client in
112112
client.sendStart(
113113
payload: GraphQLRequest(
@@ -131,7 +131,7 @@ class GraphqlTransportWSTests: XCTestCase {
131131
}
132132

133133
client.sendConnectionInit(
134-
payload: ConnectionInitAuth(
134+
payload: TokenInitPayload(
135135
authToken: ""
136136
)
137137
)
@@ -154,7 +154,7 @@ class GraphqlTransportWSTests: XCTestCase {
154154
var dataIndex = 1
155155
let dataIndexMax = 3
156156

157-
let client = Client(messenger: clientMessenger)
157+
let client = Client<TokenInitPayload>(messenger: clientMessenger)
158158
client.onConnectionAck { _, client in
159159
client.sendStart(
160160
payload: GraphQLRequest(
@@ -191,7 +191,7 @@ class GraphqlTransportWSTests: XCTestCase {
191191
}
192192

193193
client.sendConnectionInit(
194-
payload: ConnectionInitAuth(
194+
payload: TokenInitPayload(
195195
authToken: ""
196196
)
197197
)

Tests/GraphQLTransportWSTests/TestMessenger.swift renamed to Tests/GraphQLTransportWSTests/Utils/TestMessenger.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import Foundation
1010
class TestMessenger: Messenger {
1111
weak var other: TestMessenger?
1212
var onRecieve: (String) -> Void = { _ in }
13-
var onClose: () -> Void = { }
1413
let queue: DispatchQueue = .init(label: "Test messenger")
1514

1615
init() {}
@@ -30,16 +29,11 @@ class TestMessenger: Messenger {
3029
self.onRecieve = callback
3130
}
3231

33-
func onClose(callback: @escaping () -> Void) {
34-
self.onClose = callback
35-
}
36-
3732
func error(_ message: String, code: Int) {
3833
self.send("\(code): \(message)")
3934
}
4035

4136
func close() {
4237
// This is a testing no-op
43-
self.onClose()
4438
}
4539
}

0 commit comments

Comments
 (0)