Skip to content
Merged
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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,25 @@ let app = Application(
```

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).

### Custom Encoding/Decoding

You can set custom encoders and decoders using the `Config.coders` argument:

```swift
// Configure custom JSON encoder
let graphQLJSONEncoder = GraphQLJSONEncoder()
graphQLJSONEncoder.dateEncodingStrategy = .millisecondsSince1970

// Inject it using the `config.coders` argument
router.graphql(
schema: schema,
config: .init(
coders: .init(graphQLJSONEncoder: graphQLJSONEncoder)
)
) { _, _ in
GraphQLContext()
}
```

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.
57 changes: 57 additions & 0 deletions Sources/GraphQLHummingbird/GraphQLConfig.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import Foundation
import GraphQL
import Hummingbird

/// Configuration options for GraphQLHummingbird
public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Sendable {
let allowGet: Bool
let allowMissingAcceptHeader: Bool
let coders: Coders
let ide: IDE
let subscriptionProtocols: Set<SubscriptionProtocol>
let websocket: WebSocket
Expand All @@ -20,6 +23,7 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
public init(
allowGet: Bool = true,
allowMissingAcceptHeader: Bool = false,
coders: Coders = .init(),
ide: IDE = .graphiql,
subscriptionProtocols: Set<SubscriptionProtocol> = [.websocket],
websocket: WebSocket = .init(
Expand All @@ -31,11 +35,34 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
self.allowGet = allowGet
self.allowMissingAcceptHeader = allowMissingAcceptHeader
self.additionalValidationRules = additionalValidationRules
self.coders = coders
self.ide = ide
self.subscriptionProtocols = subscriptionProtocols
self.websocket = websocket
}

public struct Coders: Sendable {
public let graphQLJSONEncoder: GraphQLJSONEncoder
public let jsonDecoder: JSONDecoder
public let jsonEncoder: JSONEncoder
public let urlEncodedFormDecoder: URLEncodedFormDecoder
public let urlEncodedFormEncoder: URLEncodedFormEncoder

public init(
graphQLJSONEncoder: GraphQLJSONEncoder? = nil,
jsonDecoder: JSONDecoder? = nil,
jsonEncoder: JSONEncoder? = nil,
urlEncodedFormDecoder: URLEncodedFormDecoder? = nil,
urlEncodedFormEncoder: URLEncodedFormEncoder? = nil
) {
self.graphQLJSONEncoder = graphQLJSONEncoder ?? defaultGraphQLJSONEncoder
self.jsonDecoder = jsonDecoder ?? defaultJSONDecoder
self.jsonEncoder = jsonEncoder ?? defaultJSONEncoder
self.urlEncodedFormDecoder = urlEncodedFormDecoder ?? defaultURLEncodedFormDecoder
self.urlEncodedFormEncoder = urlEncodedFormEncoder ?? defaultURLEncodedFormEncoder
}
}

public struct IDE: Sendable, Equatable {
/// GraphiQL: https://github.com/graphql/graphiql
public static var graphiql: Self {
Expand Down Expand Up @@ -80,3 +107,33 @@ public struct GraphQLConfig<WebSocketInit: Equatable & Codable & Sendable>: Send
}
}
}

let defaultGraphQLJSONEncoder: GraphQLJSONEncoder = {
let encoder = GraphQLJSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}()

let defaultJSONDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()

let defaultJSONEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}()

let defaultURLEncodedFormDecoder: URLEncodedFormDecoder = {
var decoder = URLEncodedFormDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()

let defaultURLEncodedFormEncoder: URLEncodedFormEncoder = {
var encoder = URLEncodedFormEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}()
12 changes: 6 additions & 6 deletions Sources/GraphQLHummingbird/HTTP/GraphQLHandler+HTTP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ extension GraphQLHandler {
switch mediaType {
case .applicationJson, .applicationJsonGraphQL:
do {
graphQLRequest = try await JSONDecoder().decode(GraphQLRequest.self, from: request, context: context)
graphQLRequest = try await config.coders.jsonDecoder.decode(GraphQLRequest.self, from: request, context: context)
} catch {
throw HTTPError(.badRequest, message: error.localizedDescription)
}
case .applicationUrlEncoded:
do {
graphQLRequest = try await URLEncodedFormDecoder().decode(GraphQLRequest.self, from: request, context: context)
graphQLRequest = try await config.coders.urlEncodedFormDecoder.decode(GraphQLRequest.self, from: request, context: context)
} catch {
throw HTTPError(.badRequest, message: error.localizedDescription)
}
Expand Down Expand Up @@ -109,19 +109,19 @@ extension GraphQLHandler {
// Try to respond with the best matching media type, in order
for mediaType in acceptedTypes {
if MediaType.applicationJsonGraphQL.isType(mediaType) {
return try GraphQLJSONEncoder().encode(result, from: request, context: context)
return try config.coders.graphQLJSONEncoder.encode(result, from: request, context: context)
}
if MediaType.applicationJson.isType(mediaType) {
return try JSONEncoder().encode(result, from: request, context: context)
return try config.coders.jsonEncoder.encode(result, from: request, context: context)
}
if MediaType.applicationUrlEncoded.isType(mediaType) {
return try URLEncodedFormEncoder().encode(result, from: request, context: context)
return try config.coders.urlEncodedFormEncoder.encode(result, from: request, context: context)
}
}

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

// Fail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ struct HTTPStatusCodeGraphQLJSONTests {
uri: "/graphql",
method: .post,
headers: jsonGraphQLHeaders,
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ error }")))
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ error }")))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(!result.errors.isEmpty)
#expect(result.errors.first?.message == "Something went wrong")
}
Expand Down
4 changes: 2 additions & 2 deletions Tests/GraphQLHummingbirdTests/HTTPStatusCodeJSONTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,12 @@ struct HTTPStatusCodeJSONTests {
uri: "/graphql",
method: .post,
headers: jsonGraphQLHeaders,
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ error }")))
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ error }")))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(!result.errors.isEmpty)
#expect(result.errors.first?.message == "Something went wrong")
}
Expand Down
59 changes: 45 additions & 14 deletions Tests/GraphQLHummingbirdTests/HTTPTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ struct HTTPTests {
uri: "/graphql",
method: .post,
headers: jsonGraphQLHeaders,
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(result.data?["hello"] == "World")
#expect(result.errors.isEmpty)
}
Expand Down Expand Up @@ -66,15 +66,15 @@ struct HTTPTests {
uri: "/graphql",
method: .post,
headers: jsonGraphQLHeaders,
body: .init(data: JSONEncoder().encode(GraphQLRequest(
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(
query: "query Greet($name: String) { greet(name: $name) }",
variables: ["name": "Alice"]
)))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

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

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

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

let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(result.data?["hello"] == "World")
#expect(result.errors.isEmpty)
}
Expand All @@ -194,7 +194,7 @@ struct HTTPTests {
headers: [
.contentType: MediaType.applicationJsonGraphQL.description,
],
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
) { response in
#expect(response.status == .notAcceptable)
}
Expand All @@ -215,12 +215,12 @@ struct HTTPTests {
headers: [
.contentType: MediaType.applicationJsonGraphQL.description,
],
body: .init(data: JSONEncoder().encode(GraphQLRequest(query: "{ hello }")))
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

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

let result = try JSONDecoder().decode(GraphQLResult.self, from: response.body)
let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(result.data?["hello"] == "World")
#expect(result.errors.isEmpty)
}
Expand Down Expand Up @@ -311,4 +311,35 @@ struct HTTPTests {
}
}
}

@Test func customEncoder() async throws {
let graphQLJSONEncoder = GraphQLJSONEncoder()
graphQLJSONEncoder.dateEncodingStrategy = .secondsSince1970
let router = Router()
router.graphql(
schema: helloWorldSchema,
config: .init(
coders: .init(graphQLJSONEncoder: graphQLJSONEncoder)
)
) { _, _ in
EmptyContext()
}
let app = Application(router: router)

try await app.test(.router) { client in
try await client.execute(
uri: "/graphql",
method: .post,
headers: jsonGraphQLHeaders,
body: .init(data: defaultJSONEncoder.encode(GraphQLRequest(query: "{ hello }")))
) { response in
#expect(response.status == .ok)
#expect(response.headers[.contentType] == "application/graphql-response+json; charset=utf-8")

let result = try defaultJSONDecoder.decode(GraphQLResult.self, from: response.body)
#expect(result.data?["hello"] == "World")
#expect(result.errors.isEmpty)
}
}
}
}
4 changes: 2 additions & 2 deletions Tests/GraphQLHummingbirdTests/WebSocketTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import Testing

@Suite
struct WebSocketTests {
let decoder = JSONDecoder()
let encoder = GraphQLJSONEncoder()
let decoder = defaultJSONDecoder
let encoder = defaultGraphQLJSONEncoder

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