Skip to content

Commit 5c195aa

Browse files
Initial commit
0 parents  commit 5c195aa

15 files changed

+1021
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>IDEDidComputeMac32BitWarning</key>
6+
<true/>
7+
</dict>
8+
</plist>

Package.resolved

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"object": {
3+
"pins": [
4+
{
5+
"package": "Graphiti",
6+
"repositoryURL": "https://github.com/GraphQLSwift/Graphiti.git",
7+
"state": {
8+
"branch": null,
9+
"revision": "c9bc9d1cc9e62e71a824dc178630bfa8b8a6e2a4",
10+
"version": "1.0.0"
11+
}
12+
},
13+
{
14+
"package": "GraphQL",
15+
"repositoryURL": "https://github.com/NeedleInAJayStack/GraphQL.git",
16+
"state": {
17+
"branch": "fix/GraphQLRequest",
18+
"revision": "239211cf822585e48c31081f1378c809608ec5b8",
19+
"version": null
20+
}
21+
},
22+
{
23+
"package": "GraphQLRxSwift",
24+
"repositoryURL": "https://github.com/GraphQLSwift/GraphQLRxSwift.git",
25+
"state": {
26+
"branch": null,
27+
"revision": "c7ec6595f92ef5d77c06852e4acc4cd46a753622",
28+
"version": "0.0.4"
29+
}
30+
},
31+
{
32+
"package": "RxSwift",
33+
"repositoryURL": "https://github.com/ReactiveX/RxSwift.git",
34+
"state": {
35+
"branch": null,
36+
"revision": "b4307ba0b6425c0ba4178e138799946c3da594f8",
37+
"version": "6.5.0"
38+
}
39+
},
40+
{
41+
"package": "swift-collections",
42+
"repositoryURL": "https://github.com/apple/swift-collections",
43+
"state": {
44+
"branch": null,
45+
"revision": "48254824bb4248676bf7ce56014ff57b142b77eb",
46+
"version": "1.0.2"
47+
}
48+
},
49+
{
50+
"package": "swift-nio",
51+
"repositoryURL": "https://github.com/apple/swift-nio.git",
52+
"state": {
53+
"branch": null,
54+
"revision": "6aa9347d9bc5bbfe6a84983aec955c17ffea96ef",
55+
"version": "2.33.0"
56+
}
57+
}
58+
]
59+
},
60+
"version": 1
61+
}

Package.swift

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// swift-tools-version:5.4
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "GraphQLWS",
7+
products: [
8+
.library(
9+
name: "GraphQLTransportWS",
10+
targets: ["GraphQLTransportWS"]
11+
),
12+
],
13+
dependencies: [
14+
.package(name: "Graphiti", url: "https://github.com/GraphQLSwift/Graphiti.git", from: "1.0.0"),
15+
// TODO: Mainline when this PR is merged: https://github.com/GraphQLSwift/GraphQL/pull/97
16+
.package(name: "GraphQL", url: "https://github.com/NeedleInAJayStack/GraphQL.git", .branch("fix/GraphQLRequest")),
17+
.package(name: "GraphQLRxSwift", url: "https://github.com/GraphQLSwift/GraphQLRxSwift.git", from: "0.0.4"),
18+
.package(name: "RxSwift", url: "https://github.com/ReactiveX/RxSwift.git", from: "6.1.0"),
19+
.package(name: "swift-nio", url: "https://github.com/apple/swift-nio.git", .upToNextMinor(from: "2.33.0")),
20+
],
21+
targets: [
22+
.target(
23+
name: "GraphQLTransportWS",
24+
dependencies: [
25+
.product(name: "Graphiti", package: "Graphiti"),
26+
.product(name: "GraphQLRxSwift", package: "GraphQLRxSwift"),
27+
.product(name: "GraphQL", package: "GraphQL"),
28+
.product(name: "NIO", package: "swift-nio"),
29+
.product(name: "RxSwift", package: "RxSwift")
30+
]),
31+
.testTarget(
32+
name: "GraphQLTransportWSTests",
33+
dependencies: ["GraphQLTransportWS"]
34+
),
35+
]
36+
)

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# GraphQLTransportWS
2+
3+
A description of this package.
+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2021 PassiveLogic, Inc.
2+
3+
import Foundation
4+
5+
/// Adds client-side [graphql-transport-ws protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)
6+
/// support, namely parsing and adding callbacks for each type of server respose.
7+
class Client {
8+
let onMessage: (String) -> Void
9+
let onConnectionAck: (ConnectionAckResponse) -> Void
10+
let onNext: (NextResponse) -> Void
11+
let onError: (ErrorResponse) -> Void
12+
let onComplete: (CompleteResponse) -> Void
13+
14+
let decoder = JSONDecoder()
15+
16+
/// Create a new client.
17+
///
18+
/// - Parameters:
19+
/// - onMessage: callback run on receipt of any message
20+
/// - onConnectionAck: callback run on receipt of a `connection_ack` message
21+
/// - onNext: callback run on receipt of a `next` message
22+
/// - onError: callback run on receipt of an `error` message
23+
/// - onComplete: callback run on receipt of a `complete` message
24+
init(
25+
onMessage: @escaping (String) -> Void = { _ in () },
26+
onConnectionAck: @escaping (ConnectionAckResponse) -> Void = { _ in () },
27+
onNext: @escaping (NextResponse) -> Void = { _ in () },
28+
onError: @escaping (ErrorResponse) -> Void = { _ in () },
29+
onComplete: @escaping (CompleteResponse) -> Void = { _ in () }
30+
) {
31+
self.onMessage = onMessage
32+
self.onConnectionAck = onConnectionAck
33+
self.onNext = onNext
34+
self.onError = onError
35+
self.onComplete = onComplete
36+
}
37+
38+
func attach(to messenger: Messenger) {
39+
messenger.onRecieve { message in
40+
self.onMessage(message)
41+
42+
// Detect and ignore error responses.
43+
if message.starts(with: "44") {
44+
// TODO: Determine what to do with returned error messages
45+
return
46+
}
47+
48+
guard let json = message.data(using: .utf8) else {
49+
let error = GraphqlTransportWsError.invalidEncoding()
50+
messenger.error(error.message, code: error.code)
51+
return
52+
}
53+
54+
let response: Response
55+
do {
56+
response = try self.decoder.decode(Response.self, from: json)
57+
}
58+
catch {
59+
let error = GraphqlTransportWsError.noType()
60+
messenger.error(error.message, code: error.code)
61+
return
62+
}
63+
64+
switch response.type {
65+
case .connectionAck:
66+
guard let connectionAckResponse = try? self.decoder.decode(ConnectionAckResponse.self, from: json) else {
67+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .connectionAck)
68+
messenger.error(error.message, code: error.code)
69+
return
70+
}
71+
self.onConnectionAck(connectionAckResponse)
72+
case .next:
73+
guard let nextResponse = try? self.decoder.decode(NextResponse.self, from: json) else {
74+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .next)
75+
messenger.error(error.message, code: error.code)
76+
return
77+
}
78+
self.onNext(nextResponse)
79+
case .error:
80+
guard let errorResponse = try? self.decoder.decode(ErrorResponse.self, from: json) else {
81+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .error)
82+
messenger.error(error.message, code: error.code)
83+
return
84+
}
85+
self.onError(errorResponse)
86+
case .complete:
87+
guard let completeResponse = try? self.decoder.decode(CompleteResponse.self, from: json) else {
88+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .complete)
89+
messenger.error(error.message, code: error.code)
90+
return
91+
}
92+
self.onComplete(completeResponse)
93+
case .unknown:
94+
let error = GraphqlTransportWsError.invalidType()
95+
messenger.error(error.message, code: error.code)
96+
}
97+
}
98+
}
99+
}
100+
101+
extension Messenger {
102+
/// - Parameters:
103+
/// - onMessage: callback run on receipt of any message
104+
/// - onConnectionAck: callback run on receipt of a `connection_ack` message
105+
/// - onNext: callback run on receipt of a `next` message
106+
/// - onError: callback run on receipt of an `error` message
107+
/// - onComplete: callback run on receipt of a `complete` message
108+
func graphQLTransportWSClient(
109+
onMessage: @escaping (String) -> Void = { _ in () },
110+
onConnectionAck: @escaping (ConnectionAckResponse) -> Void = { _ in () },
111+
onNext: @escaping (NextResponse) -> Void = { _ in () },
112+
onError: @escaping (ErrorResponse) -> Void = { _ in () },
113+
onComplete: @escaping (CompleteResponse) -> Void = { _ in () }
114+
) {
115+
let decoder = JSONDecoder()
116+
117+
self.onRecieve { message in
118+
onMessage(message)
119+
120+
// Detect and ignore error responses.
121+
if message.starts(with: "44") {
122+
// TODO: Determine what to do with returned error messages
123+
return
124+
}
125+
126+
guard let json = message.data(using: .utf8) else {
127+
let error = GraphqlTransportWsError.invalidEncoding()
128+
self.error(error.message, code: error.code)
129+
return
130+
}
131+
132+
let response: Response
133+
do {
134+
response = try decoder.decode(Response.self, from: json)
135+
}
136+
catch {
137+
let error = GraphqlTransportWsError.noType()
138+
self.error(error.message, code: error.code)
139+
return
140+
}
141+
142+
switch response.type {
143+
case .connectionAck:
144+
guard let connectionAckResponse = try? decoder.decode(ConnectionAckResponse.self, from: json) else {
145+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .connectionAck)
146+
self.error(error.message, code: error.code)
147+
return
148+
}
149+
onConnectionAck(connectionAckResponse)
150+
case .next:
151+
guard let nextResponse = try? decoder.decode(NextResponse.self, from: json) else {
152+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .next)
153+
self.error(error.message, code: error.code)
154+
return
155+
}
156+
onNext(nextResponse)
157+
case .error:
158+
guard let errorResponse = try? decoder.decode(ErrorResponse.self, from: json) else {
159+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .error)
160+
self.error(error.message, code: error.code)
161+
return
162+
}
163+
onError(errorResponse)
164+
case .complete:
165+
guard let completeResponse = try? decoder.decode(CompleteResponse.self, from: json) else {
166+
let error = GraphqlTransportWsError.invalidResponseFormat(messageType: .complete)
167+
self.error(error.message, code: error.code)
168+
return
169+
}
170+
onComplete(completeResponse)
171+
case .unknown:
172+
let error = GraphqlTransportWsError.invalidType()
173+
self.error(error.message, code: error.code)
174+
}
175+
}
176+
}
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) 2021 PassiveLogic, Inc.
2+
3+
struct GraphqlTransportWsError: Error {
4+
let message: String
5+
let code: Int
6+
7+
init(_ message: String, code: Int) {
8+
self.message = message
9+
self.code = code
10+
}
11+
12+
static func unauthorized() -> Self {
13+
return self.init(
14+
"Unauthorized",
15+
code: 4401
16+
)
17+
}
18+
19+
static func tooManyInitializations() -> Self {
20+
return self.init(
21+
"Too many initialisation requests",
22+
code: 4429
23+
)
24+
}
25+
26+
static func notInitialized() -> Self {
27+
return self.init(
28+
"Connection not initialized",
29+
code: 4407
30+
)
31+
}
32+
33+
static func subscriberAlreadyExists(id: String) -> Self {
34+
return self.init(
35+
"Subscriber for \(id) already exists",
36+
code: 4409
37+
)
38+
}
39+
40+
static func invalidEncoding() -> Self {
41+
return self.init(
42+
"Message was not encoded in UTF8",
43+
code: 4400
44+
)
45+
}
46+
47+
static func noType() -> Self {
48+
return self.init(
49+
"Message has no 'type' field",
50+
code: 4400
51+
)
52+
}
53+
54+
static func invalidType() -> Self {
55+
return self.init(
56+
"Message 'type' value does not match supported types",
57+
code: 4400
58+
)
59+
}
60+
61+
static func invalidRequestFormat(messageType: RequestMessageType) -> Self {
62+
return self.init(
63+
"Request message doesn't match '\(messageType.rawValue)' JSON format",
64+
code: 4400
65+
)
66+
}
67+
68+
static func invalidResponseFormat(messageType: ResponseMessageType) -> Self {
69+
return self.init(
70+
"Response message doesn't match '\(messageType.rawValue)' JSON format",
71+
code: 4400
72+
)
73+
}
74+
75+
static func internalAPIStreamIssue() -> Self {
76+
return self.init(
77+
"API Response did not result in a stream type",
78+
code: 4400
79+
)
80+
}
81+
82+
static func graphQLError(_ error: Error) -> Self {
83+
return self.init(
84+
"\(error)",
85+
code: 4400
86+
)
87+
}
88+
}

0 commit comments

Comments
 (0)