Skip to content

Commit 54bbca1

Browse files
doc: Adds protocol docs and readme usage examples
1 parent 9aea1cc commit 54bbca1

File tree

4 files changed

+139
-7
lines changed

4 files changed

+139
-7
lines changed

README.md

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

33
This implements the [graphql-ws WebSocket subprotocol](https://github.com/apollographql/subscriptions-transport-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: "git@gitlab.com:PassiveLogic/platform/GraphQLWS.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 GraphQLWS
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 = GraphQLWS.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 = GraphQLWS.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/GraphQLWS/Messenger.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +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
16+
17+
/// Close the messenger
1118
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
1224
func error(_ message: String, code: Int) -> Void
1325
}

Sources/GraphQLWS/Server.swift

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

99
/// Server implements the server-side portion of the protocol, allowing a few callbacks for customization.
10+
///
11+
/// By default, there are no authorization checks
1012
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?

Tests/GraphQLWSTests/GraphQLWSTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ class GraphqlWsTests: XCTestCase {
1414
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

0 commit comments

Comments
 (0)