diff --git a/Package.resolved b/Package.resolved index 3e34fcf..7a5c615 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,33 +1,34 @@ { - "originHash" : "30951f6d77c03868bb74b0838ce93637391a168c6668a029c8a8a1dd9fb01aa5", - "pins" : [ - { - "identity" : "graphiti", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GraphQLSwift/Graphiti.git", - "state" : { - "revision" : "a23a3d232df202fc158ad2d698926325b470523c", - "version" : "3.0.0" + "object": { + "pins": [ + { + "package": "Graphiti", + "repositoryURL": "https://github.com/GraphQLSwift/Graphiti.git", + "state": { + "branch": null, + "revision": "a23a3d232df202fc158ad2d698926325b470523c", + "version": "3.0.0" + } + }, + { + "package": "GraphQL", + "repositoryURL": "https://github.com/GraphQLSwift/GraphQL.git", + "state": { + "branch": null, + "revision": "397c0f43a1eb6a401858f896263288375efcf0bd", + "version": "4.1.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version": "1.3.0" + } } - }, - { - "identity" : "graphql", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GraphQLSwift/GraphQL.git", - "state" : { - "revision" : "0fe18bc0bbbc9ab8929c285f419adea7c8fc7da2", - "version" : "4.0.1" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", - "version" : "1.2.1" - } - } - ], - "version" : 3 + ] + }, + "version": 1 } diff --git a/Sources/GraphQLTransportWS/Client.swift b/Sources/GraphQLTransportWS/Client.swift index bcd82f1..9b0e816 100644 --- a/Sources/GraphQLTransportWS/Client.swift +++ b/Sources/GraphQLTransportWS/Client.swift @@ -70,7 +70,7 @@ public class Client { return } try await self.onComplete(completeResponse, self) - case .unknown: + default: try await self.error(.invalidType()) } } diff --git a/Sources/GraphQLTransportWS/GraphqlTransportWSError.swift b/Sources/GraphQLTransportWS/GraphqlTransportWSError.swift index 3fda638..61775a1 100644 --- a/Sources/GraphQLTransportWS/GraphqlTransportWSError.swift +++ b/Sources/GraphQLTransportWS/GraphqlTransportWSError.swift @@ -60,14 +60,14 @@ struct GraphQLTransportWSError: Error { static func invalidRequestFormat(messageType: RequestMessageType) -> Self { return self.init( - "Request message doesn't match '\(messageType.rawValue)' JSON format", + "Request message doesn't match '\(messageType.type.rawValue)' JSON format", code: .invalidRequestFormat ) } static func invalidResponseFormat(messageType: ResponseMessageType) -> Self { return self.init( - "Response message doesn't match '\(messageType.rawValue)' JSON format", + "Response message doesn't match '\(messageType.type.rawValue)' JSON format", code: .invalidResponseFormat ) } diff --git a/Sources/GraphQLTransportWS/Requests.swift b/Sources/GraphQLTransportWS/Requests.swift index 98267ca..09665b6 100644 --- a/Sources/GraphQLTransportWS/Requests.swift +++ b/Sources/GraphQLTransportWS/Requests.swift @@ -1,53 +1,105 @@ import Foundation import GraphQL -/// We also require that an 'authToken' field is provided in the 'payload' during the connection -/// init message. For example: -/// ``` -/// { -/// "type": 'connection_init', -/// "payload": { -/// "authToken": "eyJhbGciOiJIUz..." -/// } -/// } -/// ``` - /// A general request. This object's type is used to triage to other, more specific request objects. -struct Request: Equatable, JsonEncodable { - let type: RequestMessageType +public struct Request: Equatable, JsonEncodable { + public let type: RequestMessageType } /// A websocket `connection_init` request from the client to the server -struct ConnectionInitRequest: Equatable, JsonEncodable { - var type = RequestMessageType.connectionInit - let payload: InitPayload +public struct ConnectionInitRequest: Equatable, JsonEncodable { + public let type: RequestMessageType = .connectionInit + public let payload: InitPayload + + public init(payload: InitPayload) { + self.payload = payload + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(RequestMessageType.self, forKey: .type) != .connectionInit { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(RequestMessageType.connectionInit.type)`" + )) + } + payload = try container.decode(InitPayload.self, forKey: .payload) + } } /// A websocket `subscribe` request from the client to the server -struct SubscribeRequest: Equatable, JsonEncodable { - var type = RequestMessageType.subscribe - let payload: GraphQLRequest - let id: String +public struct SubscribeRequest: Equatable, JsonEncodable { + public let type = RequestMessageType.subscribe + public let payload: GraphQLRequest + public let id: String + + public init(payload: GraphQLRequest, id: String) { + self.payload = payload + self.id = id + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(RequestMessageType.self, forKey: .type) != .subscribe { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(RequestMessageType.subscribe.type)`" + )) + } + payload = try container.decode(GraphQLRequest.self, forKey: .payload) + id = try container.decode(String.self, forKey: .id) + } } /// A websocket `complete` request from the client to the server -struct CompleteRequest: Equatable, JsonEncodable { - var type = RequestMessageType.complete - let id: String +public struct CompleteRequest: Equatable, JsonEncodable { + public let type = RequestMessageType.complete + public let id: String + + public init(id: String) { + self.id = id + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(RequestMessageType.self, forKey: .type) != .complete { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(RequestMessageType.complete.type)`" + )) + } + id = try container.decode(String.self, forKey: .id) + } } /// The supported websocket request message types from the client to the server -enum RequestMessageType: String, Codable { - case connectionInit = "connection_init" - case subscribe - case complete - case unknown - - init(from decoder: Decoder) throws { - guard let value = try? decoder.singleValueContainer().decode(String.self) else { - self = .unknown - return - } - self = RequestMessageType(rawValue: value) ?? .unknown +public struct RequestMessageType: Equatable, Codable, Sendable { + // This is implemented as a struct with only public static properties, backed by an internal enum + // in order to grow the list of accepted request types in a non-breaking way. + + let type: RequestType + + init(type: RequestType) { + self.type = type + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + type = try container.decode(RequestType.self) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(type) + } + + public static let connectionInit: Self = .init(type: .connectionInit) + public static let subscribe: Self = .init(type: .subscribe) + public static let complete: Self = .init(type: .complete) + + enum RequestType: String, Codable { + case connectionInit = "connection_init" + case subscribe + case complete } } diff --git a/Sources/GraphQLTransportWS/Responses.swift b/Sources/GraphQLTransportWS/Responses.swift index 34da4fd..71d7d4f 100644 --- a/Sources/GraphQLTransportWS/Responses.swift +++ b/Sources/GraphQLTransportWS/Responses.swift @@ -2,48 +2,79 @@ import Foundation import GraphQL /// A general response. This object's type is used to triage to other, more specific response objects. -struct Response: Equatable, JsonEncodable { - let type: ResponseMessageType +public struct Response: Equatable, JsonEncodable { + public let type: ResponseMessageType } /// A websocket `connection_ack` response from the server to the client public struct ConnectionAckResponse: Equatable, JsonEncodable { - let type: ResponseMessageType + public let type: ResponseMessageType = .connectionAck public let payload: [String: Map]? - init(_ payload: [String: Map]? = nil) { - type = .connectionAck + public init(payload: [String: Map]? = nil) { self.payload = payload } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(ResponseMessageType.self, forKey: .type) != .connectionAck { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(ResponseMessageType.connectionAck.type)`" + )) + } + payload = try container.decodeIfPresent([String: Map].self, forKey: .payload) + } } /// A websocket `next` response from the server to the client public struct NextResponse: Equatable, JsonEncodable { - let type: ResponseMessageType + public let type: ResponseMessageType = .next public let payload: GraphQLResult? public let id: String - init(_ payload: GraphQLResult? = nil, id: String) { - type = .next + public init(payload: GraphQLResult?, id: String) { self.payload = payload self.id = id } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(ResponseMessageType.self, forKey: .type) != .next { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(ResponseMessageType.next.type)`" + )) + } + payload = try container.decodeIfPresent(GraphQLResult.self, forKey: .payload) + id = try container.decode(String.self, forKey: .id) + } } /// A websocket `complete` response from the server to the client public struct CompleteResponse: Equatable, JsonEncodable { - let type: ResponseMessageType + public let type: ResponseMessageType = .complete public let id: String - init(id: String) { - type = .complete + public init(id: String) { self.id = id } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(ResponseMessageType.self, forKey: .type) != .complete { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(ResponseMessageType.complete.type)`" + )) + } + id = try container.decode(String.self, forKey: .id) + } } /// A websocket `error` response from the server to the client public struct ErrorResponse: Equatable, JsonEncodable { - let type: ResponseMessageType + public let type: ResponseMessageType = .error public let payload: [GraphQLError] public let id: String @@ -56,26 +87,54 @@ public struct ErrorResponse: Equatable, JsonEncodable { return GraphQLError(error) } } - type = .error payload = graphQLErrors self.id = id } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + if try container.decode(ResponseMessageType.self, forKey: .type) != .error { + throw DecodingError.dataCorrupted(.init( + codingPath: decoder.codingPath, + debugDescription: "type must be `\(ResponseMessageType.error.type)`" + )) + } + payload = try container.decode([GraphQLError].self, forKey: .payload) + id = try container.decode(String.self, forKey: .id) + } } /// The supported websocket response message types from the server to the client -enum ResponseMessageType: String, Codable { - case connectionAck = "connection_ack" - case next - case error - case complete - case unknown - - init(from decoder: Decoder) throws { - guard let value = try? decoder.singleValueContainer().decode(String.self) else { - self = .unknown - return - } - self = ResponseMessageType(rawValue: value) ?? .unknown +public struct ResponseMessageType: Equatable, Codable, Sendable { + // This is implemented as a struct with only public static properties, backed by an internal enum + // in order to grow the list of accepted response types in a non-breaking way. + + let type: ResponseType + + init(type: ResponseType) { + self.type = type + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + type = try container.decode(ResponseType.self) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(type) + } + + public static let connectionAck: Self = .init(type: .connectionAck) + public static let next: Self = .init(type: .next) + public static let complete: Self = .init(type: .complete) + public static let error: Self = .init(type: .error) + + enum ResponseType: String, Codable { + case connectionAck = "connection_ack" + case next + case complete + case error } } diff --git a/Sources/GraphQLTransportWS/Server.swift b/Sources/GraphQLTransportWS/Server.swift index 74db3d5..751b863 100644 --- a/Sources/GraphQLTransportWS/Server.swift +++ b/Sources/GraphQLTransportWS/Server.swift @@ -89,7 +89,7 @@ public class Server< return } try await self.onOperationComplete(completeRequest) - case .unknown: + default: try await self.error(.invalidType()) } } @@ -214,7 +214,7 @@ public class Server< private func sendConnectionAck(_ payload: [String: Map]? = nil) async throws { guard let messenger = messenger else { return } try await messenger.send( - ConnectionAckResponse(payload).toJSON(encoder) + ConnectionAckResponse(payload: payload).toJSON(encoder) ) } @@ -223,7 +223,7 @@ public class Server< guard let messenger = messenger else { return } try await messenger.send( NextResponse( - payload, + payload: payload, id: id ).toJSON(encoder) ) diff --git a/Tests/GraphQLTransportWSTests/GraphQLTransportWSTests.swift b/Tests/GraphQLTransportWSTests/GraphQLTransportWSTests.swift index e26de4c..d8bc65b 100644 --- a/Tests/GraphQLTransportWSTests/GraphQLTransportWSTests.swift +++ b/Tests/GraphQLTransportWSTests/GraphQLTransportWSTests.swift @@ -3,7 +3,7 @@ import Foundation import GraphQL import XCTest -@testable import GraphQLTransportWS +import GraphQLTransportWS class GraphqlTransportWSTests: XCTestCase { var clientMessenger: TestMessenger!