Skip to content

Commit 185ee56

Browse files
Merge pull request #9 from GraphQLSwift/feat/custom-coders
Adds custom encoder/decoder support
2 parents c7c0b7e + 22de539 commit 185ee56

7 files changed

Lines changed: 136 additions & 26 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,25 @@ let app = Application(
127127
```
128128

129129
The example above follows Hummingbird best practices when it uses a separate router for HTTP and WebSocket requests. For more details, see the [Hummingbird WebSocket documentation](https://docs.hummingbird.codes/2.0/documentation/hummingbird/websocketserverupgrade#Overview).
130+
131+
### Custom Encoding/Decoding
132+
133+
You can set custom encoders and decoders using the `Config.coders` argument:
134+
135+
```swift
136+
// Configure custom JSON encoder
137+
let graphQLJSONEncoder = GraphQLJSONEncoder()
138+
graphQLJSONEncoder.dateEncodingStrategy = .millisecondsSince1970
139+
140+
// Inject it using the `config.coders` argument
141+
router.graphql(
142+
schema: schema,
143+
config: .init(
144+
coders: .init(graphQLJSONEncoder: graphQLJSONEncoder)
145+
)
146+
) { _, _ in
147+
GraphQLContext()
148+
}
149+
```
150+
151+
Like [Hummingbird](https://docs.hummingbird.codes/2.0/documentation/hummingbird/responseencoding#Date-encoding), all default GraphQL encoders and decoders use the standard settings with ISO8601 date formatting.

Sources/GraphQLHummingbird/GraphQLConfig.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import Foundation
12
import GraphQL
3+
import Hummingbird
24

35
/// Configuration options for GraphQLHummingbird
46
public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Sendable {
57
let allowGet: Bool
68
let allowMissingAcceptHeader: Bool
9+
let coders: Coders
710
let ide: IDE
811
let subscriptionProtocols: Set<SubscriptionProtocol>
912
let websocket: WebSocket
@@ -20,6 +23,7 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
2023
public init(
2124
allowGet: Bool = true,
2225
allowMissingAcceptHeader: Bool = false,
26+
coders: Coders = .init(),
2327
ide: IDE = .graphiql,
2428
subscriptionProtocols: Set<SubscriptionProtocol> = [.websocket],
2529
websocket: WebSocket = .init(
@@ -31,11 +35,34 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
3135
self.allowGet = allowGet
3236
self.allowMissingAcceptHeader = allowMissingAcceptHeader
3337
self.additionalValidationRules = additionalValidationRules
38+
self.coders = coders
3439
self.ide = ide
3540
self.subscriptionProtocols = subscriptionProtocols
3641
self.websocket = websocket
3742
}
3843

44+
public struct Coders: Sendable {
45+
public let graphQLJSONEncoder: GraphQLJSONEncoder
46+
public let jsonDecoder: JSONDecoder
47+
public let jsonEncoder: JSONEncoder
48+
public let urlEncodedFormDecoder: URLEncodedFormDecoder
49+
public let urlEncodedFormEncoder: URLEncodedFormEncoder
50+
51+
public init(
52+
graphQLJSONEncoder: GraphQLJSONEncoder? = nil,
53+
jsonDecoder: JSONDecoder? = nil,
54+
jsonEncoder: JSONEncoder? = nil,
55+
urlEncodedFormDecoder: URLEncodedFormDecoder? = nil,
56+
urlEncodedFormEncoder: URLEncodedFormEncoder? = nil
57+
) {
58+
self.graphQLJSONEncoder = graphQLJSONEncoder ?? defaultGraphQLJSONEncoder
59+
self.jsonDecoder = jsonDecoder ?? defaultJSONDecoder
60+
self.jsonEncoder = jsonEncoder ?? defaultJSONEncoder
61+
self.urlEncodedFormDecoder = urlEncodedFormDecoder ?? defaultURLEncodedFormDecoder
62+
self.urlEncodedFormEncoder = urlEncodedFormEncoder ?? defaultURLEncodedFormEncoder
63+
}
64+
}
65+
3966
public struct IDE: Sendable, Equatable {
4067
/// GraphiQL: https://github.com/graphql/graphiql
4168
public static var graphiql: Self {
@@ -80,3 +107,33 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
80107
}
81108
}
82109
}
110+
111+
let defaultGraphQLJSONEncoder: GraphQLJSONEncoder = {
112+
let encoder = GraphQLJSONEncoder()
113+
encoder.dateEncodingStrategy = .iso8601
114+
return encoder
115+
}()
116+
117+
let defaultJSONDecoder: JSONDecoder = {
118+
let decoder = JSONDecoder()
119+
decoder.dateDecodingStrategy = .iso8601
120+
return decoder
121+
}()
122+
123+
let defaultJSONEncoder: JSONEncoder = {
124+
let encoder = JSONEncoder()
125+
encoder.dateEncodingStrategy = .iso8601
126+
return encoder
127+
}()
128+
129+
let defaultURLEncodedFormDecoder: URLEncodedFormDecoder = {
130+
var decoder = URLEncodedFormDecoder()
131+
decoder.dateDecodingStrategy = .iso8601
132+
return decoder
133+
}()
134+
135+
let defaultURLEncodedFormEncoder: URLEncodedFormEncoder = {
136+
var encoder = URLEncodedFormEncoder()
137+
encoder.dateEncodingStrategy = .iso8601
138+
return encoder
139+
}()

Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ extension GraphQLHandler {
4444
switch mediaType {
4545
case .applicationJson, .applicationJsonGraphQL:
4646
do {
47-
graphQLRequest = try await JSONDecoder().decode(GraphQLRequest.self, from: request, context: context)
47+
graphQLRequest = try await config.coders.jsonDecoder.decode(GraphQLRequest.self, from: request, context: context)
4848
} catch {
4949
throw HTTPError(.badRequest, message: error.localizedDescription)
5050
}
5151
case .applicationUrlEncoded:
5252
do {
53-
graphQLRequest = try await URLEncodedFormDecoder().decode(GraphQLRequest.self, from: request, context: context)
53+
graphQLRequest = try await config.coders.urlEncodedFormDecoder.decode(GraphQLRequest.self, from: request, context: context)
5454
} catch {
5555
throw HTTPError(.badRequest, message: error.localizedDescription)
5656
}
@@ -109,19 +109,19 @@ extension GraphQLHandler {
109109
// Try to respond with the best matching media type, in order
110110
for mediaType in acceptedTypes {
111111
if MediaType.applicationJsonGraphQL.isType(mediaType) {
112-
return try GraphQLJSONEncoder().encode(result, from: request, context: context)
112+
return try config.coders.graphQLJSONEncoder.encode(result, from: request, context: context)
113113
}
114114
if MediaType.applicationJson.isType(mediaType) {
115-
return try JSONEncoder().encode(result, from: request, context: context)
115+
return try config.coders.jsonEncoder.encode(result, from: request, context: context)
116116
}
117117
if MediaType.applicationUrlEncoded.isType(mediaType) {
118-
return try URLEncodedFormEncoder().encode(result, from: request, context: context)
118+
return try config.coders.urlEncodedFormEncoder.encode(result, from: request, context: context)
119119
}
120120
}
121121

122122
// Use the default if configured to do so
123123
if config.allowMissingAcceptHeader {
124-
return try GraphQLJSONEncoder().encode(result, from: request, context: context)
124+
return try config.coders.graphQLJSONEncoder.encode(result, from: request, context: context)
125125
}
126126

127127
// Fail

Tests/GraphQLHummingbirdTests/HTTPStatusCodeGraphQLJSONTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ struct HTTPStatusCodeGraphQLJSONTests {
181181
uri: "/graphql",
182182
method: .post,
183183
headers: jsonGraphQLHeaders,
184-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ error }")))
184+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ error }")))
185185
) { response in
186186
#expect(response.status == .ok)
187187
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
188188

189-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
189+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
190190
#expect(!result.errors.isEmpty)
191191
#expect(result.errors.first?.message == "Something went wrong")
192192
}

Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,12 @@ struct HTTPStatusCodeJSONTests {
181181
uri: "/graphql",
182182
method: .post,
183183
headers: jsonGraphQLHeaders,
184-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ error }")))
184+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ error }")))
185185
) { response in
186186
#expect(response.status == .ok)
187187
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
188188

189-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
189+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
190190
#expect(!result.errors.isEmpty)
191191
#expect(result.errors.first?.message == "Something went wrong")
192192
}

Tests/GraphQLHummingbirdTests/HTTPTests.swift

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ struct HTTPTests {
2222
uri: "/graphql",
2323
method: .post,
2424
headers: jsonGraphQLHeaders,
25-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
25+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
2626
) { response in
2727
#expect(response.status == .ok)
2828
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
2929

30-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
30+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
3131
#expect(result.data?["hello"] == "World")
3232
#expect(result.errors.isEmpty)
3333
}
@@ -66,15 +66,15 @@ struct HTTPTests {
6666
uri: "/graphql",
6767
method: .post,
6868
headers: jsonGraphQLHeaders,
69-
body: .init(data: JSONEncoder().encode(GraphQLRequest(
69+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(
7070
query: "query Greet($name: String) { greet(name: $name) }",
7171
variables: ["name": "Alice"]
7272
)))
7373
) { response in
7474
#expect(response.status == .ok)
7575
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
7676

77-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
77+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
7878
#expect(result.data?["greet"] == "Hello, Alice")
7979
#expect(result.errors.isEmpty)
8080
}
@@ -114,12 +114,12 @@ struct HTTPTests {
114114
uri: "/graphql",
115115
method: .post,
116116
headers: jsonGraphQLHeaders,
117-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ contextMessage }")))
117+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ contextMessage }")))
118118
) { response in
119119
#expect(response.status == .ok)
120120
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
121121

122-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
122+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
123123
#expect(result.data?["contextMessage"] == "Hello from context!")
124124
#expect(result.errors.isEmpty)
125125
}
@@ -141,12 +141,12 @@ struct HTTPTests {
141141
.accept: MediaType.applicationJson.description,
142142
.contentType: MediaType.applicationJsonGraphQL.description,
143143
],
144-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
144+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
145145
) { response in
146146
#expect(response.status == .ok)
147147
#expect(response.headers[.contentType] == "application/json; charset=utf-8")
148148

149-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
149+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
150150
#expect(result.data?["hello"] == "World")
151151
#expect(result.errors.isEmpty)
152152
}
@@ -168,12 +168,12 @@ struct HTTPTests {
168168
.accept: MediaType.applicationJsonGraphQL.description,
169169
.contentType: MediaType.applicationJson.description,
170170
],
171-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
171+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
172172
) { response in
173173
#expect(response.status == .ok)
174174
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
175175

176-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
176+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
177177
#expect(result.data?["hello"] == "World")
178178
#expect(result.errors.isEmpty)
179179
}
@@ -194,7 +194,7 @@ struct HTTPTests {
194194
headers: [
195195
.contentType: MediaType.applicationJsonGraphQL.description,
196196
],
197-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
197+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
198198
) { response in
199199
#expect(response.status == .notAcceptable)
200200
}
@@ -215,12 +215,12 @@ struct HTTPTests {
215215
headers: [
216216
.contentType: MediaType.applicationJsonGraphQL.description,
217217
],
218-
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
218+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
219219
) { response in
220220
#expect(response.status == .ok)
221221
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
222222

223-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
223+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
224224
#expect(result.data?["hello"] == "World")
225225
#expect(result.errors.isEmpty)
226226
}
@@ -242,7 +242,7 @@ struct HTTPTests {
242242
) { response in
243243
#expect(response.status == .ok)
244244

245-
let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
245+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
246246
#expect(result.data?["hello"] == "World")
247247
#expect(result.errors.isEmpty)
248248
}
@@ -311,4 +311,35 @@ struct HTTPTests {
311311
}
312312
}
313313
}
314+
315+
@Test func customEncoder() async throws {
316+
let graphQLJSONEncoder = GraphQLJSONEncoder()
317+
graphQLJSONEncoder.dateEncodingStrategy = .secondsSince1970
318+
let router = Router()
319+
router.graphql(
320+
schema: helloWorldSchema,
321+
config: .init(
322+
coders: .init(graphQLJSONEncoder: graphQLJSONEncoder)
323+
)
324+
) { _, _ in
325+
EmptyContext()
326+
}
327+
let app = Application(router: router)
328+
329+
try await app.test(.router) { client in
330+
try await client.execute(
331+
uri: "/graphql",
332+
method: .post,
333+
headers: jsonGraphQLHeaders,
334+
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
335+
) { response in
336+
#expect(response.status == .ok)
337+
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")
338+
339+
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
340+
#expect(result.data?["hello"] == "World")
341+
#expect(result.errors.isEmpty)
342+
}
343+
}
344+
}
314345
}

Tests/GraphQLHummingbirdTests/WebSocketTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import Testing
1111

1212
@Suite
1313
struct WebSocketTests {
14-
let decoder = JSONDecoder()
15-
let encoder = GraphQLJSONEncoder()
14+
let decoder = defaultJSONDecoder
15+
let encoder = defaultGraphQLJSONEncoder
1616

1717
@Test func subscription() async throws {
1818
let pubsub = SimplePubSub<String>()

0 commit comments

Comments
 (0)